管理者が注文を一覧・受付・編集し、入金ステータスを管理する機能の仕様を、開発者向けにまとめます。
本書は
specs/33_管理者向け注文管理機能 実装手順書.md(Ver.3.2) を土台に、現行コード(mg_orders)と照合して再構成したものです。挙動は実装を正とし、差分は本文中で明示します。
関連: 購入者向け機能(注文の生成側) / 送料計算 / design/データベース設計
| 機能 | ファイル |
|---|---|
| 注文一覧・受付・編集・キャンセル・繰越充当 | mg_orders/views.py(order_list / order_accept / order_edit / order_cancel / order_apply_credit) |
| 入金ステータス更新 API | mg_orders/api.py(PaymentStatusUpdateView) |
全ビューは /management/orders/ 配下(dashboard:mg_orders:*)。@login_required + @user_passes_test(is_staff_user) でスタッフ限定。トランザクションの掛け方はビューごとに異なる: 注文編集(order_edit)はデコレータ @transaction.atomic、入金 API(PaymentStatusUpdateView)は更新部分を with transaction.atomic(): で保護、注文受付(order_accept)は明示的な atomic なし(単一フィールド更新のみ)。
order_list)一覧は管理画面の共通販売年度(Issue #23)に属する注文だけを表示する。受付・編集・入金・キャンセルも、対象注文が選択年度に属することをサーバー側で検証する。CLOSED年度でも受付・入金・数量減少・キャンセルは可能だが、数量増加と商品追加はできない。
URL: dashboard:mg_orders:order_list(画面 SCR-AD-020)。
GET パラメータによる複合フィルタ。
filter=active(既定): 発送済み(SHIPPED)・手渡し済み(DELIVERED)・キャンセル済み(CANCELED)を除外した「対応中」の注文。status=<値>: 指定の注文ステータスで絞り込み(filter_type='status')。payment_status=<値>: 入金ステータス(UNPAID/PAID)で絞り込み。variety(品種)・weight(容量)・product_type(玄米/精米)を AND 条件で組み合わせ、該当商品を含む注文を distinct() で抽出。GET パラメータの優先順位:
statusとpayment_statusを同時指定した場合、実装はelif分岐のためstatusが優先されpayment_statusは無視される。商品属性フィルタは、これらステータス系フィルタの結果にさらに AND で重ねて適用される。
select_related('user', 'destination') + prefetch_related('items__product__variety') で N+1 を抑制。作成日時の降順。
order_accept)URL: dashboard:mg_orders:order_accept(POST)。
NEW(新規注文)の注文を ACCEPTED(注文受付)に変更する。NEW 以外)の場合は警告を出して何もしない。order_edit)URL: dashboard:mg_orders:order_edit(画面 SCR-AD-030、GET 表示/POST 更新、@transaction.atomic)。
Phase 4-D/Issue #23実装済み:
Order.season(FK、作成後不変)により注文は必ず1つの販売年度に属する。注文明細の商品年度は、サービス層(products.services.validate_items_single_season)とOrderItem.save()の二重ガードで注文年度との一致を保証する。管理者向け新規作成画面固有の年度ドロップダウンは廃止し、新規注文は常にアクティブ年度で作る。編集時の追加候補はorder.seasonで絞り込む。
is_editable が False)の注文は編集不可。is_deleted=False)と、注文と同じ販売年度の販売中商品を渡す。注文年度そのものは編集できない。
is_deleted=True)でも選択肢に必ず含める。含めないと先頭 option が誤選択され、宛先を変えない通常編集まで「お届け先変更」と誤判定されるため(Issue #4)。削除済みの現お届け先は「(削除済み・現在のお届け先)」と表示して区別する。Allocation)が存在する場合は、お届け先選択を disabled にし、変更不可の旨を表示する。各明細について、数量(quantity_<id>)・単価(price_<id>)を受け取り、さらに新規商品(new_product + new_product_quantity)の追加に対応する。
Stock を select_for_update() でロックして available_kg と比較。不足する品種があればエラーにして編集画面へ戻す。destination を読み取り、現お届け先(order.destination_id)と異なる場合のみ変更要求として扱う(現値一致は no-op。現お届け先が削除済みでも通常編集を壊さない)。変更先はその注文の購入者に紐づく有効なお届け先(user=order.user, is_deleted=False)に限定して検証し、不正値は拒否して編集画面へ戻す。
Allocation 無し)の場合のみ order.destination を更新する。お届け先変更時は配送方法・送料区分が変わり得るため参考送料を再計算し、final_shipping_fee を None にリセットする。final_shipping_fee を None に戻して請求額を変えるため禁止対象。変更が必要な場合は、注文をキャンセルして作り直す(キャンセル時に充当額は繰越残高へ戻る)。product_content_changed)またはお届け先が変更された場合は、_calculate_provisional_shipping_fee() で参考送料を変更後のお届け先で再計算し、final_shipping_fee を None にリセットする(要件 4.2.3)。単価のみの変更ではリセットしない。Stock.total_ordered_kg に反映し、明細を更新(数量 0 は削除)、新規商品があれば OrderItem を作成(単価は product.price)。補足: 後工程に入った注文の変更は、直接編集ではなく「キャンセルして作り直す」運用へ寄せる。繰越充当済み注文は請求額が変わる編集を拒否し、引当済み注文のお届け先変更も拒否する。箱詰め済み・発送準備済み注文のキャンセル時は、下記のキャンセル機能が箱から対象袋を外し、残った箱の送料・発送準備を再確認できる状態にする。
なお、顧客側の注文詳細画面では、Issue #5 対応により、ステータスが「新規注文」の間だけ数量変更・お届け先変更・キャンセルが可能。お届け先変更先は顧客本人に紐づく有効なお届け先に限られ、変更後数量と変更後お届け先で参考送料を再計算し、
final_shipping_feeをNoneに戻す。「注文受付」以降は従来どおり顧客側から変更・キャンセルできない。
order_cancel)URL: dashboard:mg_orders:order_cancel_confirm(確認画面) / dashboard:mg_orders:order_cancel(POST 実行)。
管理者は注文の状態を確認してからキャンセルを実行する。状態判定は mg_orders.services.cancellation に集約され、注文・袋・箱・伝票の状況に応じて、実行可否と管理者が確認すべき内容を表示する。
CANCELED にする。PackageItem を箱から外し、対応する Allocation を解除してから注文をキャンセルする。残った箱は送料・発送準備の再確認対象になる。入金済み(PAID)注文をキャンセルする場合は、原則としてお金の扱いを必ず選択する。
| 選択肢 | 記録 |
|---|---|
| 返金 | PaymentAdjustment(action=REFUND) |
| 次回支払いへ繰越 | PaymentAdjustment(action=CARRY_OVER) + CustomerCreditTransaction(CARRY_OVER_FROM_CANCELED_ORDER)。source_season=order.season・recorded_in_season=記録時点のactive seasonを設定する(Issue #17 Phase 4-G) |
| 寄付扱い | PaymentAdjustment(action=DONATION) |
繰越充当で PAID になった注文は、今回新たに現金を受け取っていないため、返金/繰越/寄付の3択は出さない。キャンセル時は、その注文へのまだ取消(reversal)されていないAPPLIED_TO_ORDER行を全件・行ごとに1件ずつRETURNED_FROM_CANCELED_APPLIED_ORDERとして顧客残高へ戻す(1回の部分充当ごとに戻し行が1つ対応する。Issue #17 Phase 4-G)。詳細は §5 を参照。
キャンセル確認画面から「キャンセルして作り直す」を選ぶと、キャンセル成功後に管理者注文作成画面へ遷移し、元注文の顧客・お届け先・明細を引き継いだ新規注文を作成できる。新注文は通常の新規注文と同じく status=NEW / payment_status=UNPAID で作成される。元注文と作り直し先注文は recreated_from / recreations で相互に参照でき、注文一覧・注文編集画面にリンクを表示する。
order_apply_credit)URL: dashboard:mg_orders:order_apply_credit(POST)。
入金済みキャンセルで「次回支払いへ繰越」を選んだ金額は、顧客ごとの繰越残高として台帳管理される。管理者は注文編集画面から、顧客の繰越残高を送料確定済み注文へ充当できる。
final_shipping_fee が確定済みで、かつ payment_status=UNPAID の注文のみ。min(顧客の繰越残高, 請求残額)。部分充当を許可する。注文総額 - 有効な繰越充当額。payment_status=PAID にする。残額がある場合は UNPAID のまま「繰越充当済み・残額あり」と表示する。PaymentStatusUpdateView からの手動 payment_status 変更も拒否する。これは、繰越と現金/振込の支払い原資が混ざる状態を初期実装で作らないため。order -> user の順で select_for_update() ロックを取り、同一顧客の残高変更を直列化する。CustomerCreditTransactionにsource_season(繰越元年度)・target_season(充当先年度)・recorded_in_season(記録時点のactive season)・reversal_of(取消対象の充当取引、自己参照)を追加し、繰越残高の動きを年度単位で追跡できるようにした。
CreditApplicationAllocation): 顧客残高は年度を問わず1つのプール(fungible)として扱うため、apply_credit_to_orderが充当を確定する際、その顧客の供給側取引(amount > 0の行。繰越加算・戻し)のうち未配賦残額があるものをcreated_at昇順(登録が古い順)で必要額に達するまで消費し、CreditApplicationAllocation(消費取引・元取引・配賦額)で内訳を記録する。1回の充当が複数年度由来の元取引にまたがる場合、配賦明細行も複数作成される。return_credit_on_cancelは、対象注文へのまだ取消されていないAPPLIED_TO_ORDER行を全件取得し、行ごとに1件ずつreversal_of付きのRETURNED_FROM_CANCELED_APPLIED_ORDERを作成する。reversal_ofはOneToOneFieldのため、同一の充当行への二重取消はDB制約で防止される。created_atとする供給側取引)として扱う。台帳は追記のみ(immutable)を維持する設計方針のため。reversal_ofを経由して取消対象のAPPLIED_TO_ORDER行、さらにallocations_consumedを辿ることで、「取り消された充当が具体的にどの元繰越取引からいくら配賦されたものか」まで一意に追跡できる。source_order/target_orderが設定されている行は、対応するsource_season/target_seasonがその注文のseasonと一致することをサービス層+CustomerCreditTransaction.save()の防御的ガードの両方で検証する。reversal_ofを設定する行も、参照先がAPPLIED_TO_ORDER種別であること・顧客が一致すること・充当先注文が一致することを同様に検証する。PaymentStatusUpdateView)URL: dashboard:mg_orders:api_order_payment_status_update(POST、JSON)。注文一覧からのトグル操作で入金ステータスを更新する。
payment_status が UNPAID/PAID のいずれかであることを検証(不正値は 400)。Order を select_for_update() でロックして更新(@transaction.atomic)。対象が無ければ 404。payment_status 変更を拒否する(400)。入金状態は繰越充当操作・キャンセル時の残高戻しで整合させる。staff_member_required + csrf_protect。注文ステータス(Order.OrderStatus)と入金ステータス(Order.PaymentStatus)の値は データベース設計 §2.3 を参照。受付(order_accept)以降の出荷フロー(袋詰・梱包・発送)は 出荷ワークフロー を参照。
年度締めドライラン(管理者向け補助機能 §2)は、本機能の Order.status / Order.payment_status / Order.final_shipping_fee を締め前チェックの対象として参照する。Phase 4-DでOrder.seasonを導入し、Phase 4-Hで締め前チェック(未受付・対応中・送料未確定・未入金の4項目)をOrder.season=closing_seasonで絞り込むよう統合した。締める年度に無関係な別年度の残存注文は締めをブロックしない。
管理者は共通年度を切り替えて各年度を参照できるが、1注文内では年度を混在させない: 新規注文はアクティブ年度にだけ作成でき、商品候補もアクティブ年度に限定する。既存注文の編集ではorder.seasonを変更せず、その年度の商品だけを追加候補にする。過去注文の「作り直し」も、新注文はアクティブ年度になり、品種・種類・容量が一致するアクティブ年度の商品へ対応付ける。
締め済み(CLOSED)年度の注文(Phase 4-H、決定事項8): 新規注文の作成・既存注文の増量(数量を増やす)・商品追加は拒否する(購入者・管理者どちらの経路も)。キャンセル・数量減少・商品削除は締め済みでも常に許可する(未入金の督促断念によるキャンセル等、締め後の後始末を可能にするため)。購入者側order_createはSystemSetting.active_seasonがCLOSEDを指す場合に拒否するが、通常の運用フローではこの状態には到達しない(active_seasonがCLOSEDを指すのは/admin/経由の手動操作のみ)。
元資料: specs/33_管理者向け注文管理機能 実装手順書.md (Ver.3.2) を現行コード(mg_orders/views.py, mg_orders/api.py)と照合して再構成
関連: README, specs棚卸し表, Issue #1