Issue #12 は、管理者向け注文編集画面で後工程に進んだ注文の内容変更を制御する。
関連 Issue の現在地:
これにより、#12 では「後工程注文を編集画面で無理に差し戻す」のではなく、直接編集を制限し、必要なら #11/#14 のキャンセル・作り直し導線へ寄せるのが自然。
まずは安全側として、管理者編集画面での内容変更を次のように制御する。
| 注文状態 | お届け先変更 | 数量変更 | 商品追加/削除 | 単価変更 | 備考 |
|---|---|---|---|---|---|
| 未引当 | 許可 | 許可 | 許可 | 許可 | 後工程データと接続していないため従来どおり編集可能。 |
| 引当済み・未梱包 | 不可 | 不可 | 不可 | 原則不可 | 引当済み袋と注文明細が食い違うため、キャンセルして作り直す。 |
| 箱詰め済み | 不可 | 不可 | 不可 | 不可 | 箱・送料・発送準備へ影響するため、キャンセルして作り直す。 |
| 発送準備完了 | 不可 | 不可 | 不可 | 不可 | キャンセル時も梱包ボードで再確認する状態。直接編集しない。 |
| 伝票印刷済み / ロック箱 / 発送済み / 手渡し済み / キャンセル済み | 不可 | 不可 | 不可 | 不可 | 既存の BLOCKED / Order.is_editable 方針に従う。 |
ここでいう「内容変更」は、以下を含む。
単価変更は引当・箱詰めの物理作業には直接影響しないが、請求額・入金状態・請求書・繰越充当と強く結びつく。#13 では繰越充当済み注文の単価変更を既に禁止しているため、#12 初期実装では単価変更も「金額が変わる編集」として後工程済み注文では禁止する。
以下は #12 初期実装では扱わない。
これらは運用上必要になった場合、別 Issue で扱う。
対象は主に mg_orders/views.py の order_edit。
現在の order_edit は以下の状態。
has_allocations(order) を使い、お届け先変更だけは引当済みで拒否している。OrderEditDestinationTests.test_allocated_order_allows_quantity_edit_when_destination_unchanged は、引当済み注文でも数量変更できることを期待している。amount_changing or destination_changed を拒否している。can_recreate が true の場合は「キャンセルして作り直す」導線が表示される。#12 では、#13 の特殊ガードを一般化し、「繰越充当の有無に関係なく、後工程に入った注文では内容変更を拒否する」形にする。
当初の Phase 1〜5 は設計上の観点分けとしては有効だが、実作業では独立した検証単位にならない。
allocation_exists = has_allocations(order) を使う判断であり、単独では動作が変わらない。そのため、実装は次の 2 コミット構成を基本にする。
| 実行単位 | 内容 | 旧 Phase 対応 |
|---|---|---|
| コミットA: サーバ側ガード + テスト反転 | #12 POST ガード追加、#13 との順序調整、既存テスト反転、拒否系テスト追加、未引当従来挙動と #13 維持の確認 | Phase 1+2+3 と Phase 5 の大半 |
| コミットB: UI 表示 | 後工程済み注文の数量・単価・商品追加フォーム disabled、理由文言、GET 表示テスト | Phase 4 と Phase 5 の GET 表示テスト |
各コミット境界で orders + mg_orders のテストが通る状態を目指す。
order_edit 内で、既存の allocation_exists = has_allocations(order) を利用する。
まずは最小実装として、後工程判定は allocation_exists でよい。実装上は content_edit_locked = allocation_exists のような変数を導入して一貫して使うか、allocation_exists を直接使う。どちらかに統一し、中途半端な別名を残さない。
content_edit_locked = allocation_exists
理由:
Allocation を経由しているため、allocation_exists=True になる。has_allocations(order) を共有している。将来、箱詰め済みだが Allocation が欠落した壊れたデータを検出したい場合は、get_cancellation_state(order).stage != UNALLOCATED のような判定へ強化する。ただし初期実装では既存方針との整合を優先する。
order_edit の POST では既に以下を判定している。
product_content_changed: 数量変更または商品追加amount_changing: 数量変更、商品追加、単価変更destination_changed: お届け先変更これらの算出後、在庫ロックや保存処理へ進む前に次のガードを追加する。
挿入位置は、明細ループと新規商品追加の解析が終わり、現 #13 ガードの直前がよい。#4 のお届け先変更ガードは明細ループ前に early return する構造なので、#12 ガードは必然的にその後段に入る。
if allocation_exists and (product_content_changed or amount_changing):
エラーメッセージを出して編集画面へ戻す
実装上は product_content_changed は amount_changing に含まれるため、条件は allocation_exists and amount_changing で足りる。ただし意図が読みやすいように、変数名を整理してもよい。
推奨:
content_changing = amount_changing
if content_changing and allocation_exists:
...
メッセージ案:
この注文はすでに出荷準備作業が始まっているため、数量・商品・単価はこの画面では変更できません。内容を変更する場合は、注文をキャンセルして必要な内容で作り直してください。
注意:
order.delete() も、後工程済み注文では不可にする。実コード上は、数量を 0 にすると new_quantity != item.quantity により amount_changing=True になるため、上記ガードに包含される。delete 専用の二重ガードは不要。現在は繰越充当済み注文に対して、金額が変わる編集を拒否している。
#12 実装後も、このガードは必要。
理由:
順序は以下が分かりやすい。
この順にすると、引当済み注文では #12 のメッセージが出る。未引当だが繰越充当済み注文では #13 のメッセージが出る。
コード上の具体位置:
destination POST 値を見た時点で不正・変更不可なら early return する。amount_changing が確定した直後に置く。(amount_changing or destination_changed) and CustomerCreditTransaction.has_credit_applied(order) ガードとして残す。POST ガードを追加すると、既存の OrderEditDestinationTests.test_allocated_order_allows_quantity_edit_when_destination_unchanged は必ず落ちる。したがって、サーバ側ガードと同じコミットでテストを反転・追加する。
既存テストの変更:
OrderEditDestinationTests.test_allocated_order_allows_quantity_edit_when_destination_unchanged
destination_id が変わらないことも確認する。追加したいテスト:
引当済み注文の数量変更を拒否する。
OrderItem.quantity が変わらない。Stock.total_ordered_kg が変わらない。引当済み注文の単価変更を拒否する。
OrderItem.price_at_order が変わらない。items_subtotal 相当が変わらない。引当済み注文の商品追加を拒否する。
OrderItem 件数が増えない。Stock.total_ordered_kg が変わらない。引当済み注文で既存明細数量を 0 にしても削除されない。
Order が削除されない。OrderItem が残る。未引当注文では数量・商品追加・単価変更が従来どおり可能。
繰越充当済みだが未引当の注文では、#13 ガードにより金額変更が拒否される。
CreditApplicationTests.test_credit_applied_order_blocks_amount_changing_edit を維持する。コミットAの確認コマンド:
docker compose -f docker-compose.dev.yml exec app python manage.py test orders mg_orders
docker compose -f docker-compose.dev.yml exec app python manage.py makemigrations --check --dry-run
templates/mg_orders/order_edit.html で、後工程済み注文では内容変更できないことを画面上でも示す。
推奨:
disabled にする。disabled にする。request.POST.get(f'quantity_{item.id}') / price_... をデフォルトなしで int() しているため、disabled にした input が送信されないまま保存ループへ到達すると TypeError になり得る。後工程済み注文は必ず #12 ガードで保存前に return し、未引当注文では disabled にしないこと。文言案:
この注文はすでに出荷準備作業が始まっているため、数量・商品・単価はこの画面では変更できません。内容を変更する場合は、注文をキャンセルして作り直してください。
can_recreate が true の場合は、「キャンセルして作り直す」ボタンが下部に出るため、その導線へつなぐ。
GET 表示で、引当済み注文は数量・単価・商品追加フォームが disabled になることを確認する。
実行コマンド:
docker compose -f docker-compose.dev.yml exec app python manage.py test orders mg_orders
docker compose -f docker-compose.dev.yml exec app python manage.py makemigrations --check --dry-run
order_edit は現在 1 関数に多くの責務が集まっている。#12 で大きくリファクタリングするとリスクが増えるため、初期実装ではガード追加と UI 表示変更に絞る。mg_orders/services/edit_policy.py のようなサービスに切り出す候補はある。can_recreate 導線を活かし、拒否メッセージは「キャンセルして作り直す」運用へ誘導する。orders + mg_orders のテストが通る。makemigrations --check --dry-run でモデル変更なしを確認する。