揀貨單狀態機說明
本文件說明揀貨單(Picking List)的完整狀態機邏輯。
狀態定義
揀貨單有以下 5 種狀態:
| 狀態代碼 | 中文名稱 | 說明 |
|---|---|---|
ready | 待處理 | 揀貨單剛建立,等待開始揀貨 |
processing | 揀貨中 | 倉庫人員正在進行揀貨作業 |
done | 已完成 | 揀貨單所有訂單已出貨完成 |
canceled | 已取消 | 揀貨單已取消(可恢復) |
terminated | 已終止 | 揀貨單已終止(部分訂單取消或其他原因) |
狀態轉換圖
(狀態轉換圖 picking_list_status.svg 暫缺,請參考下方「狀態轉換事件」表格)
D2 檔案: docs/state_machines/picking_list_status.d2
狀態轉換事件
1. start_picking (開始揀貨)
功能: 開始揀貨作業
狀態轉換:
ready→processing
觸發時機:
- 倉庫人員點擊「開始揀貨」按鈕
- 第一個揀貨項目被掃描時
業務邏輯:
- 記錄開始揀貨時間
- 記錄揀貨倉庫人員
- 建立揀貨歷史記錄
Guard 條件:
- 揀貨單狀態必須為
ready - 揀貨單包含至少一個揀貨項目 (
has_items?)
2. cancel (取消揀貨單)
功能: 取消揀貨單(可恢復)
狀態轉換:
ready→canceledprocessing→canceled
觸發時機:
- 倉庫人員手動取消揀貨單
- 需要暫停揀貨作業時
業務邏輯:
- 記錄取消時間
- 保留揀貨單資料(可恢復)
Guard 條件:
- 揀貨單狀態必須為
ready或processing
3. resume (恢復揀貨單)
功能: 恢復已取消的揀貨單
狀態轉換:
canceled→processing
觸發時機:
- 倉庫人員手動恢復揀貨單
- 取消原因已解決時
業務邏輯:
- 清除取消時間
- 恢復揀貨作業
Guard 條件:
- 揀貨單狀態必須為
canceled
4. complete (完成揀貨)
功能: 完成所有訂單的揀貨與出貨
狀態轉換:
processing→done
觸發時機:
- 揀貨單內所有訂單的出貨狀態變更為
done - 系統自動觸發(監聽訂單狀態變更)
- 或倉庫人員手動標記完成
業務邏輯:
- 記錄完成時間
- 建立完成歷史記錄
- 釋放揀貨單資源
Guard 條件:
- 揀貨單狀態必須為
processing - 所有關聯訂單的
status==done(all_orders_shipped?)
5. terminate (終止揀貨單)
功能: 終止揀貨單(部分訂單被取消或其他異常情況)
狀態轉換:
processing→terminated
觸發時機:
- 倉庫人員手動終止
- 部分訂單被取消導致揀貨單無法繼續
- 系統異常需要終止揀貨流程
業務邏輯:
- 記錄終止時間
- 記錄終止原因
- 將未完成訂單解除綁定
- 釋放已分配的庫存(如適用)
Guard 條件:
- 揀貨單狀態必須為
processing - 需要管理員權限或特殊確認
權限標誌方法
以下是建議的權限判斷方法,用於控制 UI 按鈕顯示和功能啟用:
editable?
說明: 揀貨單是否可編輯(修改訂單內容)
判斷邏輯:
def editable?
ready?
end返回 true 的狀態:
ready- 待處理狀態可以修改訂單
pickable?
說明: 揀貨單是否可開始揀貨
判斷邏輯:
def pickable?
ready? && has_items?
end返回 true 的狀態:
ready- 待處理且有揀貨項目時可開始揀貨
completable?
說明: 揀貨單是否可手動完成
判斷邏輯:
def completable?
processing? && all_orders_shipped?
end返回 true 的狀態:
processing- 揀貨中且所有訂單已出貨
terminatable?
說明: 揀貨單是否可終止
判斷邏輯:
def terminatable?
processing?
end返回 true 的狀態:
processing- 揀貨中可終止
cancelable?
說明: 揀貨單是否可取消
判斷邏輯:
def cancelable?
ready? || processing?
end返回 true 的狀態:
ready- 待處理可取消processing- 揀貨中可取消
resumable?
說明: 揀貨單是否可恢復
判斷邏輯:
def resumable?
canceled?
end返回 true 的狀態:
canceled- 已取消可恢復
in_progress?
說明: 揀貨單是否正在進行中
判斷邏輯:
def in_progress?
processing?
end返回 true 的狀態:
processing- 揀貨中
finished?
說明: 揀貨單是否已結束(不論完成、取消或終止)
判斷邏輯:
def finished?
done? || terminated? || canceled?
end返回 true 的狀態:
done- 已完成canceled- 已取消terminated- 已終止
業務場景說明
場景 1: 正常揀貨流程
- 建立揀貨單: 系統根據揀貨規則自動建立揀貨單 →
ready - 開始揀貨: 倉庫人員掃描第一個商品 →
processing - 揀貨與出貨: 倉庫人員完成揀貨、打包、出貨作業
- 自動完成: 所有訂單出貨完成 →
done
場景 2: 終止揀貨單
情況 A: 部分訂單取消
- 揀貨單狀態為
processing - 其中幾個訂單被貨主取消
- 倉庫人員評估後決定終止此揀貨單
- 點擊「終止」並輸入原因 →
terminated - 剩餘有效訂單可重新分配到其他揀貨單
情況 B: 系統異常
- 揀貨單狀態為
processing - 發生庫存異常或系統錯誤
- 管理員決定終止此揀貨單
- 輸入終止原因 →
terminated - 後續由管理員處理訂單重新分配
場景 3: 部分訂單取消(不終止)
情況:
- 揀貨單狀態為
processing - 其中一個訂單被貨主取消
處理邏輯:
- 從揀貨單移除被取消的訂單
- 如果還有其他有效訂單,揀貨單繼續正常流程
- 如果所有訂單都被取消,系統自動將揀貨單變更為
terminated
自動觸發檢查
系統應該在以下情況自動檢查並更新揀貨單狀態:
1. 訂單狀態變更後
檢查點: 當關聯訂單的 status 變更後
檢查邏輯:
def check_completion
return unless processing?
if all_orders_done?
complete!
end
end
def check_termination
return unless processing?
# 如果所有訂單都被取消,自動終止揀貨單
if orders.all?(&:canceled?)
terminate!
end
end2. 揀貨項目更新後(可選)
檢查點: 當 picking_list_item 更新 picked_quantity 後
檢查邏輯:
# 這個檢查是可選的,視業務需求而定
def check_all_items_scanned
return unless processing?
if all_items_scanned?
# 可以發送通知或更新進度
notify_all_items_scanned
end
end資料庫欄位說明
以下是目前的時間戳記欄位:
| 欄位名稱 | 類型 | 說明 |
|---|---|---|
status | string | 狀態(必須) |
done_at | datetime | 完成時間 |
warehouse_id | integer | 倉庫 ID |
parent_id | integer | 父揀貨單 ID(如有階層關係) |
UI 按鈕顯示規則
以下是各個狀態下應該顯示的操作按鈕:
| 狀態 | 編輯 | 開始揀貨 | 取消 | 恢復 | 終止 | 完成 |
|---|---|---|---|---|---|---|
ready | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
processing | ❌ | ❌ | ✅ | ❌ | ✅ | ✅* |
done | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
canceled | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
terminated | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
注意:
- ✅ 表示該按鈕應該顯示且可用
- ❌ 表示該按鈕應該隱藏或禁用
- ✅* 表示條件顯示(
processing狀態下,只有所有訂單都已出貨時才顯示「完成」按鈕)
實作建議
使用 AASM Gem
建議使用 AASM gem 實作狀態機,類似 Inbound model:
class PickingList < ApplicationRecord
include AASM
aasm column: :status do
state :ready, initial: true
state :processing
state :done
state :terminated
# 事件:開始揀貨
event :start_picking do
transitions from: :ready, to: :processing
after do
update_column(:started_at, Time.current) if respond_to?(:started_at)
end
end
# 事件:完成
event :complete do
transitions from: :processing, to: :done, guard: :all_orders_done?
after do
update_column(:done_at, Time.current)
end
end
# 事件:終止
event :terminate do
transitions from: :processing, to: :terminated
after do
update_column(:terminated_at, Time.current) if respond_to?(:terminated_at)
unbind_orders
end
end
end
# 權限判斷方法
def editable?
ready?
end
def pickable?
ready?
end
def completable?
processing? && all_orders_done?
end
def terminatable?
processing?
end
def in_progress?
processing?
end
def finished?
done? || terminated?
end
# Guard 方法
def all_orders_done?
orders.where.not(status: 'done').count.zero?
end
# Helper 方法
def unbind_orders
orders.update_all(picking_list_id: nil)
end
end使用 Enum(簡化版)
如果不需要複雜的事件和回調,也可以使用 Rails 內建的 enum:
class PickingList < ApplicationRecord
belongs_to :merchant
has_many :picking_list_items, dependent: :destroy
has_many :orders
# 狀態定義
enum :status, {
ready: 'ready',
processing: 'processing',
done: 'done',
terminated: 'terminated'
}
validates :status, inclusion: { in: statuses.keys }
# 權限判斷方法
def editable?
ready?
end
def pickable?
ready?
end
def completable?
processing? && all_orders_done?
end
def terminatable?
processing?
end
def in_progress?
processing?
end
def finished?
done? || terminated?
end
# 狀態轉換方法
def start_picking!
raise AppErrors::Error::AppError.new(AppErrors::Error::INVALID_STATUS) unless pickable?
update!(status: :processing, started_at: Time.current)
end
def complete!
raise AppErrors::Error::AppError.new(AppErrors::Error::INVALID_STATUS) unless completable?
update!(status: :done, done_at: Time.current)
end
def terminate!(reason = nil)
raise AppErrors::Error::AppError.new(AppErrors::Error::INVALID_STATUS) unless terminatable?
update!(
status: :terminated,
terminated_at: Time.current,
terminate_reason: reason
)
unbind_orders
end
# Helper 方法
def all_orders_done?
orders.where.not(status: 'done').count.zero?
end
def unbind_orders
orders.update_all(picking_list_id: nil)
end
# 自動檢查方法(在訂單狀態變更時調用)
def check_completion
return unless processing?
if all_orders_done?
complete!
end
end
def check_termination
return unless processing?
if orders.all?(&:canceled?)
terminate!('所有訂單已取消')
end
end
end相關文件
- 訂單狀態機 - 訂單的狀態機說明
- 入倉單狀態機 - 入倉單的狀態機說明
- 揀貨單操作說明 - 倉庫人員如何使用揀貨單