Issue #17 の年度締め実行後、SystemSetting.active_season は次年度へ切り替わる。しかし現状は、商品マスタ・在庫マスタ・新規注文画面の一部だけがアクティブ年度を初期値としており、ダッシュボード、注文管理、袋詰管理、パッケージング、発送管理では複数年度のデータが混在する。
本Issueでは、管理者向け業務画面に共通の販売年度コンテキストを導入する。年度締め直後は新しいアクティブ年度を初期表示し、過去年度は履歴確認と既存データの後処理のため選択可能にする。同時に、画面だけを絞り込んでAPIが全年度を操作する状態を避けるため、読み取り・更新の両方をサーバー側で年度分離する。
関連Issue:
SystemSetting.active_season は年度締め実行時に次年度へ切り替わる。Product、Order、Package、SeasonStock は SalesSeason を参照する。| 画面・処理 | 現状 |
|---|---|
| ダッシュボード | 注文・袋詰・未入金・箱を全年度で集計 |
| 注文管理 | 年度フィルタがなく全年度を一覧表示 |
| 袋詰管理 | ACCEPTED注文と準備済み袋を全年度で集計 |
| パッケージングボード | ボード状態・未引当・袋・箱・提案を全年度で取得 |
| 発送管理 | PACKAGING以外の箱、CSV、ステータス更新を全年度で扱う |
| 管理画面サイドバーの袋在庫サマリー | 全年度を合算 |
| 商品・在庫マスタ | 年度解決処理が各viewに個別実装され、共通選択とは連動しない |
この状態では、画面上で2025年度と2026年度の業務が混ざるだけでなく、一括梱包やCSV出力などの操作対象にも別年度が混入し得る。
SystemSetting.active_season とする。next_season へ切り替える。SalesSeason を選択肢に表示し、「アクティブ」「締め済み」を明示する。MANAGEMENT_SEASON_SESSION_KEY をセッションから削除する。4.1節)。「締め済みでない(OPEN)」を一律に「新規作成してよい」とは扱わない。OPENの年度には、性質が異なる2つの状態がある。
SystemSetting.active_season): 現在営業中の年度。一般顧客向けの商品一覧・カート・注文は、本Issueの前後を問わず常にこの年度だけを対象とする。SalesSeasonを締め実行前に作成しておく運用(season_create_view)が既にあるため、この状態は実際に発生する(次年度の商品・在庫を締め前に準備するための状態であり、まだ営業中ではない)。この2つを区別せず「OPENなら新規作成可」としてしまうと、一般顧客には閉じているはずの準備中の年度への新規注文が、スタッフ向け画面からだけ作れてしまう、といった抜け道が生じる。そのため新規作成系の操作は、対象ごとに次のいずれかの基準を使う。
| 操作 | 許可される年度の状態 | 理由 |
|---|---|---|
| 商品の新規作成、在庫供給量の追加 | アクティブ または 準備中(CLOSED以外) | 次年度分の商品・在庫は、年度が切り替わってアクティブになる前に準備しておく必要がある(Issue #22関連)。 |
| 新規注文の作成、新しい袋の作成、新しい箱の作成 | アクティブのみ | 一般顧客の注文経路は常にアクティブ年度限定であり、スタッフ向け画面だけ準備中の年度に新規の実業務(注文・袋・箱)を発生させる理由がない。 |
dashboard/services/management_season.py には、この2基準に対応する2つのガード関数を用意する(4.1節の require_open_season / require_active_season)。
CLOSED年度は読み取り専用に固定するのではなく、年度締め後も必要な既存データの後処理を許可する。判断基準は「締めた年度の数字(注文数量・在庫供給量など)を後から増やすかどうか」であり、増やさない操作は許可し、増やす操作・新しく記録を作る操作は禁止する(Issue #17 決定事項8)。
許可する例:
禁止する例:
order_edit。既存実装済み、Issue #17 Phase 4-H 決定事項8)これらの禁止は、業務上絶対に不可能でなければならないという意味ではなく、共通年度セレクタの導入でスタッフが年度を選び間違えたまま新規記録を作ってしまう事故を防ぐためのガードレールである。本当に必要な例外的訂正は、既存の在庫マスタ(stock_list_view)と同様、通常の管理画面では塞いだ上でDjango管理サイト経由に限定する。
既存のIssue #17 Phase 4-Hで定めた「CLOSED年度でもキャンセル・数量減少を許可する」方針を維持する。
次のモデル・画面は販売年度を持たないため、本Issueでは年度フィルタを追加しない。
ProductVariety / 品種マスタMillingRecord / 精米管理EmptyBagStock / 空袋在庫これらに年度を導入する場合は、別Issueでモデル設計から行う。
新規ファイル例:
dashboard/services/management_season.py
提供する責務:
MANAGEMENT_SEASON_SESSION_KEY = 'management_season_id'
def get_management_season(request) -> SalesSeason:
"""セッションの明示選択を返す。未選択・不正時はactive season。"""
def set_management_season(request, season: SalesSeason) -> None:
"""スタッフが選択した年度をセッションへ保存する。"""
def clear_management_season(request) -> None:
"""必要時に明示選択を解除し、active追従へ戻す。"""
def require_open_season(season: SalesSeason, *, operation: str) -> None:
"""商品・在庫など、準備中の年度でも許可する新規作成系操作のガード。CLOSEDだけを拒否する。"""
def require_active_season(season: SalesSeason, *, operation: str) -> None:
"""注文・袋・箱など、アクティブ年度でしか許可しない新規作成系操作のガード。
準備中のOPEN年度・CLOSED年度のどちらも拒否する。"""
def object_matches_season(obj, season: SalesSeason) -> bool:
"""Order/Package/Product等のseason_id一致検証に使う。"""
注意事項:
SalesSeason の存在を確認する。SystemSetting.load().active_season はフォールバックにのみ用いる。season_id でも検証する。django.contrib.auth.signals.user_logged_in に receiver を登録し、ログインのたびに clear_management_season(request) を呼んで明示選択をセッションから削除する(3.1節)。これにより、切り替えた選択は同一ログインセッション中だけ有効になり、次回ログイン時は必ずactive seasonから始まる。dashboard/urls.py にスタッフ限定POSTを追加する。
例:
POST /management/season/select/
POST項目:
season: SalesSeason.pknext: 選択後に戻るサイト内URL処理:
season が実在することを確認する。next は url_has_allowed_host_and_scheme() でサイト内URLだけを許可する。next はダッシュボードへフォールバックする。GETでセッションを書き換えない。年度変更はCSRF保護されたPOSTだけに限定する。
dashboard/context_processors.py にスタッフ向けの年度コンテキストを追加し、config/settings.py のテンプレートcontext processorへ登録する。
テンプレートへ渡す値:
management_seasons: 全年度(-year順)management_season: 現在選択中の年度active_management_season: 現在のアクティブ年度management_season_is_closedmanagement_season_is_active: 選択年度がアクティブ年度と一致するか(常時警告バナーの表示条件に使う)非ログイン利用者・一般顧客には空辞書を返し、管理画面以外への不要なDBアクセスを避ける。必要ならURL名前空間またはrequest pathで /management/ 配下だけに限定する。
新規partial例:
templates/dashboard/_management_season_selector.html
対象テンプレートから base_management.html の専用blockを有効化し、画面上部に次を表示する。
management_season_is_active が偽の間(CLOSED・準備中OPENいずれも)、画面上部に常時表示の警告バナーを出す(例:「今はアクティブ年度以外を表示中です。新規の注文・袋・箱は作成できません」)。CLOSED限定にせず、非アクティブ全般に適用する。スクロールしても見失わないよう固定表示にする。年度セレクタを表示する対象:
dashboard/dashboard.htmlmg_orders/order_list.htmlmg_orders/order_create.htmlmg_workflow/bagging_list.htmlmg_workflow/packaging_board.htmlmg_workflow/shipping_list.htmlmg_masters/product_list.htmlmg_masters/product_form.htmlmg_masters/stock_list.html年度締め画面は closing_year / next_year という独自の対象年度を持つため、共通セレクタを表示しない。
order_create.html は、共通セレクタの値に関わらず新規注文は常にアクティブ年度で作成される(3.2節)。共通選択がアクティブ年度でない場合も、セレクタ自体は表示を継続し、新規作成ボタンだけを無効化する。
対象:
dashboard/services/management_season.py(新規)dashboard/views.pydashboard/urls.pydashboard/context_processors.pyconfig/settings.pytemplates/base_management.htmltemplates/dashboard/_management_season_selector.html(新規)実装内容:
year_end_execute_view() の成功時に、実行した操作者の選択年度を next_season へ更新する。user_logged_in シグナルの receiver を追加し、ログインのたびに明示選択をセッションから削除する。テスト:
next リダイレクトを拒否する。management_season_is_active が偽になり、警告バナーが表示される。対象:
dashboard/views.py::dashboard_viewtemplates/dashboard/dashboard.htmlすべての業務集計を選択年度で絞る。
Order.objects.filter(season=management_season, ...)
OrderItem.objects.filter(order__season=management_season, ...)
Package.objects.filter(season=management_season, ...)
変更する件数:
画面タイトル付近に「2026年度のToDo」のように選択年度を明示する。各遷移先はセッションで同じ年度を解決するため、別年度へ戻らない。
テスト:
対象:
mg_orders/views.py::order_listmg_orders/views.py::order_createmg_orders/views.py::_order_create_contextmg_orders/views.py::_order_create_posttemplates/mg_orders/order_list.htmltemplates/mg_orders/order_create.html一覧:
Order.objects.filter(season=management_season) から開始する。select_related() に season を追加する。新規作成:
season がアクティブ年度と一致することを require_active_season で再検証する。FOR_SALE に限定する。既存注文操作:
order_editに実装済み)。対象:
mg_masters/views.pytemplates/mg_masters/product_list.htmltemplates/mg_masters/product_form.htmltemplates/mg_masters/stock_list.html商品一覧:
GET season / season=all 独自処理を共通年度コンテキストへ置き換える。商品作成:
_resolve_season() は存在確認だけなので、require_open_season によるOPEN検証を追加する。商品編集の年度変更(product_edit_view):
Bag は自身の年度を持たず、常に product.season を参照して年度を判定する(orders/models.py Bag)。一方 Package.season は箱作成時点で固定される独立FKであり、梱包まわりの各所は「袋の商品の年度=箱の年度」を前提にガードしている(Issue #17 Phase 4-F)。そのため商品の年度を後から変更すると、箱・袋を一切触っていないのに整合性が壊れる。これは新しい業務量を増やす操作ではなく、既存データの整合性破壊なので、3.2/3.3節の基準とは別に、次の基準で確定する。
OrderItem または Bag に使われた商品は、年度変更を一切禁止する(画面の年度セレクタを無効化し、改ざんPOSTもサーバー側で拒否する)。「使われているか」の判定は product_list_view の is_deletable(is_used_in_orderitems / is_used_in_bags)と同じ考え方を流用する。require_open_season)。在庫:
SeasonStock を get_or_create する。?season=<id> URLは互換期間中だけ受け付け、妥当なら共通選択へ同期するか、共通選択画面へリダイレクトする。恒久的に2つの年度状態を持たない。対象:
mg_workflow/views.py::bagging_list_viewtemplates/mg_workflow/bagging_list.htmldashboard/context_processors.py::bag_stock_summaryGET集計:
OrderItem.order.season=management_seasonBag.product.season=management_seasonPOST:
product_id の全商品が選択年度に属することを検証する。Bag の作成は選択年度がアクティブ年度の場合だけ許可する(require_active_season。準備中のOPEN年度・CLOSED年度のどちらでも作成しない)。スタッフ用サイドバー:
bag_stock_summary の注文・袋・箱集計を選択年度へ限定する。stock_summary は従来どおりactive season固定とし、管理者のセッション選択に影響させない。対象:
mg_workflow/views.py::packaging_board_viewtemplates/mg_workflow/packaging_board.htmlmg_workflow/static/js/packaging_board.jsmg_workflow/api_packaging.py画面とJS:
data-season-id として渡す。season_id を付ける。season_id を含める。appState を使わない。読み取りAPIで年度を必須化する対象:
BoardStateViewPreparedBagsViewAllocatableItemsViewSidebarStockSummaryViewProposePackingViewProposeGlobalPackingView絞り込み規則:
order__season=selected_seasonproduct__season=selected_seasonpackage__season=selected_season_ordered_qty()、_allocated_qty()、_planned_qty_dest()、_iter_order_items_with_demand() 等の内部関数へ season を明示的に渡し、宛先と商品IDだけで全年度を合算しない。_get_repack_assets()、_get_global_repack_assets()、_calculate_global_proposal() も選択年度を必須引数にする。更新API:
require_active_season。準備中のOPEN年度でも作成しない)。新規注文・新しい袋の作成をアクティブ年度限定にしたことで、梱包対象の元データ自体がアクティブ年度以外に新規発生することはなくなるが、防御としてAPI側でも検証する。PackageCreateView は暗黙のactive fallbackを廃止し、画面から明示されたアクティブ年度とACCEPTED注文年度を検証する。season_id と選択年度が一致しなければ season_mismatch で拒否する。セキュリティ上、クライアントの非表示・disabledだけに依存しない。すべての更新APIでサーバー側検証を行う。
対象:
templates/mg_workflow/shipping_list.htmlmg_workflow/static/js/shipping_list.jsmg_workflow/api_shipping.py読み取り:
ShippingDataView に season_id を必須で渡し、Package.season で絞る。season_id / season_year を含め、画面にも年度を表示する。更新:
package_ids が選択年度に属することを、更新前に一括検証する。更新候補:
docs/features/管理者向け補助機能.mddocs/features/注文管理.mddocs/features/出荷ワークフロー.mddocs/features/商品在庫マスタ.mddocs/design/アーキテクチャ.md記載する内容:
少なくとも次を用意する。
共通年度:
画面:
API:
CLOSED年度:
準備中のOPEN年度(OPENだが非active):
require_active_season)商品の年度変更:
OrderItemまたはBagに一度でも使われた商品は、年度変更(改ざんPOSTを含む)を拒否する回帰:
推奨順序:
画面だけを先に公開するとAPI操作が全年度のまま残るため、各画面と対応APIは同じコミットまたは同じデプロイ単位で切り替える。
本Issueの基本設計では新規DBカラムは不要で、Djangoセッションと既存の SalesSeason FKを使う。そのため通常はマイグレーション不要である。
デプロイ前確認:
docker compose -f docker-compose.dev.yml exec app python manage.py test dashboard mg_orders mg_workflow mg_masters
docker compose -f docker-compose.dev.yml exec app python manage.py test
docker compose -f docker-compose.dev.yml exec app python manage.py check
docker compose -f docker-compose.dev.yml exec app python manage.py makemigrations --check --dry-run
本番反映時はIssue #17・#18の未反映マイグレーションが別途存在する可能性があるため、本Issueだけを単独でデプロイできるとは限らない。適用予定マイグレーションと本番バックアップを確認してから、通常の git pull / docker compose up -d --build 手順で反映する。