管理者向けの横断機能——ダッシュボード(ToDo)・デバッグ画面・旧システムデータ移行・オンラインマニュアル管理——を開発者向けにまとめます。これらは特定領域に属さない機能として dashboard アプリが提供します。
本書は
specs/36_管理者向け補助機能 実装手順書.md(Ver.3.0) を土台に、現行コード(dashboard)と照合して再構成したものです。挙動は実装を正とします。
関連: design/アーキテクチャ(dashboardは管理者機能の司令塔・入口)
| 機能 | URL 名 | 実装(dashboard/views.py) |
|---|---|---|
| ダッシュボード(ToDo) | dashboard:dashboard_top(/management/) |
dashboard_view |
| 共通販売年度の切替 | dashboard:management_season_select(/management/season/select/) |
management_season_select_view |
| 年度締めドライラン | dashboard:year_end(/management/year-end/) |
year_end_view |
| 年度締め確認結果の保存 | dashboard:year_end_save(/management/year-end/save/) |
year_end_save_view |
| 年度締めの実行 | dashboard:year_end_execute(/management/year-end/execute/) |
year_end_execute_view |
| DBバックアップのダウンロード | dashboard:year_end_backup_download(/management/year-end/backup-download/) |
year_end_backup_download_view |
| 販売年度一覧 | dashboard:season_list(/management/seasons/) |
season_list_view |
| 販売年度の新規作成 | dashboard:season_create(/management/seasons/create/) |
season_create_view |
| 開発者向けデバッグ | dashboard:debug_view(/management/debug/) |
debug_view |
| 旧システムデータ移行 | dashboard:import(/management/import/) |
import_old_data_view |
| オンラインマニュアル管理 | dashboard:manuals(/management/manuals/) |
manual_management_view |
| マニュアル本文取得 API | dashboard:api_manual_content(/management/api/manuals/<page_name>/) |
manual_content_view |
| CSRF シード API | dashboard:api_csrf_seed(/management/api/csrf/) |
csrf_seed |
manual_content_view / csrf_seed を除き @user_passes_test(is_staff_user) でスタッフ限定。
ダッシュボード・注文管理・商品/在庫マスタ・袋詰・自動引当・発送管理は、ログインセッション内で1つの販売年度を共有する。初期値は SystemSetting.active_season。切替はCSRF保護されたPOSTだけで行い、ログインのたびに明示選択を消してアクティブ年度へ戻す。
season_id とセッション選択の一致を検証する。一括更新に別年度のIDが1件でも混ざれば全件拒否する。dashboard_view(画面 SCR-AD-010)。ログイン後の管理者トップ。「次に何をすべきか」を件数付きで表示する。
| 項目 | コンテキスト | 集計 |
|---|---|---|
| 新規注文(未受付) | new_orders_count |
status=NEW の注文数 |
| 要袋詰 | bagging_total_bags |
ACCEPTED 注文明細の数量合計 |
| 未入金 | unpaid_customer_count |
未入金(キャンセル除く)の注文を持つ顧客数(distinct) |
| 要パッケージング | packaging_count |
PACKAGING のパッケージを持つ宛先数(distinct) |
| 発送準備完了 | shipping_packages_count |
READY_TO_SHIP のうち手渡し以外の箱数 |
| 手渡し待ち等 | ready_for_pickup_count |
READY_TO_SHIP のうち手渡しの箱数 |
すべての件数は共通選択中の販売年度だけで集計する。
year_end_view(/management/year-end/)。年度締め前チェックを一覧表示する管理者向け画面。チェック結果の表示自体(GET)は DB を一切変更しない。 POST で行う操作は2種類あり、「確認結果として保存」(year_end_save_view)は YearEndRun を1件 INSERT するだけ(業務データは変更しない)、「年度締めを実行」(year_end_execute_view、Phase 3b)は商品の販売停止と active_season の切替を伴う(下記 §2.2)。
実装案: 検討用/17_2025年のデータを閉めて2026年の受注業務に備える_実装案(段階構成の全体像・未決事項)
現行仕様には販売年度・作付年度を表す明示的なフィールドがない。そのため画面上の「締める年度」「次年度」は表示ラベルにすぎず、集計クエリは年度で絞り込まず「閉じていない(後工程が残っている)データ」を状態から全件対象にする。GET パラメータ closing_year / next_year(省略時は 今年-1 / 今年)で表示ラベルを切り替えられる。
集計ロジックは dashboard/services/year_end.py の build_year_end_preview(closing_year, next_year) に切り出し、view には直書きしない。各チェックは YearEndCheckItem(key / label / status / count / detail / items)を返し、YearEndPreview.can_close は BLOCK が1件もないことを示す。
| チェック項目 | 対象クエリ(概要) | 判定 |
|---|---|---|
| 未受付注文 | Order.status=NEW, season__year=closing_year |
1件以上で WARN(対応中注文とは重複させない) |
| 対応中注文 | NEW/CANCELED以外の注文について、注文数量とSHIPPED/DELIVERED状態の箱の引当数量を比較(season__year=closing_year) |
数量が一致しない注文が1件以上で BLOCK。個別に「確認済みとして除外」可(Issue #18) |
| 送料未確定注文 | NEW/CANCELED を除外し final_shipping_fee IS NULL、season__year=closing_year |
1件以上で BLOCK(NEW は参考送料のみでよいため対象外)。個別に「確認済みとして除外」可(Issue #18) |
| 未入金注文 | CANCELED を除外し payment_status=UNPAID、season__year=closing_year |
1件以上で BLOCK(判断理由は下記)。個別に「確認済みとして除外」可(Issue #18) |
| 発送管理の途中箱 | Package.status IN (PACKAGING, READY_TO_SHIP, LABEL_PRINTED)、season__year=closing_year |
PACKAGING が1件でもあれば BLOCK、なければ WARN(SHIPPED/DELIVERED は対象外)。個別に「確認済みとして除外」可(Issue #18) |
| 繰越残高が残っている顧客 | CustomerCreditTransaction.values('user_id').annotate(balance=Sum('amount')).exclude(balance=0) + User.objects.in_bulk |
1件以上で WARN(顧客単位の累計残高であり年度では絞り込まない) |
| 販売中商品(締める年度) | Product.status=FOR_SALE, season__year=closing_year |
1件以上で WARN(締め実行時にこれらの商品だけが SUSPENDED になる。next_year の商品は含まない) |
| 在庫一覧(締める年度) | SeasonStock.objects.filter(season__year=closing_year).select_related('variety', 'season') |
available_kg < 0(供給量超過)の品種があれば WARN、なければ OK(在庫は締め実行でも変更しない参考情報) |
未受付・対応中・送料未確定・未入金の注文4項目、発送管理の途中箱、在庫一覧、販売中商品は、いずれも season__year=closing_year(またはそれに準ずるFK比較)で締める年度に絞り込む(Issue #17 Phase 4-C〜4-F・4-H)。締める年度に無関係な別年度の残存(「塩漬け」)注文・途中箱があっても締めをブロックしない。繰越残高のみ、顧客単位の累計額という性質上、年度では絞り込まない。あわせて、次年度向けの SalesSeason が OPEN で存在するか(YearEndPreview.next_year_ready_count、season__year=next_year, status=OPEN で判定。Phase 4-H で「FOR_SALE 商品が1件以上」という基準から変更)を締め実行の前提条件として保持する。
「対応中注文」の判定基準(Issue #18): Order.status を PACKED/SHIPPED/DELIVERED へ進める書き込み処理はアプリ内のどこにも実装されておらず(order_accept による NEW→ACCEPTED のみ)、実際の出荷進捗は Package.status で管理されている。そのため Order.status ベースの判定では、正常に発送済みの注文も ACCEPTED のまま永久に「対応中」と判定されてしまう(本番由来データで実際に発生し発覚)。_raw_in_progress_orders()(dashboard/services/year_end.py)は、NEW/CANCELED を除いた注文ごとに注文明細の数量合計と、SHIPPED/DELIVERED 状態の箱に含まれる引当済み数量(Allocation→Bag→PackageItem→Package)を比較し、一致しない場合(未引当・未梱包・箱が PACKAGING/READY_TO_SHIP/LABEL_PRINTED のまま・複数箱の一部だけ発送済み等)を安全側で「対応中」として残す。
年度締めドライランのBLOCKを生成しうる4チェック(対応中注文・送料未確定注文・未入金注文・発送管理の途中箱)は、対象データ(Order/Package)の状態を一切変更せずに、個別の対象単位で「確認済みとして除外」できる。未受付注文・繰越残高・販売中商品・在庫の4チェックはWARN/OK専用でcan_closeに影響しないため対象外(実装時の判断)。
year_end.models.YearEndCheckAcknowledgement(closing_year・check_key・object_type・object_id・state_fingerprint・reason・acknowledged_by/acknowledged_at・revoked_by/revoked_at)。同一対象(closing_year+check_key+object_type+object_id)は常に高々1行を更新・再有効化する形で使い回す(解除後の再確認も新規行を作らない)。DB一意制約はMySQLが条件付きユニーク制約をサポートしないため、条件なしの一意制約+サービス層の運用で担保する。Orderの場合はstatus/payment_status/final_shipping_fee/注文数量/発送済み数量など、既存の各チェックのシリアライザ出力+αをハッシュ化したもの)をstate_fingerprintとして保存する。プレビュー生成のたびに現在の状態から同じ方法で再計算し、一致しなければ「確認後に対象データが変化した」とみなして除外を無効化する(要再確認。確認記録自体は監査目的で残る)。YearEndCheckItemのstatus/count/itemsは除外適用後の実効判定(can_closeの算出にはこちらを使う)。raw_status/raw_countは除外前の原判定。acknowledgedに現在有効な確認(staleフラグ付き)の一覧を持つ。dashboard/services/year_end.py・dashboard/services/year_end_acknowledgements.py): acknowledge_from_current_state()(現在のチェック結果から対象を再特定し、その時点の指紋で確認済み除外を作成。対象が現在の結果に含まれない場合はエラー)、revoke_acknowledgement()(解除。行は削除せず解除者・解除日時を記録)。/management/year-end/ の各チェック詳細のテーブルに、対象ごとのチェックボックス(ヘッダーの一括選択チェックボックスでグループ内すべてを選択/解除できる)と、テーブル下部に確認理由の入力欄(必須)+「チェックした対象を確認済みとして除外」ボタンを表示する。1回の送信でチェックした複数対象へ同じ確認理由を一括適用できる。確認済み一覧(確認理由・確認者・確認日時・解除ボタン、要再確認は警告表示)も表示する。year_end_check_acknowledge_view(POST /management/year-end/acknowledge/、object_idは複数値を受け取り一部失敗があっても有効な対象だけ処理する)・year_end_check_acknowledgement_revoke_view(POST /management/year-end/acknowledge/<id>/revoke/)はいずれもスタッフ限定・PRGパターン。dry_run_snapshot(serialize_year_end_preview、version=4)に各チェックのraw_status/raw_count/acknowledged(確認理由・確認者・確認日時・対象データ・staleかどうか)を含めるため、締め実行時に保存されるdry_run_snapshotからも「何が原判定でBLOCKだったか」「誰がいつ何を理由に除外したか」を後から追跡できる。Phase 4(4-A〜4-H)はすべて実装済み。
sales_seasons.SalesSeason(statusはOPEN/CLOSEDの2値のみ。「アクティブか」はSalesSeason側では持たずSystemSetting.active_seasonという単一のポインタでのみ表す。Issue #17 コメント#3256で確定)の導入(Phase 4-A)、SystemSetting.active_seasonへの型変更(Phase 4-B)、Product.seasonへの型変更(Phase 4-C)、Order.seasonの追加と年度混在防止(Phase 4-D)、SeasonStockへの切替(Phase 4-E)、Package.seasonの追加と送料確定のseason化(Phase 4-F)、顧客残高の年度監査フィールド(Phase 4-G)、締め済み年度ガードと年度締め実行への統合(Phase 4-H)は、いずれも実装済み(上記§2.2、商品在庫マスタ)。締め実行(execute_year_end())は同一トランザクションでclosingseason をOPEN→CLOSEDへ変更したうえでactive_seasonポインタをclosing→nextへ付け替える(Phase 4-H。CLOSED→OPENの逆遷移・締め取消は提供しない)。箱(Package)は計画商品・箱詰め済み袋の元注文明細と年度が一致することをサービス層のガードで保証し、年度混在箱を禁止する。送料確定(finalize_shipping_fee_if_all_packed)は宛先×season単位で確定し、旧season未完了箱が新season送料確定を妨げない。顧客残高(CustomerCreditTransaction)はsource_season/target_season/recorded_in_seasonにより繰越元年度・繰越先年度を直接記録する。締め済み年度(CLOSED)への新規注文・商品追加・数量増加・在庫供給量増加を禁止するが、キャンセル・数量減少は許可する(Phase 4-H、決定事項8)。詳細は 検討用/17_..._実装案 §Phase 4詳細設計 を参照。
未入金注文の判定を BLOCK にした理由(実装時の判断): 実装案では「WARN または BLOCK は運用判断」として未決だった。未入金のまま年度を閉じると回収漏れや繰越処理との混同につながりやすく、金銭に関わる項目のため安全側の判断が妥当と考え、BLOCK を採用した。請求残額は mg_orders.services.credit.remaining_billed_amount で繰越充当後の額を算出し、あわせて表示する。
繰越残高一覧は values('user_id').annotate(...) による DB 集計と User.objects.in_bulk() の2クエリのみで完結させ、顧客ごとに個別クエリを発行しない(N+1回避)。
画面下部には注文一覧・発送管理・自動引当ボード・商品マスタ・在庫マスタ・顧客一覧への遷移リンクを配置する。
year_end_save_view(POST /management/year-end/save/、dashboard:year_end_save)。現在のドライラン結果を「誰が・いつ・どの年度について確認したか」の記録として保存する。締め実行の可否判定ではないため、BLOCK 項目があっても保存できる。
@require_POST + スタッフ限定(@login_required + @user_passes_test(is_staff_user))。CSRF 保護は標準の CsrfViewMiddleware と {% csrf_token %} による。dashboard:year_end(保存時の closing_year/next_year をクエリ文字列で維持)へ redirect し、結果は django.contrib.messages で表示する。GET では何も保存しない。closing_year / next_year は POST から読み取るが、JSON スナップショットは サーバー側で build_year_end_preview() を再実行して生成する。クライアントから送信された集計内容(チェック結果そのもの)を信用しない。created_by は request.user から設定し、POST 値(created_by を含めても)は一切参照しない。dashboard.services.year_end.validate_closing_next_year() で検証する(整数であること、2000〜2100 の範囲、closing_year < next_year)。不正な場合は YearEndRunValidationError を送出し、YearEndRun は作成せずエラーメッセージ付きで redirect する。year_end.models.YearEndRunYearEndRun は新規アプリ year_end(プレフィックスなし)が持つ。配置判断の詳細は 検討用実装案「Phase 2: 年度切替ログモデル」節 と アーキテクチャ を参照。
| フィールド | 内容 |
|---|---|
closing_year / next_year |
締める年度・次年度(PositiveIntegerField) |
status |
Status.DRY_RUN_SAVED(ドライラン結果保存)の1値のみ。Phase 3 着手時に実行系の状態を追加する予定で、現時点では先取りしない |
dry_run_snapshot |
ドライラン結果の JSON スナップショット(後述) |
executed_actions |
実行した操作の JSON。Phase 2 では常に {}(Phase 2 は保存のみで実行しないため) |
created_by |
保存操作を行ったスタッフ(request.user、on_delete=PROTECT) |
created_at |
作成日時(auto_now_add) |
executed_at |
実行日時。Phase 2 では常に None |
PaymentAdjustment / CustomerCreditTransaction(orders アプリの監査ログ的モデル)が Django 管理サイトに登録されていない前例に合わせ、YearEndRun も /admin/ には登録せず、/management/year-end/ 画面からのみ参照する。
dashboard.services.year_end.serialize_year_end_preview)YearEndPreview は Order / Package / Product / Stock / User のモデルインスタンスや Decimal(weight_kg 等)・datetime(created_at 等)を含むため、そのまま JSONField へは保存できない(本 JSONField は encoder 未指定=標準 json エンコーダのため、Decimal/datetime を直接渡すと保存時に TypeError になる)。そこで serialize_year_end_preview(preview) -> dict がプリミティブ値のみの辞書へ変換してから保存する。
{
"version": 1,
"closing_year": 2025,
"next_year": 2026,
"can_close": false,
"checks": [
{"key": "unaccepted_orders", "label": "未受付注文", "status": "WARN", "count": 1, "detail": "...", "items": [ /* 後述 */ ]}
]
}
key / label / status / count / detail / items を保存する。items の中身はチェックの種類ごとに個別のシリアライザ(_serialize_order / _serialize_package / _serialize_unpaid_order_row / _serialize_credit_balance_row / _serialize_product / _serialize_stock)で変換し、id・状態・表示用文字列など「後から追跡できる最低限の情報」のみを複製する。個人情報は表示に使う username のみとし、メールアドレス等は複製しない。Decimal(weight_kg / total_supplied_kg 等)は str() に変換し、datetime(created_at 等)は isoformat() に変換する。金額系(final_shipping_fee / total_price / balance 等)はもともと IntegerField またはその合計のため int のまま保存できる。version フィールドを持たせる: チェック項目やシリアライズ形式は変わりうるため、version を持たせることで、将来のコードが過去に保存済みのスナップショットを形式ごとに区別して解釈できるようにする(元の Order/Package 等のレコードが変更・削除された後でも、保存当時どの形式で判定したかを確認できる)。version=1(Phase 2)→ version=2(Phase 3b。products_for_sale を closing_year で絞り込むよう変更し、商品項目に sales_year を追加、トップレベルに next_year_ready_count を追加)→ version=3(Phase 4-H。未受付・対応中・送料未確定・未入金の注文4チェックをOrder.seasonベースでclosing_yearに絞り込むよう変更し、next_year_ready_countの判定基準を「next_yearのSalesSeasonがOPENで存在するか」へ変更)。/management/year-end/ の GET レスポンスに以下を追加する。
closing_year/next_year を hidden field に持つ POST フォーム)。BLOCK があっても押せる。dashboard.services.year_end.list_year_end_run_history()、select_related('created_by') で N+1 回避、新しい順に最大50件)。対象年度・次年度・種別(確認記録/締め実行)・判定結果(dry_run_snapshot.can_close から BLOCKなし/BLOCKあり バッジ)・変更商品数(EXECUTED のみ)・作成者・作成日時を表示し、<details> でスナップショット詳細・実行内容(executed_actions)を展開できる。year_end_backup_download_view(POST /management/year-end/backup-download/、dashboard:year_end_backup_download)。年度締め画面の「DBバックアップを確認しました」チェックボックス付近から、その場で mysqldump(dashboard.services.db_backup.create_mysql_backup())を実行し、ダンプをそのまま HTTP レスポンスとしてダウンロードさせる。サーバー側にはファイルを保存しない。
GET は405)。年度締めの実行可否(can_execute)とは独立しており、実行前提条件を満たさない状態でもダウンロードできる(バックアップ取得自体は読み取り専用で、年度締めの実行に影響しないため)。riceshop_user は対象DBに ALL PRIVILEGES を持つ)。mysqldump の実行に失敗した場合(DatabaseBackupError)は {"error": "..."} の JSON を 500 で返す。templates/dashboard/year_end.html)は fetch でこのエンドポイントを呼び、レスポンスの Content-Disposition からファイル名を取り出して Blob としてダウンロードさせる。ダウンロードに成功すると backup_confirmed チェックボックスを自動でチェックする。mysqldump コマンド自体は Dockerfile に default-mysql-client を追加して導入した(development/開発環境構築 §2.1)。ボタンが使えない場合の手動コマンドは引き続き画面内の折りたたみと operations/データ移行 §4 に残している。year_end_execute_view(POST /management/year-end/execute/、dashboard:year_end_execute)。年度締めを実際に実行し、closing_year 向けの販売中商品を停止し、closing 側の SalesSeason.status を OPEN→CLOSED へ遷移させたうえで、SystemSetting.active_season を next_year に対応する SalesSeason へ切り替える(Issue #17 Phase 4-B で active_sales_year(int) から型変更。SalesSeason.status の OPEN→CLOSED 遷移はPhase 4-Hで追加。逆遷移・締め取消は提供しない)。
CsrfViewMiddleware)。closing_year / next_year が妥当(validate_closing_next_year: 整数・2000〜2100・closing_year < next_year)。SystemSetting.active_season.year が closing_year と一致する。BLOCK がない。year=next_year かつ status=OPEN の SalesSeason が存在する(next_year_ready_count >= 1。Phase 4-H で「FOR_SALE 商品が1件以上」という基準から変更。次年度商品を自動で販売開始にすることはなく、事前に管理者が商品マスタで「販売開始」にした商品だけが対象になる点は変わらない)。不足している場合、年度締め画面に{{ next_year }}年度のSalesSeasonを今すぐ作成するボタンを表示し、/management/seasons/へ画面遷移せずその場で作成できる(season_create_viewへPOSTし、成功後はnextパラメータ―サイト内の相対パスのみ許可、オープンリダイレクト対策―で元の年度締め画面(closing_year/next_yearのクエリ文字列を維持)へ直接戻る。UX指摘対応)。warnings_confirmed)。backup_confirmed)。自己申告のチェックボックスであることは変わらないが、§2.1.1のダウンロードボタンを使ってバックアップを取得した場合は成功時に自動でチェックされる。closing_year を再入力した値が対象年度と一致する(closing_year_confirm)。これらは dashboard.services.year_end.execute_year_end() が単一の transaction.atomic() 内で検証・実行する。画面表示時点の状態(can_execute)はUIの活性/非活性制御にのみ使い、実行可否の最終判定はサーバー側の再検証に委ねる。
SystemSetting を select_for_update() でロックし、active_season.year を再確認する。BLOCK があれば中止する。season=setting.active_season(ロック済みの SalesSeason インスタンス。closing_year と一致確認済み)かつ status=FOR_SALE の商品を select_for_update() でロックし、SUSPENDED へ一括変更する(next_year の商品には触れない)。year=next_year, status=OPEN の SalesSeason を取得する(存在しない場合は YearEndExecutionError で中止する)。closing 側の SalesSeason を status=CLOSED・closed_at・closed_by を設定して OPEN→CLOSED へ遷移させる(Phase 4-H。逆遷移は提供しない)。SystemSetting.active_season を手順4で取得した next 側の SalesSeason へ変更する。YearEndRun を status=EXECUTED として作成し、dry_run_snapshot(実行直前のプレビュー)・executed_actions(変更内容)・executed_at を保存する。変更しないもの: 注文・注文明細・箱・入金・繰越残高・在庫・品種・next_year 商品の販売ステータス。
SystemSetting の行ロックと active_season.year の再確認を主ガードにする。1回目の成功で active_season が closing_year から next_year の SalesSeason へ変わるため、同じ closing_year での2回目の実行は、ロック取得後の再確認で自然に拒否される(条件付き UniqueConstraint 等の MySQL で効かない仕組みには依存しない)。同時POSTも、後着のトランザクションが先着のコミットを待ってから再確認するため、重複変更は起きない。実行後は PRG パターンで dashboard:year_end へリダイレクトする。
注文確定(orders.views.order_create)との同時実行対策(Issue #17 Phase 3b レビュー指摘): 当初、order_create は購入可否検証時に SystemSetting.load() でロックなしに active_sales_year を読んでいたため、年度締めが SystemSetting をロックして再集計している間に、注文確定が旧年度の active_sales_year を読んで購入可能と誤判定し、年度締め完了後に旧年度商品の未受付注文が残る競合があった。order_create も同じ SystemSetting 行(pk=1)を select_for_update() でロックしてから active_season_id を読むように修正し、ロックは注文作成が完了するまで(transaction.atomic が commit するまで)保持する。これにより、どちらが先にロックを取得しても以下が保証される。
payment_status=UNPAID(デフォルト)で作成されるため _check_unpaid_orders が BLOCK を検出し、年度締めは中止される。active_season_id(next_yearに対応するSalesSeasonのid)を読む。旧年度商品は product.season_id が一致しない(かつ SUSPENDED に変更済み)ため is_product_purchasable が False を返し、注文確定は拒否される。ロック取得順は両者とも「SystemSetting → (年度締めは Product、注文確定は Stock)」であり、Product と Stock の間で相互にロック待ちが発生する経路はないため、デッドロックは起きない。回帰テストは orders.tests.OrderCreateYearEndConcurrencyTests(threading.Event で実際のロック取得順を制御し、両方向を検証)。
executed_actions の形式{
"version": 2,
"action": "YEAR_END_EXECUTE",
"closing_year": 2025,
"next_year": 2026,
"closing_season_id": 1,
"next_season_id": 2,
"product_count": 3,
"products": [
{"product_id": 12, "name": "コシヒカリ 玄米 5kg", "sales_year": 2025, "before_status": "販売開始", "after_status": "販売停止中"}
],
"warnings_confirmed": true,
"backup_confirmed": true,
"active_sales_year_before": 2025,
"active_sales_year_after": 2026,
"executed_by_id": 3,
"executed_by_username": "staff",
"executed_at": "2026-07-14T09:00:00+09:00"
}
JSONField 保存可能なプリミティブ値のみで構成する(Product.name は変更しない)。version(executed_actions 自体のスキーマ版)は dry_run_snapshot の version とは独立したカウンタ。closing_season_id/next_season_id の追加に伴い version=1(Phase 3b)→ version=2(Phase 4-H)へ上げた。
年度締め画面の「締め実行」エリアに、現在の active_season.year、実行前提条件を満たさない理由(execution_blockers)、実行の影響範囲(対象年度商品の販売停止・closing season が CLOSED になり締め取消・再開ができないこと・active_season の切替・在庫や注文は変更されないこと・next_year 商品は自動で販売開始されないこと)を明示する。実行前提条件を満たさない場合はチェックボックス・年度再入力欄・実行ボタンをすべて非活性にする(§2.1.1のDBバックアップダウンロードボタンは対象外。読み取り専用で年度締めの実行に影響しないため、常に押せる)。
season_list_view(/management/seasons/)・season_create_view(/management/seasons/create/)。sales_seasons.SalesSeason(Phase 4-A)の新規作成のみを許可する最小画面。Phase 4-Aレビュー対応でSalesSeasonのDjango admin登録は撤回済みのため、本画面が唯一の作成経路になる。
season_list_view)SalesSeason.objects.order_by('-year')で全件表示する。列は販売年度・状態(OPEN/CLOSED)・アクティブ年度かどうか・締め日時・締め実行者。「アクティブ年度かどうか」はSalesSeason自身には保存されておらず、SystemSetting.load().active_seasonとの比較(season.id == active_season_id)でその場で導出する(2.2節・データベース設計 §2.1と同じ「正データの二重化を避ける」設計方針)。
season_create_view)dashboard.forms.SalesSeasonCreateForm(ModelForm、Meta.fields = ['year'])を使う。 status・closed_at・closed_byはフォームが認識するフィールド自体に存在しないため、POSTされても一切バインドされない。作成されるSalesSeasonのstatusは常にモデルの既定値(OPEN)になる。year > SystemSetting.active_season.year。アクティブ年度と同じ年度・過去年度は拒否する。翌年度だけに限定せず、複数年先の年度も作成できる。過去年度をOPENで作成できてしまうと、その年度はactive_seasonにならず、状態編集・再開も許可しない設計のため、正規の締め処理を通せない不正なOPEN年度として残ってしまうことが理由。clean_year()でPOST処理時にSystemSetting.load().active_season.yearを読み直してyear > active_season.yearを再検証する。request.POST.get('year')を手動でint()変換していたが、SalesSeason.year(PositiveIntegerField)が本来持つDB範囲チェック(MySQLのINT UNSIGNED範囲を超える極端に大きい整数など)を経由せず、そのような値はDataError等の未捕捉例外で500になり得た。ModelFormはis_valid()内部でModel.full_clean()を呼ぶため、この範囲チェックがフォームエラーとして扱われる。極端に大きい整数・非数値の入力はいずれもDBへ到達する前にフォームエラーになる。ModelFormが自動的に行う一意検証(Model.validate_unique())でフォームエラーとして表示する(一次防御)のに加え、SalesSeason.yearのDB一意制約を最終防衛線として維持する。事前チェックをすり抜けた同時POSTによる重複作成はform.save()実行時のIntegrityErrorを捕捉し、form.add_error()で利用者向けエラーへ変換する(500にしない。mg_mastersの商品作成と同じtry/except IntegrityErrorパターン)。year欄は、active_season.year + 1以上で最小の未登録年度を初期表示する(例: active=2025、2026登録済みなら2027)。入力の手間を減らす利便性のためであり、利用者はそれより先の年度も入力できる。status・closed_at・closed_byの変更、CLOSED → OPEN、締め取消・再開、active_seasonの直接切替。状態変更とactive_seasonの切替は、Phase 4-Hのexecute_year_end()だけが行う(2.2節の設計方針を維持)。nextパラメータによる呼び出し元への復帰(UX指摘対応): POSTにnext(サイト内の相対パスのみ。/で始まり//で始まらないもの以外は無視してオープンリダイレクトを防ぐ)を含めると、作成成功後にseason_listではなくnextで指定した画面へredirectする。年度締め画面(2.2節)の「今すぐ作成する」ボタンはこれを使い、/management/seasons/へ画面遷移せず、かつclosing_year/next_yearのクエリ文字列を保ったまま年度締め画面へ戻れるようにしている。debug_view(画面 SCR-AD-099)。データ整合性を目視確認するための内部ダンプ画面。
bag_id まで)。Bag)の状態(未準備/未引当(準備済み在庫)/未梱包/箱入 + 引当先・格納先)。available_kg)。import_old_data_view(画面 SCR-AD-089、GET 表示/POST 取込)。旧システムからエクスポートした UTF-8 の SQL ファイルをアップロードして顧客・お届け先を取り込む。
INSERT INTO 文を正規表現で解析し、日本語テーブル名 販売システムユーザー(→ User)と 顧客名簿(→ Destination)の値タプルを抽出(_parse_sql_values が NULL・クオート・数値を解釈)。TEWATASHI、「遠地」→ ENCHI にマッピング(それ以外は YAMATO/NORMAL)。住所は複数カラムを連結。User の対応表で、お届け先を所有ユーザーに紐づける。重要(破壊的な全置換・
@transaction.atomic): 取込は追記ではなく全置換。トランザクション内で 全Package削除 → 非スタッフ非superuserユーザーの注文削除 → 全Destination削除 → 当該ユーザー削除の後に、新規ユーザー・お届け先を作成する。既存の顧客データ・注文・パッケージは消えるため、本番では取り扱い注意(operations/データ移行 も参照)。
Issue #9 対応済み:
dashboard/views.pyはdjango.shortcuts.redirectを import 済み。SQL 未選択時・取込対象なし時・取込完了/失敗後のredirect('dashboard:import')と、マニュアル削除後のredirect('dashboard:manuals')はNameErrorにならず実行できる。
manual_management_view(画面 SCR-AD-087)。static/manuals/ 配下の Markdown(.md)ファイルを管理する。
.md を受け取り保存。同名ファイルがある場合は一旦 confirm_overwrite を返し、確認後に上書き保存する。.md 以外やパストラバーサル文字(.. / \)を含む名前は除外。.md を削除(不正名・非存在は拒否)。dashboard:manuals を参照する(Issue #10 対応済み)。manual_content_view)URL dashboard:api_manual_content。コンテキスト対応ヘルプ用に、page_name に対応する Markdown を返す(text/markdown)。
index を要求した場合は index_admin.md を返す(管理者向け目次)。それ以外は <page_name>.md。page_name は Http404。@login_required のみ(スタッフ限定ではない=購入者のヘルプ表示にも使う想定)。現行実装上の注意(API と JS の乖離): この API は実装上存在するが、現行のフロント JS(
static/js/manual.js)は API を呼ばず/static/manuals/<page>.mdと/static/manuals/index.mdを直接fetchしている。そのためindex→index_admin.md差し替えなど API 側のロジックは効かず、またstatic/manuals/にはindex_admin.md/index_user.mdはあるがindex.mdが無いため、JS の目次/フォールバック経路が 404 になりうる。API 経路へ寄せるか JS 側の参照先を整理するか、実装方針の確認が必要。
csrf_seedURL dashboard:api_csrf_seed。@ensure_csrf_cookie で CSRF クッキーを発行する補助 API(非同期 POST 前の取得用)。
元資料: specs/36_管理者向け補助機能 実装手順書.md (Ver.3.0) を現行コード(dashboard/views.py, dashboard/urls.py)と照合して再構成
関連: README, specs棚卸し表, Issue #1