Issue #5 は、顧客側の注文詳細画面で、注文ステータスが NEW(新規注文)の間だけお届け先変更を可能にする機能拡張である。
関連 Issue の現在地:
#5 は顧客側の orders アプリに閉じ、管理者側の後工程制御とは混ぜない。顧客側では既に「注文変更・キャンセルは NEW のみ」という制限があるため、お届け先変更もこの制限へそろえる。
既存の顧客側「注文内容変更」導線に、お届け先変更を同居させる。
templates/orders/detail.html の既存編集モードに、お届け先選択 UI を追加する。orders:order_update に destination POST 値の検証・保存を追加する。Order.is_cancelable == True、つまり NEW のみとする。is_deleted=False)だけに限定する。final_shipping_fee = None に戻す。以下は #5 初期実装では扱わない。
ACCEPTED 以降の顧客側お届け先変更。対象は主に以下。
orders/views.py
order_detail: 現在は order だけをテンプレートへ渡している。order_update: 既存明細の数量変更、在庫チェック、参考送料再計算を行う。入口で order.is_cancelable を確認している。_calculate_provisional_shipping_fee: cart_group['destination'] と cart_group['items'] から参考送料を計算する。templates/orders/detail.html
orders/models.py
Order.is_cancelable は status == NEW。Order.is_editable はより広い条件だが、顧客側変更では使わない。実装は次の 2 コミット構成を基本にする。
| 実行単位 | 内容 |
|---|---|
| コミットA: サーバ側 | order_detail の候補渡し、order_update の宛先 POST 検証・保存・送料再計算、サーバ側テスト |
| コミットB: UI | 詳細画面の編集モードにお届け先 select を追加、表示切替 JS 調整、GET/HTML 表示テスト |
各コミット境界で orders のテストが通る状態を目指す。既存の mg_orders との境界確認も含めるなら、最終確認では orders + mg_orders を実行する。
order_detail で有効なお届け先候補を渡すorders/views.py の order_detail に、本人の有効なお届け先一覧を追加する。
customer_destinations = Destination.objects.filter(user=request.user, is_deleted=False)
コンテキスト:
{
'order': order,
'customer_destinations': customer_destinations,
}
注意:
order.is_cancelable のときだけ編集ボタンを出すため、候補取得自体は常に行ってもよい。order_update で destination POST 値を検証するorder_update の order.is_cancelable ガード通過後、数量更新・在庫チェックより前に destination を検証する。
ただし、destination 未送信は「お届け先変更なし」として扱う。現行の注文詳細画面と既存テストは quantity_* だけを POST しているため、未送信をエラーにするとコミットA単体で既存の数量変更導線が壊れる。後方互換を保つため、destination が POST された場合だけ宛先変更として検証する。
推奨順序:
order = get_object_or_404(..., user=request.user) で本人の注文に限定する。if not order.is_cancelable: で NEW 以外を拒否する。destination POST 値を取得する。destination が未送信または空文字なら、new_destination = order.destination として現状維持にする。destination が送信されている場合だけ、非数値・他人の宛先・削除済み宛先を JSON エラーで拒否する。new_destination を確定する。エラー方針:
{'success': False, 'message': 'お届け先が無効です。'}
検証条件:
destination_str = request.POST.get('destination')
if destination_str in (None, ''):
new_destination = order.destination
else:
posted_destination_id = int(destination_str) # ValueError は JSON エラーにする
new_destination = Destination.objects.filter(
id=posted_destination_id,
user=request.user,
is_deleted=False,
).first()
if new_destination is None:
JSON エラー
既存の order_update は、明細保存後に次のような temp_cart_group を作っている。
temp_cart_group = {
'destination': order.destination,
'items': [{'product': item.product, 'quantity': item.quantity, 'price': item.price_at_order} for item in order.items.all()]
}
#5 では destination を変更後宛先にする。
order.destination = new_destination
temp_cart_group = {
'destination': order.destination,
'items': ...
}
保存時は、送料と宛先をまとめて保存する。
order.provisional_shipping_fee = _calculate_provisional_shipping_fee(temp_cart_group)
order.final_shipping_fee = None
order.save(update_fields=['destination', 'provisional_shipping_fee', 'final_shipping_fee'])
注意:
orders/tests.py に order_update のお届け先変更テストを追加する。
追加したいテスト:
NEW 注文で、自分の有効なお届け先へ変更できる。
order.destination が変わる。response.json()['success'] == True。お届け先変更時に参考送料が変更後宛先ベースで再計算される。
order.provisional_shipping_fee が期待値になる。order.final_shipping_fee is None になる。数量変更とお届け先変更を同時に行った場合、変更後数量 + 変更後宛先で送料再計算される。
ACCEPTED 以降の注文へ直接 POST しても拒否される。
order.destination、数量、在庫予約、送料が変わらない。他人のお届け先は拒否される。
order.destination が変わらない。削除済みのお届け先は拒否される。
is_deleted=True の宛先 ID を POST する。非数値の destination は拒否される。
destination='abc'destination 未送信は、既存宛先のまま数量変更できる。
quantity_* のみ POST するテストを維持する。order.destination は変わらない。コミットAの確認コマンド:
docker compose -f docker-compose.dev.yml exec app python manage.py test orders
docker compose -f docker-compose.dev.yml exec app python manage.py makemigrations --check --dry-run
templates/orders/detail.html の「お届け先情報」カードを、既存の数量欄と同じ view-mode / edit-mode d-none 切替に合わせる。
現行テンプレートでは、お届け先情報カードは <form id="order-update-form"> の外側にある。JS は new FormData(form) で送信するため、カード内に select name="destination" を単純追加しても POST に含まれない。以下のどちらかで、必ず FormData(form) に含まれる構造にする。
select に form="order-update-form" 属性を付ける。order-update-form の開始位置をお届け先カードより前へ移動し、宛先 select と数量 input を同じ form 配下に入れる。既存構造を崩しにくいので、初期実装では form="order-update-form" を推奨する。
表示モード:
編集モード:
customer_destinations から select name="destination" を表示する。order.destination.id を selected にする。例:
<select name="destination" class="form-select edit-mode d-none" form="order-update-form">
...
</select>
注意:
order.is_cancelable でない注文では編集ボタンが出ないため、通常操作では select も表示されない。既存 JS は以下を切り替えている。
const viewModeItems = document.querySelectorAll('.view-mode');
const editModeItems = document.querySelectorAll('.edit-mode');
お届け先の表示要素にも同じ class を付ければ、JS の大きな変更は不要。
必要なら、編集開始時にお届け先カードの補足文を表示する程度に留める。
GET 表示で確認したいこと:
NEW 注文の詳細画面に、編集モード用のお届け先 select が含まれる。ACCEPTED 以降では編集ボタンが表示されない。HTML テストは壊れやすくしすぎない。重要なのは「候補の絞り込み」と「NEW のみ編集導線があること」。
コミットBの確認コマンド:
docker compose -f docker-compose.dev.yml exec app python manage.py test orders
docker compose -f docker-compose.dev.yml exec app python manage.py makemigrations --check --dry-run
最終的には、関連する管理者側制御との衝突がないことも含めて以下を実行する。
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
NEW 注文だけお届け先変更 UI が使える。ACCEPTED 以降は UI 上も直接 POST でもお届け先変更できない。final_shipping_fee は None に戻る。