Files
star-erp/app/Modules/Production/Controllers/ProductionOrderController.php
sky121113 106de4e945
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 53s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 修正庫存與撥補單邏輯並整合文件
1. 修復倉庫統計數據加總與樣式。
2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。
3. 撥補單商品列表加入批號與效期顯示。
4. 修正撥補單儲存邏輯以支援精確批號轉移。
5. 整合 FEATURES.md 至 README.md。
2026-01-26 14:59:24 +08:00

411 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\Production\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Production\Models\ProductionOrder;
use App\Modules\Production\Models\ProductionOrderItem;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Core\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class ProductionOrderController extends Controller
{
protected $inventoryService;
public function __construct(InventoryServiceInterface $inventoryService)
{
$this->inventoryService = $inventoryService;
}
/**
* 生產工單列表
*/
public function index(Request $request): Response
{
// 不再使用 with(),避免跨模組 Eager Loading
$query = ProductionOrder::query();
// 搜尋 (此處 orWhereHas 暫時保留,因 Laravel query builder 仍可作用於資料表層級,
// 但實務上若模組完全隔離,應考慮搜尋引擎或 ID 預選)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('code', 'like', "%{$search}%")
->orWhere('output_batch_number', 'like', "%{$search}%");
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
$productIds = \App\Modules\Inventory\Models\Product::where('name', 'like', "%{$search}%")->pluck('id');
$q->orWhereIn('product_id', $productIds);
});
}
// 狀態篩選
if ($request->filled('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
// 排除軟刪除
$query->orderBy($request->input('sort_field', 'created_at'), $request->input('sort_direction', 'desc'));
// 分頁
$perPage = $request->input('per_page', 10);
$productionOrders = $query->paginate($perPage)->withQueryString();
// --- 手動資料水和 (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');
$users = User::whereIn('id', $userIds)->get()->keyBy('id'); // Core 模組暫由 Model 直接獲取
$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;
});
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' => $this->inventoryService->getAllProducts(),
'warehouses' => $this->inventoryService->getAllWarehouses(),
'units' => $this->inventoryService->getUnits(),
]);
}
/**
* 儲存生產單(含自動扣料與成品入庫)
*/
public function store(Request $request)
{
$status = $request->input('status', 'draft');
$baseRules = [
'product_id' => 'required',
'output_batch_number' => 'required|string|max:50',
'status' => 'nullable|in:draft,completed',
];
$completedRules = [
'warehouse_id' => 'required',
'output_quantity' => 'required|numeric|min:0.01',
'production_date' => 'required|date',
'items' => 'required|array|min:1',
'items.*.inventory_id' => 'required',
'items.*.quantity_used' => 'required|numeric|min:0.0001',
];
$rules = $status === 'completed' ? array_merge($baseRules, $completedRules) : $baseRules;
$validated = $request->validate($rules);
DB::transaction(function () use ($validated, $request, $status) {
// 1. 建立生產工單
$productionOrder = ProductionOrder::create([
'code' => ProductionOrder::generateCode(),
'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' => $request->output_box_count,
'production_date' => $validated['production_date'] ?? now()->toDateString(),
'expiry_date' => $request->expiry_date,
'user_id' => auth()->id(),
'status' => $status,
'remark' => $request->remark,
]);
activity()
->performedOn($productionOrder)
->causedBy(auth()->user())
->log('created');
// 2. 處理明細
if (!empty($request->items)) {
foreach ($request->items as $item) {
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') {
$this->inventoryService->decreaseInventoryQuantity(
$item['inventory_id'],
$item['quantity_used'],
"生產單 #{$productionOrder->code} 耗料",
ProductionOrder::class,
$productionOrder->id
);
}
}
}
// 3. 成品入庫
if ($status === 'completed') {
$this->inventoryService->createInventoryRecord([
'warehouse_id' => $validated['warehouse_id'],
'product_id' => $validated['product_id'],
'quantity' => $validated['output_quantity'],
'batch_number' => $validated['output_batch_number'],
'box_number' => $request->output_box_count,
'arrival_date' => $validated['production_date'],
'expiry_date' => $request->expiry_date,
'reason' => "生產單 #{$productionOrder->code} 成品入庫",
'reference_type' => ProductionOrder::class,
'reference_id' => $productionOrder->id,
]);
activity()
->performedOn($productionOrder)
->causedBy(auth()->user())
->log('completed');
}
});
return redirect()->route('production-orders.index')
->with('success', $status === 'completed' ? '生產單已完成並入庫' : '草稿已儲存');
}
/**
* 檢視生產單詳情
*/
public function show(ProductionOrder $productionOrder): Response
{
// 手動水和主表資料
$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();
}
$productionOrder->warehouse = $this->inventoryService->getWarehouse($productionOrder->warehouse_id);
$productionOrder->user = User::find($productionOrder->user_id);
// 手動水和明細資料
$items = $productionOrder->items;
$inventoryIds = $items->pluck('inventory_id')->unique()->filter()->toArray();
$inventories = $this->inventoryService->getInventoriesByIds(
$inventoryIds,
['product.baseUnit', 'sourcePurchaseOrder.vendor']
)->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);
}
return Inertia::render('Production/Show', [
'productionOrder' => $productionOrder,
]);
}
/**
* 取得倉庫內可用庫存
*/
public function getWarehouseInventories($warehouseId)
{
$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,
];
});
return response()->json($data);
}
/**
* 編輯生產單
*/
public function edit(ProductionOrder $productionOrder): Response
{
if ($productionOrder->status !== 'draft') {
return redirect()->route('production-orders.show', $productionOrder->id)
->with('error', '只有草稿狀態的生產單可以編輯');
}
// 基本水和
$productionOrder->product = $this->inventoryService->getProduct($productionOrder->product_id);
$productionOrder->warehouse = $this->inventoryService->getWarehouse($productionOrder->warehouse_id);
// 手動水和明細資料
$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);
}
return Inertia::render('Production/Edit', [
'productionOrder' => $productionOrder,
'products' => $this->inventoryService->getAllProducts(),
'warehouses' => $this->inventoryService->getAllWarehouses(),
'units' => $this->inventoryService->getUnits(),
]);
}
/**
* 更新生產單
*/
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' => 'required|in:draft,completed',
'remark' => 'nullable|string',
];
// 完工時的嚴格驗證規則
$completedRules = [
'warehouse_id' => 'required|exists:warehouses,id',
'output_quantity' => 'required|numeric|min:0.01',
'production_date' => 'required|date',
'expiry_date' => 'nullable|date',
'items' => 'required|array|min:1',
'items.*.inventory_id' => 'required|exists:inventories,id',
'items.*.quantity_used' => 'required|numeric|min:0.0001',
];
// 若狀態切換為 completed需合併驗證規則
$rules = $status === 'completed' ? array_merge($baseRules, $completedRules) : $baseRules;
$validated = $request->validate($rules);
DB::transaction(function () use ($validated, $request, $status, $productionOrder) {
$productionOrder->update([
'product_id' => $validated['product_id'],
'warehouse_id' => $validated['warehouse_id'] ?? $productionOrder->warehouse_id,
'output_quantity' => $validated['output_quantity'] ?? 0,
'output_batch_number' => $validated['output_batch_number'],
'output_box_count' => $request->output_box_count,
'production_date' => $validated['production_date'] ?? now()->toDateString(),
'expiry_date' => $request->expiry_date,
'status' => $status,
'remark' => $request->remark,
]);
activity()
->performedOn($productionOrder)
->causedBy(auth()->user())
->log('updated');
// 重新建立明細
$productionOrder->items()->delete();
if (!empty($request->items)) {
foreach ($request->items as $item) {
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') {
$this->inventoryService->decreaseInventoryQuantity(
$item['inventory_id'],
$item['quantity_used'],
"生產單 #{$productionOrder->code} 耗料",
ProductionOrder::class,
$productionOrder->id
);
}
}
}
if ($status === 'completed') {
$this->inventoryService->createInventoryRecord([
'warehouse_id' => $validated['warehouse_id'],
'product_id' => $validated['product_id'],
'quantity' => $validated['output_quantity'],
'batch_number' => $validated['output_batch_number'],
'box_number' => $request->output_box_count,
'arrival_date' => $validated['production_date'],
'expiry_date' => $request->expiry_date,
'reason' => "生產單 #{$productionOrder->code} 成品入庫",
'reference_type' => ProductionOrder::class,
'reference_id' => $productionOrder->id,
]);
activity()
->performedOn($productionOrder)
->causedBy(auth()->user())
->log('completed');
}
});
return redirect()->route('production-orders.index')
->with('success', '生產單已更新');
}
/**
* 刪除生產單
*/
public function destroy(ProductionOrder $productionOrder)
{
if ($productionOrder->status === 'completed') {
return redirect()->back()->with('error', '已完工的生產單無法刪除');
}
DB::transaction(function () use ($productionOrder) {
// 紀錄刪除動作 (需在刪除前或使用軟刪除)
activity()
->performedOn($productionOrder)
->causedBy(auth()->user())
->log('deleted');
$productionOrder->items()->delete();
$productionOrder->delete();
});
return redirect()->route('production-orders.index')->with('success', '生產單已刪除');
}
}