riceshop のドメインモデル(テーブル)定義・リレーション・主要な制約を、開発者向けにまとめます。
本書は
specs/04_データベース設計書.md(Ver.2.2) とspecs/ER_DIAGRAM.mdを統合し、現行コード(各アプリのmodels.py)と照合して再構成したものです。フィールドの型・必須・選択肢は実装を正とし、specs との差分は本文中で明示します。
凡例: 必須欄の ● は NOT NULL(null=False)。
User(顧客アカウント) — AbstractUser を継承
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| ログインID | username |
CharField(150) | ● | ユニーク。CustomUnicodeUsernameValidator によりひらがな・カタカナ・漢字・スペース・半角英数字・@.+-_ を許可 |
| メールアドレス | email |
EmailField | 任意(blank=True)。REQUIRED_FIELDS ではあるが空文字許容 |
|
| パスワード | password |
CharField(128) | ● | ハッシュ化 |
| スタッフ権限 | is_staff |
BooleanField | ● | True で管理者扱い |
Destination(お届け先)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 顧客 | user |
FK→User (CASCADE) | ● | 所有顧客 |
| 宛名 | name |
CharField(255) | ● | |
| 郵便番号 | postal_code |
CharField(10) | ● | |
| 住所 | address |
CharField(255) | ● | |
| 電話番号 | phone |
CharField(20) | ● | |
| 配送方法 | delivery_method |
CharField(10) | ● | YAMATO(ヤマト配送)/ TEWATASHI(直接手渡し)。既定 YAMATO |
| 送料区分 | shipping_zone |
CharField(10) | ● | NORMAL(通常)/ ENCHI(遠地)。既定 NORMAL |
| 削除フラグ | is_deleted |
BooleanField | ● | ソフトデリート用。既定 False |
specs との差分:
is_deleted(ソフトデリート)は04に未記載(ER_DIAGRAMには記載あり)。お届け先は物理削除せずフラグで論理削除する。
ProductVariety(品種マスタ)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 品種名 | name |
CharField(100) | ● | 例「コシヒカリ」 |
| 基準重量(kg) | base_weight_kg |
Decimal(5,2) | ● | 価格計算の基準重量 |
| 基準価格(円) | base_price |
IntegerField | ● | 基準重量あたりの価格 |
Product(商品マスタ)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 品種 | variety |
FK→ProductVariety (CASCADE) | ● | |
| 販売年度 | season |
FK→SalesSeason (PROTECT) | ● | Issue #17 Phase 3a で sales_year(int)として追加、Phase 4-C で SalesSeason へのFKへ型変更。存在しない年度の商品をDB制約(FK)そのものが防ぐ。読み取り専用の互換プロパティ Product.sales_year(return self.season.year)を提供するが、ORMクエリには使えない |
| 商品名 | name |
CharField(255) | save() で「品種 種類 容量kg」形式に自動生成。年度は含めない(Issue #17 コメント #3238) |
|
| 種類 | type |
CharField(10) | ● | DB 値は 玄米 / 精米(日本語そのもの) |
| 容量(kg) | weight_kg |
Decimal(5,2) | ● | |
| 価格係数 | price_coefficient |
Decimal(5,3) | ● | |
| 販売ステータス | status |
CharField(10) | ● | DB 値は 未公開 / 販売開始 / 販売停止中。既定 未公開 |
| 価格(円) | price |
IntegerField | ● | 既定 1000 |
specs との差分:
typeとstatusの DB 値は、04記載の英語コード(GENMAI/SEIMAI、PREPARATION/FOR_SALE)ではなく 日本語文字列そのものを格納する(products/models.pyのTextChoices)。statusには販売停止中(SUSPENDED) も存在する。
制約:unique_together = (season, variety, type, weight_kg)(Issue #17 Phase 4-C でsales_yearからseasonへ変更。同じ品種・種類・容量でも年度が異なれば登録できる)。
Stock(在庫マスタ)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 品種 | variety |
OneToOne→ProductVariety (CASCADE) | ● | primary_key=True(品種ごとに1レコード) |
| 総供給量(kg) | total_supplied_kg |
Decimal(8,3) | ● | 管理者が設定。既定 0 |
| 総注文量(kg) | total_ordered_kg |
Decimal(8,3) | ● | システムが自動更新。既定 0 |
| 最終更新日時 | updated_at |
DateTime | ● | 自動更新 |
現在在庫量は available_kg プロパティ(total_supplied_kg - total_ordered_kg)として動的計算する(カラムは持たない)。
Phase 4-E以降の扱い:
Stockは品種単位のみで年度をまたぐ累積値になるため、正データとしての役割はSeasonStock(下記)へ移行済み。Stock自体は削除せず、移行結果の比較・監査用として残すが、Phase 4-E以降は更新対象から外れ、実態(SeasonStock)と乖離していく(ロールバック安全網ではない。ロールバックは切替直前のDBバックアップ復元が原則。Issue #17 コメント#3260)。Django管理サイトには引き続き登録されている。
SeasonStock(年度別在庫マスタ、Issue #17 Phase 4-E)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 販売年度 | season |
FK→SalesSeason (PROTECT, related_name=stocks) |
● | |
| 品種 | variety |
FK→ProductVariety (PROTECT, related_name=season_stocks) |
● | Stockと異なりOneToOneではなく通常のFK(品種は複数年度分の行を持つ) |
| 総供給量(kg) | total_supplied_kg |
Decimal(8,3) | ● | 管理者が設定。既定 0 |
| 総注文量(kg) | total_ordered_kg |
Decimal(8,3) | ● | システムが自動更新。既定 0 |
| 最終更新日時 | updated_at |
DateTime | ● | 自動更新 |
現在在庫量はStockと同じくavailable_kgプロパティで動的計算する。制約: unique_together = (season, variety)。新年度の行はget_or_create時に供給量・注文量とも0から開始する(バルク作成のシグナルは持たない)。
購入者向け(商品一覧・カート・注文確認/確定)は常にactive seasonのSeasonStockを参照する。既存注文の変更(数量変更・キャンセル、購入者・管理者とも)は、その注文が属するorder.seasonのSeasonStockを参照する(activeとは限らない)。管理者向け在庫マスタ画面(mg_masters::stock_list)はseason選択UIを持ち、season.status == CLOSEDの行は供給量調整(管理者によるtotal_supplied_kg変更)を拒否する。締め済み年度分の訂正が必要な場合はDjango管理サイト(SeasonStockAdmin)を経由する。既存Stock(品種単位、3件)は移行時点のactive seasonへ1:1複製した(products.0012_populate_season_stock)。
MillingRecord(精米作業記録)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 品種 | variety |
FK→ProductVariety (PROTECT) | ● | |
| 実精米量(kg) | output_seimai_kg |
Decimal(7,3) | ● | |
| 精米日時 | performed_at |
DateTime | ● | 自動設定 |
| メモ | note |
TextField | 任意 |
specs との差分:
MillingRecordは04に未記載(ER_DIAGRAMのみ記載)。品種ごとの精米実績を記録する。
Order(注文ヘッダー) — 1 注文=1 お届け先
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 購入者 | user |
FK→User (PROTECT) | ● | |
| 販売年度 | season |
FK→SalesSeason (PROTECT, related_name=orders) |
● | Phase 4-D。作成後変更不可。全明細のProduct.seasonと一致させる |
| お届け先 | destination |
FK→Destination (SET_NULL) | nullable。お届け先削除時は NULL に |
|
| 注文ステータス | status |
CharField(20) | ● | NEW/ACCEPTED/PACKED/SHIPPED/DELIVERED/CANCELED |
| 入金ステータス | payment_status |
CharField(20) | ● | UNPAID/PAID |
| 参考送料(円) | provisional_shipping_fee |
IntegerField | ● | 注文時シミュレーション値 |
| 確定送料(円) | final_shipping_fee |
IntegerField | nullable。NULL は未確定 |
|
| 作り直し元注文 | recreated_from |
FK→Order (SET_NULL) | キャンセルして作り直した新注文から元注文を参照 | |
| 注文日時 | created_at |
DateTime | ● | 自動設定 |
specs との差分:
destinationは04では必須(●)扱いだが、実装はon_delete=SET_NULL, null=True, blank=True。お届け先の論理削除(is_deleted)と整合する設計。主なプロパティ(実装):
shipping_fee(確定があればそれ、なければ参考値)、is_shipping_fee_finalized、items_subtotal、total_price、is_cancelable/is_editable/is_acceptable(ステータス判定)、is_paid、packages(引当経由で関連パッケージを取得)、get_total_weight_in_genmai()(玄米換算重量)。
OrderItem(注文明細)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 注文 | order |
FK→Order (CASCADE, related_name=items) |
● | |
| 商品 | product |
FK→Product (PROTECT) | ● | |
| 注文時単価(円) | price_at_order |
IntegerField | ● | 注文確定時点の単価 |
| 数量 | quantity |
PositiveInteger | ● |
OrderItem.save()はproduct.season_id == order.season_idを検証し、年度が異なる明細を拒否する。注文作成・数量増加・商品追加の各サービス経路でもvalidate_items_single_season()を呼び、保存前とモデル保存時の二重で年度混在を防止する。
Bag(物理的な袋)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 商品 | product |
FK→Product (PROTECT) | ● | |
| 袋詰済みフラグ | prepared |
BooleanField | ● | 既定 False |
| 作成/更新日時 | created_at/updated_at |
DateTime | ● |
補助プロパティ: is_unprepared / is_allocated(allocation の有無)/ is_packaged(packageitem の有無)。
Allocation(引当) — 注文明細と袋の紐付け
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 注文明細 | order_item |
FK→OrderItem (CASCADE, related_name=allocations) |
● | |
| 袋 | bag |
OneToOne→Bag (CASCADE) | ● | 1袋は1注文明細にのみ引当 |
| 引当日時 | created_at |
DateTime | ● |
Package(荷物/箱)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| お届け先 | destination |
FK→Destination (PROTECT) | ● | |
| 販売年度 | season |
FK→SalesSeason (PROTECT) | ● | Issue #17 Phase 4-F。箱作成時点で確定し、以降変更する経路はない。空箱の段階から所属seasonが決まる |
| パッケージステータス | status |
CharField(20) | ● | PACKAGING/READY_TO_SHIP/LABEL_PRINTED/SHIPPED/DELIVERED |
| ロック状態 | lock_status |
CharField(20) | ● | UNLOCKED/APPEND_ONLY/LOCKED。既定 UNLOCKED |
| 箱ラベル | label |
CharField(50) | nullable | |
| 伝票番号 | tracking_number |
CharField(50) | CSV インポートで設定 | |
| CSV出力日時 | csv_exported_at |
DateTime | nullable | |
| 作成/更新日時 | created_at/updated_at |
DateTime | ● | ordering = ['-created_at'] |
tracking_url プロパティ: delivery_method == 'YAMATO' かつ伝票番号がある場合、ヤマト運輸の荷物追跡 URL を生成。
PackagePlanItem.product・PackageItem.bag.productのseasonがPackage.seasonと一致することは、サービス層(mg_workflow/api_packaging.py)のガードで保証する(DBレイヤーではテーブルを跨るCHECK制約を持てないため)。既存98件の移行(orders.0018_populate_package_season)は、中身(PackageItem/PackagePlanItem)の商品seasonから機械的に導出してバックフィルする。season混在箱は自動判定せず移行を中断する一方、空箱(中身が一切ない箱)は出荷対象を何も持たず季節を割り当てる意味のある情報が存在しないため削除する(伝票番号・CSV出力履歴の有無を問わない)。
PackageItem(パッケージ明細)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| パッケージ | package |
FK→Package (CASCADE, related_name=items) |
● | |
| 袋 | bag |
OneToOne→Bag (PROTECT) | ● | 1袋は1箱にのみ格納 |
注意:
Allocation.bagは CASCADE、PackageItem.bagは PROTECT とon_deleteが異なる。
PackagePlanItem(パッケージ計画/予約) — 「袋在庫はないがこの箱に入れる予定」の商品
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| パッケージ | package |
FK→Package (CASCADE, related_name=plan_items) |
● | |
| 商品 | product |
FK→Product (PROTECT) | ● | |
| 予約数量 | quantity |
PositiveInteger | ● | 既定 0 |
| 作成/更新日時 | created_at/updated_at |
DateTime | ● |
制約: unique_together = (package, product)。
PaymentAdjustment(入金処理調整) — 入金済み注文をキャンセルした後のお金の扱い
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 対象注文 | order |
FK→Order (PROTECT, related_name=payment_adjustments) |
● | キャンセルされた入金済み注文 |
| 顧客 | user |
FK→User (PROTECT) | ● | |
| 処理種別 | action |
CharField(20) | ● | REFUND(返金)/ CARRY_OVER(次回支払いへ繰越)/ DONATION(寄付扱い) |
| 対象金額 | amount |
IntegerField | ● | 円 |
| 管理者メモ | note |
TextField | ||
| 記録日時 | created_at |
DateTime | ● | 自動設定 |
| 実処理日時 | handled_at |
DateTime | 返金などの実処理完了時刻。初期実装では任意 |
CustomerCreditTransaction(繰越残高履歴) — 顧客別の預かり金・繰越残高の台帳
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 顧客 | user |
FK→User (PROTECT, related_name=credit_transactions) |
● | |
| 種別 | transaction_type |
CharField(40) | ● | 下記 |
| 金額 | amount |
IntegerField | ● | 残高加算は正、注文への充当は負 |
| 繰越発生元注文 | source_order |
FK→Order (PROTECT, related_name=credit_carry_overs) |
入金済みキャンセルで繰越が発生した注文 | |
| 充当先注文 | target_order |
FK→Order (PROTECT, related_name=credit_applications) |
繰越を充当した注文、または充当戻し対象注文 | |
| 繰越元年度 | source_season |
FK→SalesSeason (PROTECT) | Issue #17 Phase 4-G。source_orderがある行ではsource_order.seasonと一致必須(サービス層+モデルsave()で検証)。取引種別により恒久的にnullable |
|
| 充当先年度 | target_season |
FK→SalesSeason (PROTECT) | Issue #17 Phase 4-G。target_orderがある行ではtarget_order.seasonと一致必須。取引種別により恒久的にnullable |
|
| 記録時点の年度 | recorded_in_season |
FK→SalesSeason (PROTECT) | ● | Issue #17 Phase 4-G。生成時点のSystemSetting.load().active_seasonを記録する汎用スタンプ |
| 取消対象の充当取引 | reversal_of |
OneToOneField→self (PROTECT) | Issue #17 Phase 4-G。RETURNED_FROM_CANCELED_APPLIED_ORDER行が取消対象の特定のAPPLIED_TO_ORDER行を直接参照する。OneToOneFieldにより同一行への二重取消をDB制約で防止する |
|
| 対応する入金処理調整 | payment_adjustment |
FK→PaymentAdjustment (PROTECT, related_name=credit_transactions) |
CARRY_OVER 記録とのリンク |
|
| 管理者メモ | note |
TextField | ||
| 操作管理者 | created_by |
FK→User (SET_NULL, related_name=created_credit_transactions) |
移行・自動反映分は NULL |
|
| 記録日時 | created_at |
DateTime | ● | 自動設定 |
transaction_type:
| 値 | 意味 | 符号 |
|---|---|---|
CARRY_OVER_FROM_CANCELED_ORDER |
キャンセル注文からの繰越加算 | 正 |
APPLIED_TO_ORDER |
注文への充当 | 負 |
RETURNED_FROM_CANCELED_APPLIED_ORDER |
充当先キャンセルによる戻し | 正 |
REFUNDED_FROM_CREDIT |
繰越残高からの返金(将来用) | 負 |
ADJUSTMENT |
管理者調整(将来用) | 正または負 |
残高は amount の合計で算出する。注文単位の有効な繰越充当額は、target_order に紐づく APPLIED_TO_ORDER と RETURNED_FROM_CANCELED_APPLIED_ORDER を合算し、符号反転して求める。
取引種別ごとのseasonフィールド設定(Issue #17 Phase 4-G):
transaction_type |
source_season |
target_season |
reversal_of |
|---|---|---|---|
CARRY_OVER_FROM_CANCELED_ORDER |
必須。source_order.seasonと一致 |
null | null |
APPLIED_TO_ORDER |
null(残高プールは特定年度に紐付かないfungibleな預かり金) | 必須。target_order.seasonと一致 |
null |
RETURNED_FROM_CANCELED_APPLIED_ORDER |
null | 必須。target_order.seasonと一致 |
必須。取消対象のAPPLIED_TO_ORDER行 |
ADJUSTMENT(将来用) |
設定したsource_orderがあればそのseasonと一致必須 |
設定したtarget_orderがあればそのseasonと一致必須 |
null |
REFUNDED_FROM_CREDIT(将来用) |
null | null | null |
いずれの行もrecorded_in_seasonは必須(生成時点のactive season)。
CreditApplicationAllocation(繰越配賦明細、Issue #17 Phase 4-G) — 消費側取引(APPLIED_TO_ORDER等)が、どの供給側取引(CARRY_OVER_FROM_CANCELED_ORDER・RETURNED_FROM_CANCELED_APPLIED_ORDER等、amount > 0の行)からいくら配賦されたかを表す明細
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 充当・払出取引 | consuming_transaction |
FK→CustomerCreditTransaction (PROTECT, related_name=allocations_consumed) |
● | |
| 元残高取引 | source_transaction |
FK→CustomerCreditTransaction (PROTECT, related_name=allocations_supplied) |
● | |
| 配賦額 | amount |
PositiveIntegerField | ● | |
| 作成日時 | created_at |
DateTime | ● | 自動設定 |
制約: unique_together = (consuming_transaction, source_transaction)。配賦順序: apply_credit_to_orderが充当額を確定する際、その顧客のamount > 0の取引のうち未配賦残額(amount - Sum(その行へのallocation.amount))が残っているものをcreated_at昇順(FIFO)で取得し、必要額に達するまで順に配賦する。1回の充当が複数の元取引にまたがる場合、その分だけ本モデルの行を複数作成する。キャンセルによる戻し: RETURNED_FROM_CANCELED_APPLIED_ORDERは取り消された充当の配賦明細を遡って変更せず、それ自体を新しい供給側取引(戻し時点をcreated_atとする新たなFIFOソース)として扱う(台帳を追記のみに保つ設計方針)。既存データ: Phase 4-G導入時点でCustomerCreditTransactionは0件のため、本モデルのバックフィルは行っていない。
EmptyBagStock(空袋在庫)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 袋の重量(kg) | weight_kg |
Decimal(5,2) | ● | unique(重量ごとに1レコード) |
| 在庫数 | quantity |
PositiveInteger | ● | 既定 0 |
| 最終更新日時 | updated_at |
DateTime | ● |
specs との差分:
EmptyBagStockは04に未記載(ER_DIAGRAMのみ)。梱包に使う空袋の在庫を重量別に管理する。
SystemSetting(システム設定) — Singleton(pk=1 固定、delete() 無効化、load() で取得)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 店舗・屋号名 | shop_name |
CharField(100) | 請求書の依頼主名 | |
| 郵便番号 | shop_postal_code |
CharField(10) | ||
| 住所 | shop_address |
CharField(255) | ||
| 電話番号 | shop_phone |
CharField(20) | ||
| B2ご請求先顧客コード | b2_customer_code |
CharField(50) | ヤマト B2 用 CSV に出力。顧客コード+運賃管理番号 | |
| アクティブ年度 | active_season |
FK→SalesSeason(PROTECT) | ● | Issue #17 Phase 3a で active_sales_year(int)として追加、Phase 4-B で SalesSeason へのFKへ型変更。年度の実体データは持たずSalesSeasonへの参照のみを持つ(「アクティブな年度はどれか」を表す唯一の正データ。SalesSeason.status側にACTIVE相当の値は持たせない)。商品マスタの年度デフォルト値計算(Phase 3a)に加え、購入者向け(products/cart/orders)の購入可否検証にも使用(Phase 3b)。恒久的な default は持たず、SystemSetting.load() が新規レコード作成時のみ現在年に対応するSalesSeason(OPEN、無ければ新規作成)を設定する。年度締め実行時(管理者向け補助機能 §2.2)に closing_year から next_year のSalesSeasonへ切り替わる |
Phase 4確定方針(一部実装済み):
SalesSeasonはPhase 4-A、SystemSetting.active_seasonはPhase 4-B、Product.seasonはPhase 4-C、Order.seasonと年度混在防止はPhase 4-Dで実装済み。以下のSeasonStock、Package.seasonと送料確定のseason単位化、顧客残高の年度監査、締め済み年度ガードはPhase 4-E以降で実装する。詳細は 検討用/17_2025年のデータを閉めて2026年の受注業務に備える_実装案 §Phase 4詳細設計 を参照。
BankAccount(振込先口座)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| システム設定 | setting |
FK→SystemSetting (CASCADE, related_name=bank_accounts) |
● | |
| 見出し | title |
CharField(100) | ● | 例「郵便振替」「銀行振込」 |
| 詳細 | details |
TextField | ● | 請求書表示用の口座情報 |
| 表示順 | order |
PositiveInteger | ● | 昇順表示。既定 0 |
year_end と同じ配置方針(プレフィックスなし、モデルのみを所有するアプリ)。専用URL・専用画面は持たない。
SalesSeason(販売年度)
| 論理名 | 物理名 | 型 | 必須 | 説明 |
|---|---|---|---|---|
| 年度 | year |
PositiveIntegerField | ● | unique |
| 状態 | status |
CharField(20) choices | ● | OPEN(未締め)/ CLOSED(締め済み)の2値のみ。既定 OPEN。ACTIVE相当の値は持たない |
| 締め日時 | closed_at |
DateTimeField | ||
| 締め実行者 | closed_by |
FK→User (PROTECT) | ||
| 作成日時 | created_at |
DateTimeField (auto_now_add) | ● | |
| 更新日時 | updated_at |
DateTimeField (auto_now) | ● |
SalesSeason自身では表さない。正データはSystemSetting.active_season(Phase 4-B で導入済み。2.4節)のみ。OPEN → CLOSED の前方のみ。締め取消・再開は提供しない(Issue #17 コメント#3262で確定)。sales_seasons/migrations/0002_populate_sales_seasons.py): Product.sales_yearの distinct 値とSystemSetting.active_sales_year(Phase 4-B でactive_seasonへ型変更する前のint値)の和集合を対象年度とし、当時のactive_sales_year未満の年度はCLOSED、以上の年度はOPENとして作成する。SystemSettingが存在しないのにProduct.sales_yearが存在する(想定外の状態)場合は黙って補正せず例外を送出して移行を中断する。逆方向は作成したSalesSeasonを全件削除する片方向のデータ移行(Phase 4-A適用時点では他モデルから未参照だったため、全件削除がそのまま巻き戻しになる設計だった。Phase 4-B以降はSystemSetting.active_seasonが、Phase 4-C以降はProduct.seasonもPROTECTで参照するため、実際にロールバックする場合はsystem_settings・products側のマイグレーションも合わせて逆方向へ戻す必要がある)。Product.seasonの既存データバックフィル(products/migrations/0009_populate_product_season.py、Issue #17 Phase 4-C): 旧sales_yearのdistinct値ごとに対応するSalesSeason(year=sales_year)を検索してseasonへ一括割当する。対応するSalesSeasonが見つからない(想定外の状態)場合はProductSeasonBackfillErrorを送出して移行を中断する。逆方向はseason.yearからsales_yearを復元してからseasonをnullへ戻す片方向のデータ移行(0008でsales_yearをnullable化済みのため、逆適用の列復元がNOT NULL制約で失敗しない)。Product: (season, variety, type, weight_kg) が unique(Issue #17 Phase 3a で sales_year を追加、Phase 4-C で season へ変更)Stock: variety が PK(品種ごとに 1 レコード)SeasonStock: (season, variety) が unique(Issue #17 Phase 4-E)Allocation.bag / PackageItem.bag: OneToOne(1 袋は 1 注文明細・1 箱にのみ)PackagePlanItem: (package, product) が uniqueEmptyBagStock.weight_kg: uniqueUser.username: uniqueSalesSeason.year: unique(Issue #17 Phase 4-A)CreditApplicationAllocation: (consuming_transaction, source_transaction) が unique(Issue #17 Phase 4-G)CustomerCreditTransaction.reversal_of: OneToOne(1つのAPPLIED_TO_ORDER行への取消は高々1回。Issue #17 Phase 4-G)on_delete)| 親 | 子 | 挙動 |
|---|---|---|
| User | Destination | CASCADE |
| User | Order | PROTECT(注文を持つ顧客は削除不可) |
| User | PaymentAdjustment / CustomerCreditTransaction | PROTECT(入金処理・繰越台帳を持つ顧客は削除不可) |
| User | SalesSeason (closed_by) |
PROTECT(年度締めを実行したユーザーは削除不可。Issue #17 Phase 4-A) |
| SalesSeason | SystemSetting (active_season) |
PROTECT(アクティブ年度として参照中のSalesSeasonは削除不可。Issue #17 Phase 4-B) |
| SalesSeason | Product (season) |
PROTECT(商品が参照している年度は削除不可。Issue #17 Phase 4-C) |
| SalesSeason | SeasonStock (season) |
PROTECT(在庫行が参照している年度は削除不可。Issue #17 Phase 4-E) |
| SalesSeason | Package (season) |
PROTECT(箱が参照している年度は削除不可。Issue #17 Phase 4-F) |
| SalesSeason | CustomerCreditTransaction (source_season/target_season/recorded_in_season) |
PROTECT(繰越台帳が参照している年度は削除不可。Issue #17 Phase 4-G) |
| CustomerCreditTransaction | CustomerCreditTransaction (reversal_of、自己参照) |
PROTECT(取消対象として参照されている充当取引は削除不可。Issue #17 Phase 4-G) |
| CustomerCreditTransaction | CreditApplicationAllocation | PROTECT(配賦明細から参照されている取引は削除不可。台帳は追記専用のため削除自体が想定されない。Issue #17 Phase 4-G) |
| ProductVariety | Product / Stock | CASCADE |
| ProductVariety | SeasonStock | PROTECT(Stockとon_deleteが異なる。Issue #17 Phase 4-E) |
| ProductVariety | MillingRecord | PROTECT |
| Order | OrderItem | CASCADE |
| Order | PaymentAdjustment / CustomerCreditTransaction | PROTECT(入金処理・繰越台帳から参照される注文は削除不可) |
| OrderItem | Allocation | CASCADE |
| Bag | Allocation | CASCADE |
| Package | PackageItem / PackagePlanItem | CASCADE |
| Product | OrderItem / Bag / PackagePlanItem | PROTECT(使用中は削除不可) |
| Destination | Order | SET_NULL |
| Destination | Package | PROTECT |
| Bag | PackageItem | PROTECT |
| SystemSetting | BankAccount | CASCADE |
商品(
Product)が注文・袋・計画に使われている場合、または品種(ProductVariety)が精米作業記録(MillingRecord)から参照されている場合は PROTECT により削除がブロックされる。これが要件「安全な削除(使用中の品種・商品は削除ボタンを非活性化)」のデータ層での裏付けとなる。
元資料: specs/04_データベース設計書.md (Ver.2.2), specs/ER_DIAGRAM.md を現行コード(accounts/products/orders/system_settings の models.py)と照合して再構成
関連: README, specs棚卸し表, Issue #1