Skip to content

揀貨單狀態機說明

本文件說明揀貨單(Picking List)的完整狀態機邏輯。


狀態定義

揀貨單有以下 5 種狀態:

狀態代碼中文名稱說明
ready待處理揀貨單剛建立,等待開始揀貨
processing揀貨中倉庫人員正在進行揀貨作業
done已完成揀貨單所有訂單已出貨完成
canceled已取消揀貨單已取消(可恢復)
terminated已終止揀貨單已終止(部分訂單取消或其他原因)

狀態轉換圖

(狀態轉換圖 picking_list_status.svg 暫缺,請參考下方「狀態轉換事件」表格)

D2 檔案: docs/state_machines/picking_list_status.d2


狀態轉換事件

1. start_picking (開始揀貨)

功能: 開始揀貨作業

狀態轉換:

  • readyprocessing

觸發時機:

  • 倉庫人員點擊「開始揀貨」按鈕
  • 第一個揀貨項目被掃描時

業務邏輯:

  • 記錄開始揀貨時間
  • 記錄揀貨倉庫人員
  • 建立揀貨歷史記錄

Guard 條件:

  • 揀貨單狀態必須為 ready
  • 揀貨單包含至少一個揀貨項目 (has_items?)

2. cancel (取消揀貨單)

功能: 取消揀貨單(可恢復)

狀態轉換:

  • readycanceled
  • processingcanceled

觸發時機:

  • 倉庫人員手動取消揀貨單
  • 需要暫停揀貨作業時

業務邏輯:

  • 記錄取消時間
  • 保留揀貨單資料(可恢復)

Guard 條件:

  • 揀貨單狀態必須為 readyprocessing

3. resume (恢復揀貨單)

功能: 恢復已取消的揀貨單

狀態轉換:

  • canceledprocessing

觸發時機:

  • 倉庫人員手動恢復揀貨單
  • 取消原因已解決時

業務邏輯:

  • 清除取消時間
  • 恢復揀貨作業

Guard 條件:

  • 揀貨單狀態必須為 canceled

4. complete (完成揀貨)

功能: 完成所有訂單的揀貨與出貨

狀態轉換:

  • processingdone

觸發時機:

  • 揀貨單內所有訂單的出貨狀態變更為 done
  • 系統自動觸發(監聽訂單狀態變更)
  • 或倉庫人員手動標記完成

業務邏輯:

  • 記錄完成時間
  • 建立完成歷史記錄
  • 釋放揀貨單資源

Guard 條件:

  • 揀貨單狀態必須為 processing
  • 所有關聯訂單的 status == done (all_orders_shipped?)

5. terminate (終止揀貨單)

功能: 終止揀貨單(部分訂單被取消或其他異常情況)

狀態轉換:

  • processingterminated

觸發時機:

  • 倉庫人員手動終止
  • 部分訂單被取消導致揀貨單無法繼續
  • 系統異常需要終止揀貨流程

業務邏輯:

  • 記錄終止時間
  • 記錄終止原因
  • 將未完成訂單解除綁定
  • 釋放已分配的庫存(如適用)

Guard 條件:

  • 揀貨單狀態必須為 processing
  • 需要管理員權限或特殊確認

權限標誌方法

以下是建議的權限判斷方法,用於控制 UI 按鈕顯示和功能啟用:

editable?

說明: 揀貨單是否可編輯(修改訂單內容)

判斷邏輯:

ruby
def editable?
  ready?
end

返回 true 的狀態:

  • ready - 待處理狀態可以修改訂單

pickable?

說明: 揀貨單是否可開始揀貨

判斷邏輯:

ruby
def pickable?
  ready? && has_items?
end

返回 true 的狀態:

  • ready - 待處理且有揀貨項目時可開始揀貨

completable?

說明: 揀貨單是否可手動完成

判斷邏輯:

ruby
def completable?
  processing? && all_orders_shipped?
end

返回 true 的狀態:

  • processing - 揀貨中且所有訂單已出貨

terminatable?

說明: 揀貨單是否可終止

判斷邏輯:

ruby
def terminatable?
  processing?
end

返回 true 的狀態:

  • processing - 揀貨中可終止

cancelable?

說明: 揀貨單是否可取消

判斷邏輯:

ruby
def cancelable?
  ready? || processing?
end

返回 true 的狀態:

  • ready - 待處理可取消
  • processing - 揀貨中可取消

resumable?

說明: 揀貨單是否可恢復

判斷邏輯:

ruby
def resumable?
  canceled?
end

返回 true 的狀態:

  • canceled - 已取消可恢復

in_progress?

說明: 揀貨單是否正在進行中

判斷邏輯:

ruby
def in_progress?
  processing?
end

返回 true 的狀態:

  • processing - 揀貨中

finished?

說明: 揀貨單是否已結束(不論完成、取消或終止)

判斷邏輯:

ruby
def finished?
  done? || terminated? || canceled?
end

返回 true 的狀態:

  • done - 已完成
  • canceled - 已取消
  • terminated - 已終止

業務場景說明

場景 1: 正常揀貨流程

  1. 建立揀貨單: 系統根據揀貨規則自動建立揀貨單 → ready
  2. 開始揀貨: 倉庫人員掃描第一個商品 → processing
  3. 揀貨與出貨: 倉庫人員完成揀貨、打包、出貨作業
  4. 自動完成: 所有訂單出貨完成 → done

場景 2: 終止揀貨單

情況 A: 部分訂單取消

  1. 揀貨單狀態為 processing
  2. 其中幾個訂單被貨主取消
  3. 倉庫人員評估後決定終止此揀貨單
  4. 點擊「終止」並輸入原因 → terminated
  5. 剩餘有效訂單可重新分配到其他揀貨單

情況 B: 系統異常

  1. 揀貨單狀態為 processing
  2. 發生庫存異常或系統錯誤
  3. 管理員決定終止此揀貨單
  4. 輸入終止原因 → terminated
  5. 後續由管理員處理訂單重新分配

場景 3: 部分訂單取消(不終止)

情況:

  • 揀貨單狀態為 processing
  • 其中一個訂單被貨主取消

處理邏輯:

  1. 從揀貨單移除被取消的訂單
  2. 如果還有其他有效訂單,揀貨單繼續正常流程
  3. 如果所有訂單都被取消,系統自動將揀貨單變更為 terminated

自動觸發檢查

系統應該在以下情況自動檢查並更新揀貨單狀態:

1. 訂單狀態變更後

檢查點: 當關聯訂單的 status 變更後

檢查邏輯:

ruby
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

2. 揀貨項目更新後(可選)

檢查點: 當 picking_list_item 更新 picked_quantity

檢查邏輯:

ruby
# 這個檢查是可選的,視業務需求而定
def check_all_items_scanned
  return unless processing?

  if all_items_scanned?
    # 可以發送通知或更新進度
    notify_all_items_scanned
  end
end

資料庫欄位說明

以下是目前的時間戳記欄位:

欄位名稱類型說明
statusstring狀態(必須)
done_atdatetime完成時間
warehouse_idinteger倉庫 ID
parent_idinteger父揀貨單 ID(如有階層關係)

UI 按鈕顯示規則

以下是各個狀態下應該顯示的操作按鈕:

狀態編輯開始揀貨取消恢復終止完成
ready
processing✅*
done
canceled
terminated

注意:

  • ✅ 表示該按鈕應該顯示且可用
  • ❌ 表示該按鈕應該隱藏或禁用
  • ✅* 表示條件顯示(processing 狀態下,只有所有訂單都已出貨時才顯示「完成」按鈕)

實作建議

使用 AASM Gem

建議使用 AASM gem 實作狀態機,類似 Inbound model:

ruby
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:

ruby
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

相關文件

  • 訂單狀態機 - 訂單的狀態機說明
  • 入倉單狀態機 - 入倉單的狀態機說明
  • 揀貨單操作說明 - 倉庫人員如何使用揀貨單