Files
star-erp/app/Http/Controllers/ProductionOrderController.php

386 lines
16 KiB
PHP
Raw Normal View History

<?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)
{
2026-01-22 15:39:35 +08:00
$status = $request->input('status', 'draft'); // 預設為草稿
// 共用驗證規則
$baseRules = [
'product_id' => 'required|exists:products,id',
2026-01-22 15:39:35 +08:00
'output_batch_number' => 'required|string|max:50',
'status' => 'nullable|in:draft,completed',
];
// 完成模式需要完整驗證
$completedRules = [
'warehouse_id' => 'required|exists:warehouses,id',
'output_quantity' => 'required|numeric|min:0.01',
'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',
2026-01-22 15:39:35 +08:00
];
// 草稿模式的寬鬆規則
$draftRules = [
'warehouse_id' => 'nullable|exists:warehouses,id',
'output_quantity' => 'nullable|numeric|min:0',
'output_box_count' => 'nullable|string|max:10',
'production_date' => 'nullable|date',
'expiry_date' => 'nullable|date',
'remark' => 'nullable|string',
'items' => 'nullable|array',
'items.*.inventory_id' => 'nullable|exists:inventories,id',
'items.*.quantity_used' => 'nullable|numeric|min:0',
'items.*.unit_id' => 'nullable|exists:units,id',
];
$rules = $status === 'completed'
? array_merge($baseRules, $completedRules)
: array_merge($baseRules, $draftRules);
$validated = $request->validate($rules, [
'product_id.required' => '請選擇成品商品',
2026-01-22 15:39:35 +08:00
'output_batch_number.required' => '請輸入成品批號',
'warehouse_id.required' => '請選擇入庫倉庫',
'output_quantity.required' => '請輸入生產數量',
'production_date.required' => '請選擇生產日期',
'items.required' => '請至少新增一項原物料',
'items.min' => '請至少新增一項原物料',
]);
2026-01-22 15:39:35 +08:00
DB::transaction(function () use ($validated, $request, $status) {
// 1. 建立生產工單
$productionOrder = ProductionOrder::create([
'code' => ProductionOrder::generateCode(),
'product_id' => $validated['product_id'],
2026-01-22 15:39:35 +08:00
'warehouse_id' => $validated['warehouse_id'] ?? null,
'output_quantity' => $validated['output_quantity'] ?? 0,
'output_batch_number' => $validated['output_batch_number'],
'output_box_count' => $validated['output_box_count'] ?? null,
2026-01-22 15:39:35 +08:00
'production_date' => $validated['production_date'] ?? now()->toDateString(),
'expiry_date' => $validated['expiry_date'] ?? null,
'user_id' => auth()->id(),
2026-01-22 15:39:35 +08:00
'status' => $status,
'remark' => $validated['remark'] ?? null,
]);
2026-01-22 15:39:35 +08:00
// 2. 建立明細 (草稿與完成模式皆需儲存)
if (!empty($validated['items'])) {
foreach ($validated['items'] as $item) {
if (empty($item['inventory_id'])) continue;
// 建立明細
ProductionOrderItem::create([
'production_order_id' => $productionOrder->id,
'inventory_id' => $item['inventory_id'],
'quantity_used' => $item['quantity_used'] ?? 0,
'unit_id' => $item['unit_id'] ?? null,
]);
2026-01-22 15:39:35 +08:00
// 若為完成模式,則扣減原物料庫存
if ($status === 'completed') {
$inventory = Inventory::findOrFail($item['inventory_id']);
$inventory->decrement('quantity', $item['quantity_used']);
}
}
}
2026-01-22 15:39:35 +08:00
// 3. 若為完成模式,執行成品入庫
if ($status === 'completed') {
$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',
]);
}
});
2026-01-22 15:39:35 +08:00
$message = $status === 'completed'
? '生產單已建立,原物料已扣減,成品已入庫'
: '生產單草稿已儲存';
return redirect()->route('production-orders.index')
2026-01-22 15:39:35 +08:00
->with('success', $message);
}
/**
* 檢視生產單詳情(含追溯資訊)
*/
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)
{
2026-01-22 15:39:35 +08:00
$inventories = Inventory::with(['product.baseUnit', 'product.largeUnit'])
->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,
2026-01-22 15:39:35 +08:00
'base_unit_id' => $inv->product->base_unit_id,
'base_unit_name' => $inv->product->baseUnit?->name,
'large_unit_id' => $inv->product->large_unit_id,
'large_unit_name' => $inv->product->largeUnit?->name,
'conversion_rate' => $inv->product->conversion_rate,
];
});
return response()->json($inventories);
}
2026-01-22 15:39:35 +08:00
/**
* 編輯生產單(僅限草稿狀態)
*/
public function edit(ProductionOrder $productionOrder): Response
{
// 只有草稿可以編輯
if ($productionOrder->status !== 'draft') {
return redirect()->route('production-orders.show', $productionOrder->id)
->with('error', '只有草稿狀態的生產單可以編輯');
}
$productionOrder->load(['product', 'warehouse', 'items.inventory.product', 'items.unit']);
return Inertia::render('Production/Edit', [
'productionOrder' => $productionOrder,
'products' => Product::with(['baseUnit'])->get(),
'warehouses' => Warehouse::all(),
'units' => Unit::all(),
]);
}
/**
* 更新生產單
*/
public function update(Request $request, ProductionOrder $productionOrder)
{
// 只有草稿可以編輯
if ($productionOrder->status !== 'draft') {
return redirect()->route('production-orders.show', $productionOrder->id)
->with('error', '只有草稿狀態的生產單可以編輯');
}
$status = $request->input('status', 'draft');
// 共用驗證規則
$baseRules = [
'product_id' => 'required|exists:products,id',
'output_batch_number' => 'required|string|max:50',
'status' => 'nullable|in:draft,completed',
];
// 完成模式需要完整驗證
$completedRules = [
'warehouse_id' => 'required|exists:warehouses,id',
'output_quantity' => 'required|numeric|min:0.01',
'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',
];
// 草稿模式的寬鬆規則
$draftRules = [
'warehouse_id' => 'nullable|exists:warehouses,id',
'output_quantity' => 'nullable|numeric|min:0',
'output_box_count' => 'nullable|string|max:10',
'production_date' => 'nullable|date',
'expiry_date' => 'nullable|date',
'remark' => 'nullable|string',
'items' => 'nullable|array',
'items.*.inventory_id' => 'nullable|exists:inventories,id',
'items.*.quantity_used' => 'nullable|numeric|min:0',
'items.*.unit_id' => 'nullable|exists:units,id',
];
$rules = $status === 'completed'
? array_merge($baseRules, $completedRules)
: array_merge($baseRules, $draftRules);
$validated = $request->validate($rules, [
'product_id.required' => '請選擇成品商品',
'output_batch_number.required' => '請輸入成品批號',
'warehouse_id.required' => '請選擇入庫倉庫',
'output_quantity.required' => '請輸入生產數量',
'production_date.required' => '請選擇生產日期',
'items.required' => '請至少新增一項原物料',
'items.min' => '請至少新增一項原物料',
]);
DB::transaction(function () use ($validated, $status, $productionOrder) {
// 更新生產工單基本資料
$productionOrder->update([
'product_id' => $validated['product_id'],
'warehouse_id' => $validated['warehouse_id'] ?? null,
'output_quantity' => $validated['output_quantity'] ?? 0,
'output_batch_number' => $validated['output_batch_number'],
'output_box_count' => $validated['output_box_count'] ?? null,
'production_date' => $validated['production_date'] ?? now()->toDateString(),
'expiry_date' => $validated['expiry_date'] ?? null,
'status' => $status,
'remark' => $validated['remark'] ?? null,
]);
// 刪除舊的明細
$productionOrder->items()->delete();
// 重新建立明細 (草稿與完成模式皆需儲存)
if (!empty($validated['items'])) {
foreach ($validated['items'] as $item) {
if (empty($item['inventory_id'])) continue;
ProductionOrderItem::create([
'production_order_id' => $productionOrder->id,
'inventory_id' => $item['inventory_id'],
'quantity_used' => $item['quantity_used'] ?? 0,
'unit_id' => $item['unit_id'] ?? null,
]);
// 若為完成模式,則扣減原物料庫存
if ($status === 'completed') {
$inventory = Inventory::findOrFail($item['inventory_id']);
$inventory->decrement('quantity', $item['quantity_used']);
}
}
}
// 若為完成模式,執行成品入庫
if ($status === 'completed') {
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',
]);
}
});
$message = $status === 'completed'
? '生產單已完成,原物料已扣減,成品已入庫'
: '生產單草稿已更新';
return redirect()->route('production-orders.index')
->with('success', $message);
}
}