feat(procurement): 統一採購單按鈕樣式與術語更名為「作廢」,並加強權限控管
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 1m28s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-02-06 15:32:12 +08:00
parent 70f1709bd0
commit 6bfdd92347
11 changed files with 318 additions and 73 deletions

View File

@@ -447,11 +447,17 @@ class PurchaseOrderController extends Controller
$taxAmount = !is_null($inputTax) ? $inputTax : round($totalAmount * 0.05, 2);
$grandTotal = $totalAmount + $taxAmount;
// 狀態轉移權限檢查
if (isset($validated['status']) && $order->status !== $validated['status']) {
if (!$order->canTransitionTo($validated['status'])) {
return back()->withErrors(['error' => '您沒有權限將狀態從 ' . $order->status . ' 變更為 ' . $validated['status']]);
}
}
// 1. 填充屬性但暫不儲存以捕捉變更
$order->fill([
'vendor_id' => $validated['vendor_id'],
'warehouse_id' => $validated['warehouse_id'],
'order_date' => $validated['order_date'], // 新增
'order_date' => $validated['order_date'],
'expected_delivery_date' => $validated['expected_delivery_date'],
'total_amount' => $totalAmount,
'tax_amount' => $taxAmount,
@@ -460,11 +466,22 @@ class PurchaseOrderController extends Controller
'status' => $validated['status'],
'invoice_number' => $validated['invoice_number'] ?? null,
'invoice_date' => $validated['invoice_date'] ?? null,
'invoice_amount' => $validated['invoice_amount'] ?? null,
'invoice_amount' => (float) ($validated['invoice_amount'] ?? 0),
]);
// 捕捉變更屬性以進行手動記錄
// 捕捉變更屬性
$dirty = $order->getDirty();
// 嚴格權限檢查:如果修改了 status 以外的任何欄位,必須具備編輯權限
$otherChanges = array_diff(array_keys($dirty), ['status']);
if (!empty($otherChanges)) {
$canEdit = auth()->user()->hasRole('super-admin') || auth()->user()->can('purchase_orders.edit');
if (!$canEdit) {
throw new \Exception('您沒有權限修改採購單的基本內容,僅能執行流程異動(如:送審)。');
}
}
// 捕捉舊屬性以進行記錄
$oldAttributes = [];
$newAttributes = [];
@@ -657,7 +674,7 @@ class PurchaseOrderController extends Controller
DB::commit();
return redirect()->route('purchase-orders.index')->with('success', '採購單已刪除');
return redirect()->route('purchase-orders.index')->with('success', '採購單已作廢');
} catch (\Exception $e) {
DB::rollBack();
return back()->withErrors(['error' => '刪除失敗:' . $e->getMessage()]);

View File

@@ -70,4 +70,50 @@ class PurchaseOrder extends Model
{
return $this->hasMany(PurchaseOrderItem::class);
}
/**
* 檢查是否可以轉移至新狀態,並驗證權限。
*/
public function canTransitionTo(string $newStatus, $user = null): bool
{
$user = $user ?? auth()->user();
if (!$user) return false;
if ($user->hasRole('super-admin')) return true;
$currentStatus = $this->status;
// 定義合法的狀態轉移路徑與所需權限
$transitions = [
'draft' => [
'pending' => 'purchase_orders.view', // 基本檢視者即可送審
'cancelled' => 'purchase_orders.cancel',
],
'pending' => [
'approved' => 'purchase_orders.approve',
'draft' => 'purchase_orders.approve', // 退回草稿
'cancelled' => 'purchase_orders.cancel',
],
'approved' => [
'cancelled' => 'purchase_orders.cancel',
'partial' => null, // 系統自動轉移,不需手動權限點
],
'partial' => [
'completed' => null, // 系統自動轉移
'closed' => 'purchase_orders.approve', // 手動結案通常需要核准權限
'cancelled' => 'purchase_orders.cancel',
],
];
if (!isset($transitions[$currentStatus])) {
return false;
}
if (!array_key_exists($newStatus, $transitions[$currentStatus])) {
return false;
}
$requiredPermission = $transitions[$currentStatus][$newStatus];
return $requiredPermission ? $user->can($requiredPermission) : true;
}
}

View File

@@ -32,7 +32,7 @@ Route::middleware('auth')->group(function () {
Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show');
Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.edit');
Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->middleware('permission:purchase_orders.edit')->name('purchase-orders.update');
Route::match(['PUT', 'PATCH'], '/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->name('purchase-orders.update');
Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->middleware('permission:purchase_orders.delete')->name('purchase-orders.destroy');
});