2026-01-21 17:19:36 +08:00
|
|
|
<?php
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
namespace App\Modules\Production\Controllers;
|
2026-01-21 17:19:36 +08:00
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
|
|
|
|
|
use App\Modules\Production\Models\ProductionOrder;
|
|
|
|
|
use App\Modules\Production\Models\ProductionOrderItem;
|
2026-01-26 14:59:24 +08:00
|
|
|
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
2026-01-27 08:59:45 +08:00
|
|
|
use App\Modules\Core\Contracts\CoreServiceInterface;
|
2026-01-29 16:13:56 +08:00
|
|
|
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
2026-01-21 17:19:36 +08:00
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Inertia\Response;
|
|
|
|
|
|
2026-01-29 16:13:56 +08:00
|
|
|
|
2026-01-21 17:19:36 +08:00
|
|
|
class ProductionOrderController extends Controller
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
protected $inventoryService;
|
2026-01-27 08:59:45 +08:00
|
|
|
protected $coreService;
|
2026-01-29 16:13:56 +08:00
|
|
|
protected $procurementService;
|
2026-01-26 14:59:24 +08:00
|
|
|
|
2026-01-29 16:13:56 +08:00
|
|
|
public function __construct(
|
|
|
|
|
InventoryServiceInterface $inventoryService,
|
|
|
|
|
CoreServiceInterface $coreService,
|
|
|
|
|
ProcurementServiceInterface $procurementService
|
|
|
|
|
)
|
2026-01-26 14:59:24 +08:00
|
|
|
{
|
|
|
|
|
$this->inventoryService = $inventoryService;
|
2026-01-27 08:59:45 +08:00
|
|
|
$this->coreService = $coreService;
|
2026-01-29 16:13:56 +08:00
|
|
|
$this->procurementService = $procurementService;
|
2026-01-26 14:59:24 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-21 17:19:36 +08:00
|
|
|
/**
|
|
|
|
|
* 生產工單列表
|
|
|
|
|
*/
|
|
|
|
|
public function index(Request $request): Response
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
// 不再使用 with(),避免跨模組 Eager Loading
|
|
|
|
|
$query = ProductionOrder::query();
|
2026-01-21 17:19:36 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 搜尋 (此處 orWhereHas 暫時保留,因 Laravel query builder 仍可作用於資料表層級,
|
|
|
|
|
// 但實務上若模組完全隔離,應考慮搜尋引擎或 ID 預選)
|
2026-01-21 17:19:36 +08:00
|
|
|
if ($request->filled('search')) {
|
|
|
|
|
$search = $request->search;
|
|
|
|
|
$query->where(function ($q) use ($search) {
|
2026-01-27 08:59:45 +08:00
|
|
|
$q->where('code', 'like', "%{$search}%")
|
|
|
|
|
->orWhere('output_batch_number', 'like', "%{$search}%");
|
|
|
|
|
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
|
|
|
|
|
$productIds = $this->inventoryService->getProductsByName($search)->pluck('id');
|
2026-01-26 14:59:24 +08:00
|
|
|
$q->orWhereIn('product_id', $productIds);
|
2026-01-21 17:19:36 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 狀態篩選
|
|
|
|
|
if ($request->filled('status') && $request->status !== 'all') {
|
|
|
|
|
$query->where('status', $request->status);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 排除軟刪除
|
|
|
|
|
$query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc'));
|
2026-01-21 17:19:36 +08:00
|
|
|
|
|
|
|
|
// 分頁
|
|
|
|
|
$perPage = $request->input('per_page', 10);
|
|
|
|
|
$productionOrders = $query->paginate($perPage)->withQueryString();
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// --- 手動資料水和 (Manual Hydration) ---
|
|
|
|
|
$productIds = $productionOrders->pluck('product_id')->unique()->filter()->toArray();
|
|
|
|
|
$warehouseIds = $productionOrders->pluck('warehouse_id')->unique()->filter()->toArray();
|
|
|
|
|
$userIds = $productionOrders->pluck('user_id')->unique()->filter()->toArray();
|
|
|
|
|
|
|
|
|
|
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
|
|
|
|
$warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds)->keyBy('id');
|
2026-01-27 08:59:45 +08:00
|
|
|
$users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
|
2026-01-26 14:59:24 +08:00
|
|
|
|
|
|
|
|
$productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users) {
|
|
|
|
|
$order->product = $products->get($order->product_id);
|
|
|
|
|
$order->warehouse = $warehouses->get($order->warehouse_id);
|
|
|
|
|
$order->user = $users->get($order->user_id);
|
|
|
|
|
return $order;
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-21 17:19:36 +08:00
|
|
|
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', [
|
2026-01-26 14:59:24 +08:00
|
|
|
'products' => $this->inventoryService->getAllProducts(),
|
|
|
|
|
'warehouses' => $this->inventoryService->getAllWarehouses(),
|
|
|
|
|
'units' => $this->inventoryService->getUnits(),
|
2026-01-21 17:19:36 +08:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 儲存生產單(含自動扣料與成品入庫)
|
|
|
|
|
*/
|
|
|
|
|
public function store(Request $request)
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
$status = $request->input('status', 'draft');
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-02-12 16:30:34 +08:00
|
|
|
$rules = [
|
2026-01-26 14:59:24 +08:00
|
|
|
'product_id' => 'required',
|
2026-01-22 15:39:35 +08:00
|
|
|
'status' => 'nullable|in:draft,completed',
|
2026-02-12 16:30:34 +08:00
|
|
|
'warehouse_id' => $status === 'completed' ? 'required' : 'nullable',
|
|
|
|
|
'output_quantity' => $status === 'completed' ? 'required|numeric|min:0.01' : 'nullable|numeric',
|
|
|
|
|
'items' => 'nullable|array',
|
|
|
|
|
'items.*.inventory_id' => $status === 'completed' ? 'required' : 'nullable',
|
|
|
|
|
'items.*.quantity_used' => $status === 'completed' ? 'required|numeric|min:0.0001' : 'nullable|numeric',
|
2026-01-22 15:39:35 +08:00
|
|
|
];
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
$validated = $request->validate($rules);
|
2026-01-21 17:19:36 +08:00
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
DB::transaction(function () use ($validated, $request, $status) {
|
2026-01-21 17:19:36 +08:00
|
|
|
// 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,
|
2026-02-12 16:30:34 +08:00
|
|
|
'output_batch_number' => $request->output_batch_number, // 建立時改為選填
|
2026-01-26 14:59:24 +08:00
|
|
|
'output_box_count' => $request->output_box_count,
|
2026-02-12 16:30:34 +08:00
|
|
|
'production_date' => $request->production_date,
|
2026-01-26 14:59:24 +08:00
|
|
|
'expiry_date' => $request->expiry_date,
|
2026-01-21 17:19:36 +08:00
|
|
|
'user_id' => auth()->id(),
|
2026-02-12 16:30:34 +08:00
|
|
|
'status' => ProductionOrder::STATUS_DRAFT, // 一律存為草稿
|
2026-01-26 14:59:24 +08:00
|
|
|
'remark' => $request->remark,
|
2026-01-21 17:19:36 +08:00
|
|
|
]);
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
activity()
|
|
|
|
|
->performedOn($productionOrder)
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
->log('created');
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 2. 處理明細
|
|
|
|
|
if (!empty($request->items)) {
|
|
|
|
|
foreach ($request->items as $item) {
|
2026-01-22 15:39:35 +08:00
|
|
|
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-21 17:19:36 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return redirect()->route('production-orders.index')
|
2026-02-12 16:30:34 +08:00
|
|
|
->with('success', '生產單草稿已建立');
|
2026-01-21 17:19:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-26 14:59:24 +08:00
|
|
|
* 檢視生產單詳情
|
2026-01-21 17:19:36 +08:00
|
|
|
*/
|
|
|
|
|
public function show(ProductionOrder $productionOrder): Response
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
// 手動水和主表資料
|
|
|
|
|
$productionOrder->product = $this->inventoryService->getProduct($productionOrder->product_id);
|
|
|
|
|
if ($productionOrder->product) {
|
|
|
|
|
$productionOrder->product->base_unit = $this->inventoryService->getUnits()->where('id', $productionOrder->product->base_unit_id)->first();
|
|
|
|
|
}
|
2026-02-12 16:30:34 +08:00
|
|
|
$productionOrder->warehouse = $productionOrder->warehouse_id
|
|
|
|
|
? $this->inventoryService->getWarehouse($productionOrder->warehouse_id)
|
|
|
|
|
: null;
|
2026-01-27 08:59:45 +08:00
|
|
|
$productionOrder->user = $this->coreService->getUser($productionOrder->user_id);
|
2026-01-26 14:59:24 +08:00
|
|
|
|
|
|
|
|
// 手動水和明細資料
|
|
|
|
|
$items = $productionOrder->items;
|
|
|
|
|
$inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray();
|
2026-01-29 16:13:56 +08:00
|
|
|
|
|
|
|
|
// 修正: 移除跨模組關聯 sourcePurchaseOrder.vendor
|
2026-01-26 14:59:24 +08:00
|
|
|
$inventories = $this->inventoryService->getInventoriesByIds(
|
|
|
|
|
$inventoryIds,
|
2026-02-12 16:30:34 +08:00
|
|
|
['product.baseUnit', 'warehouse']
|
2026-01-26 14:59:24 +08:00
|
|
|
)->keyBy('id');
|
|
|
|
|
|
2026-01-29 16:13:56 +08:00
|
|
|
// 手動載入 Purchase Orders
|
|
|
|
|
$poIds = $inventories->pluck('source_purchase_order_id')->unique()->filter()->toArray();
|
|
|
|
|
$purchaseOrders = collect();
|
|
|
|
|
if (!empty($poIds)) {
|
|
|
|
|
$purchaseOrders = $this->procurementService->getPurchaseOrdersByIds($poIds, ['vendor'])->keyBy('id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
$units = $this->inventoryService->getUnits()->keyBy('id');
|
|
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
$item->inventory = $inventories->get($item->inventory_id);
|
2026-01-29 16:13:56 +08:00
|
|
|
if ($item->inventory) {
|
|
|
|
|
// 手動掛載 PO
|
|
|
|
|
$poId = $item->inventory->source_purchase_order_id;
|
|
|
|
|
$item->inventory->sourcePurchaseOrder = $purchaseOrders->get($poId);
|
|
|
|
|
}
|
2026-01-26 14:59:24 +08:00
|
|
|
$item->unit = $units->get($item->unit_id);
|
|
|
|
|
}
|
2026-01-21 17:19:36 +08:00
|
|
|
|
|
|
|
|
return Inertia::render('Production/Show', [
|
|
|
|
|
'productionOrder' => $productionOrder,
|
2026-02-12 16:30:34 +08:00
|
|
|
'warehouses' => $this->inventoryService->getAllWarehouses(),
|
2026-01-21 17:19:36 +08:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-26 14:59:24 +08:00
|
|
|
* 取得倉庫內可用庫存
|
2026-01-21 17:19:36 +08:00
|
|
|
*/
|
2026-01-26 14:59:24 +08:00
|
|
|
public function getWarehouseInventories($warehouseId)
|
2026-01-21 17:19:36 +08:00
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
$inventories = $this->inventoryService->getInventoriesByWarehouse($warehouseId);
|
|
|
|
|
|
|
|
|
|
$data = $inventories->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 ? $inv->arrival_date->format('Y-m-d') : null,
|
|
|
|
|
'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
|
|
|
|
'unit_name' => $inv->product->baseUnit->name ?? '',
|
|
|
|
|
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
|
|
|
|
'large_unit_id' => $inv->product->large_unit_id ?? null,
|
|
|
|
|
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
|
|
|
|
];
|
|
|
|
|
});
|
2026-01-21 17:19:36 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
return response()->json($data);
|
2026-01-21 17:19:36 +08:00
|
|
|
}
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-02-04 13:08:05 +08:00
|
|
|
/**
|
|
|
|
|
* 取得商品在各倉庫的庫存分佈
|
|
|
|
|
*/
|
|
|
|
|
public function getProductWarehouses($productId)
|
|
|
|
|
{
|
|
|
|
|
$inventories = \App\Modules\Inventory\Models\Inventory::with(['warehouse', 'product.baseUnit'])
|
|
|
|
|
->where('product_id', $productId)
|
|
|
|
|
->where('quantity', '>', 0)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$data = $inventories->map(function ($inv) {
|
|
|
|
|
return [
|
|
|
|
|
'id' => $inv->id, // Inventory ID
|
|
|
|
|
'warehouse_id' => $inv->warehouse_id,
|
|
|
|
|
'warehouse_name' => $inv->warehouse->name ?? '未知倉庫',
|
|
|
|
|
'batch_number' => $inv->batch_number,
|
|
|
|
|
'quantity' => $inv->quantity,
|
|
|
|
|
'expiry_date' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
|
|
|
|
'unit_name' => $inv->product->baseUnit->name ?? '',
|
|
|
|
|
'base_unit_id' => $inv->product->base_unit_id ?? null,
|
|
|
|
|
'conversion_rate' => $inv->product->conversion_rate ?? 1,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return response()->json($data);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
/**
|
2026-01-26 14:59:24 +08:00
|
|
|
* 編輯生產單
|
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', '只有草稿狀態的生產單可以編輯');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 基本水和
|
|
|
|
|
$productionOrder->product = $this->inventoryService->getProduct($productionOrder->product_id);
|
2026-02-12 16:30:34 +08:00
|
|
|
$productionOrder->warehouse = $productionOrder->warehouse_id
|
|
|
|
|
? $this->inventoryService->getWarehouse($productionOrder->warehouse_id)
|
|
|
|
|
: null;
|
2026-01-26 14:59:24 +08:00
|
|
|
|
|
|
|
|
// 手動水和明細資料
|
|
|
|
|
$items = $productionOrder->items;
|
|
|
|
|
$inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray();
|
|
|
|
|
$inventories = $this->inventoryService->getInventoriesByIds(
|
|
|
|
|
$inventoryIds,
|
|
|
|
|
['product.baseUnit']
|
|
|
|
|
)->keyBy('id');
|
|
|
|
|
|
|
|
|
|
$units = $this->inventoryService->getUnits()->keyBy('id');
|
|
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
$item->inventory = $inventories->get($item->inventory_id);
|
|
|
|
|
$item->unit = $units->get($item->unit_id);
|
|
|
|
|
}
|
2026-01-22 15:39:35 +08:00
|
|
|
|
|
|
|
|
return Inertia::render('Production/Edit', [
|
|
|
|
|
'productionOrder' => $productionOrder,
|
2026-01-26 14:59:24 +08:00
|
|
|
'products' => $this->inventoryService->getAllProducts(),
|
|
|
|
|
'warehouses' => $this->inventoryService->getAllWarehouses(),
|
|
|
|
|
'units' => $this->inventoryService->getUnits(),
|
2026-01-22 15:39:35 +08:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新生產單
|
|
|
|
|
*/
|
|
|
|
|
public function update(Request $request, ProductionOrder $productionOrder)
|
|
|
|
|
{
|
|
|
|
|
if ($productionOrder->status !== 'draft') {
|
2026-01-26 14:59:24 +08:00
|
|
|
return redirect()->route('production-orders.show', $productionOrder->id)
|
|
|
|
|
->with('error', '只有草稿可以修改');
|
2026-01-22 15:39:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$status = $request->input('status', 'draft');
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 基礎驗證規則
|
2026-02-12 16:30:34 +08:00
|
|
|
$rules = [
|
|
|
|
|
'product_id' => 'required',
|
2026-01-26 14:59:24 +08:00
|
|
|
'remark' => 'nullable|string',
|
2026-02-12 16:30:34 +08:00
|
|
|
'warehouse_id' => 'nullable',
|
|
|
|
|
'output_quantity' => 'nullable|numeric',
|
|
|
|
|
'items' => 'nullable|array',
|
|
|
|
|
'items.*.inventory_id' => 'required',
|
|
|
|
|
'items.*.quantity_used' => 'required|numeric',
|
2026-01-22 15:39:35 +08:00
|
|
|
];
|
2026-01-26 14:59:24 +08:00
|
|
|
|
|
|
|
|
$validated = $request->validate($rules);
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-02-12 16:30:34 +08:00
|
|
|
DB::transaction(function () use ($validated, $request, $productionOrder) {
|
2026-01-22 15:39:35 +08:00
|
|
|
$productionOrder->update([
|
|
|
|
|
'product_id' => $validated['product_id'],
|
2026-01-26 14:59:24 +08:00
|
|
|
'warehouse_id' => $validated['warehouse_id'] ?? $productionOrder->warehouse_id,
|
2026-01-22 15:39:35 +08:00
|
|
|
'output_quantity' => $validated['output_quantity'] ?? 0,
|
2026-02-12 16:30:34 +08:00
|
|
|
'output_batch_number' => $request->output_batch_number ?? $productionOrder->output_batch_number,
|
2026-01-26 14:59:24 +08:00
|
|
|
'output_box_count' => $request->output_box_count,
|
2026-02-12 16:30:34 +08:00
|
|
|
'production_date' => $request->production_date ?? $productionOrder->production_date,
|
|
|
|
|
'expiry_date' => $request->expiry_date ?? $productionOrder->expiry_date,
|
2026-01-26 14:59:24 +08:00
|
|
|
'remark' => $request->remark,
|
2026-01-22 15:39:35 +08:00
|
|
|
]);
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
activity()
|
|
|
|
|
->performedOn($productionOrder)
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
->log('updated');
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
// 重新建立明細
|
|
|
|
|
$productionOrder->items()->delete();
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
if (!empty($request->items)) {
|
|
|
|
|
foreach ($request->items as $item) {
|
2026-01-22 15:39:35 +08:00
|
|
|
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-02-12 16:30:34 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return redirect()->route('production-orders.index')
|
|
|
|
|
->with('success', '生產單已更新');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新生產工單狀態
|
|
|
|
|
*/
|
|
|
|
|
public function updateStatus(Request $request, ProductionOrder $productionOrder)
|
|
|
|
|
{
|
|
|
|
|
$newStatus = $request->input('status');
|
|
|
|
|
|
|
|
|
|
if (!$productionOrder->canTransitionTo($newStatus)) {
|
|
|
|
|
return response()->json(['error' => '不合法的狀態轉移或權限不足'], 403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($newStatus, $productionOrder, $request) {
|
|
|
|
|
$oldStatus = $productionOrder->status;
|
|
|
|
|
|
|
|
|
|
// 1. 執行特定狀態的業務邏輯
|
|
|
|
|
if ($oldStatus === ProductionOrder::STATUS_APPROVED && $newStatus === ProductionOrder::STATUS_IN_PROGRESS) {
|
|
|
|
|
// 開始製作 -> 扣除原料庫存
|
|
|
|
|
$items = $productionOrder->items;
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
$this->inventoryService->decreaseInventoryQuantity(
|
|
|
|
|
$item->inventory_id,
|
|
|
|
|
$item->quantity_used,
|
|
|
|
|
"生產單 #{$productionOrder->code} 開始製作 (扣料)",
|
|
|
|
|
ProductionOrder::class,
|
|
|
|
|
$productionOrder->id
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
elseif ($oldStatus === ProductionOrder::STATUS_IN_PROGRESS && $newStatus === ProductionOrder::STATUS_COMPLETED) {
|
|
|
|
|
// 完成製作 -> 成品入庫
|
|
|
|
|
$warehouseId = $request->input('warehouse_id'); // 由前端 Modal 傳來
|
|
|
|
|
$batchNumber = $request->input('output_batch_number'); // 由前端 Modal 傳來
|
|
|
|
|
$expiryDate = $request->input('expiry_date'); // 由前端 Modal 傳來
|
|
|
|
|
|
|
|
|
|
if (!$warehouseId) {
|
|
|
|
|
throw new \Exception('必須選擇入庫倉庫');
|
|
|
|
|
}
|
|
|
|
|
if (!$batchNumber) {
|
|
|
|
|
throw new \Exception('必須提供成品批號');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新單據資訊:批號、效期與自動記錄生產日期
|
|
|
|
|
$productionOrder->output_batch_number = $batchNumber;
|
|
|
|
|
$productionOrder->expiry_date = $expiryDate;
|
|
|
|
|
$productionOrder->production_date = now()->toDateString();
|
|
|
|
|
$productionOrder->warehouse_id = $warehouseId;
|
2026-01-22 15:39:35 +08:00
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
$this->inventoryService->createInventoryRecord([
|
2026-02-12 16:30:34 +08:00
|
|
|
'warehouse_id' => $warehouseId,
|
|
|
|
|
'product_id' => $productionOrder->product_id,
|
|
|
|
|
'quantity' => $productionOrder->output_quantity,
|
|
|
|
|
'batch_number' => $batchNumber,
|
|
|
|
|
'box_number' => $productionOrder->output_box_count,
|
|
|
|
|
'arrival_date' => now()->toDateString(),
|
|
|
|
|
'expiry_date' => $expiryDate,
|
|
|
|
|
'reason' => "生產單 #{$productionOrder->code} 製作完成 (入庫)",
|
2026-01-26 14:59:24 +08:00
|
|
|
'reference_type' => ProductionOrder::class,
|
|
|
|
|
'reference_id' => $productionOrder->id,
|
2026-01-22 15:39:35 +08:00
|
|
|
]);
|
|
|
|
|
}
|
2026-02-12 16:30:34 +08:00
|
|
|
|
|
|
|
|
// 2. 更新狀態
|
|
|
|
|
$productionOrder->status = $newStatus;
|
|
|
|
|
$productionOrder->save();
|
|
|
|
|
|
|
|
|
|
// 3. 紀錄 Activity Log
|
|
|
|
|
activity()
|
|
|
|
|
->performedOn($productionOrder)
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
->withProperties([
|
|
|
|
|
'old_status' => $oldStatus,
|
|
|
|
|
'new_status' => $newStatus
|
|
|
|
|
])
|
|
|
|
|
->log("status_updated_to_{$newStatus}");
|
2026-01-22 15:39:35 +08:00
|
|
|
});
|
|
|
|
|
|
2026-02-12 16:30:34 +08:00
|
|
|
return back()->with('success', '狀態已更新');
|
2026-01-26 14:59:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-12 16:30:34 +08:00
|
|
|
* 從儲存體中移除指定資源。
|
2026-01-26 14:59:24 +08:00
|
|
|
*/
|
|
|
|
|
public function destroy(ProductionOrder $productionOrder)
|
|
|
|
|
{
|
2026-02-12 16:30:34 +08:00
|
|
|
// 僅允許刪除草稿或已作廢的單據
|
|
|
|
|
if (!in_array($productionOrder->status, [ProductionOrder::STATUS_DRAFT, ProductionOrder::STATUS_CANCELLED])) {
|
|
|
|
|
return redirect()->back()->with('error', '僅有草稿或已作廢的生產單可以刪除');
|
2026-01-26 14:59:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::transaction(function () use ($productionOrder) {
|
2026-02-12 16:30:34 +08:00
|
|
|
$productionOrder->items()->delete();
|
|
|
|
|
$productionOrder->delete();
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
activity()
|
|
|
|
|
->performedOn($productionOrder)
|
|
|
|
|
->causedBy(auth()->user())
|
|
|
|
|
->log('deleted');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return redirect()->route('production-orders.index')->with('success', '生產單已刪除');
|
2026-01-22 15:39:35 +08:00
|
|
|
}
|
2026-01-21 17:19:36 +08:00
|
|
|
}
|