ドキュメント更新(6ファイル)

03_データ仕様書.md — 全面書き直し(M:N関係、中山間17列モデル、面積単位、PDF出力仕様)
04_画面設計書.md — 全面書き直し(Navbar追加、圃場管理/新規作成画面追加、インライン編集方式、PDF帳票フォーマット仕様 E-1)
01_プロダクトビジョン.md — CSV→PDF、M:1→M:N
05_実装優先順位.md — CSV→PDF、Django 5.0→5.2、モーダル→インライン、init_crops削除
00_Gemini向け統合指示書.md — CSV→PDF、Django 5.2、M:N関係、中山間17列モデル、init_crops削除、IsAuthenticated
CLAUDE.md — 既知の課題一覧、次タスク優先順追加、中山間モデル拡張、差異レポートリンク
コード修正(4件)
D-1: reports/views.py — plan.crop / plan.variety の null チェック追加
D-2: init_crops.py を削除
D-3: settings.py — LANGUAGE_CODE/TIME_ZONE の二重定義を解消
D-4: settings.py — AllowAny → IsAuthenticated に変更
次のタスクは CLAUDE.md の優先順リストに従うと A-8(圃場詳細に共済/中山間情報表示)です。続けますか?
This commit is contained in:
Akira
2026-02-17 10:56:09 +09:00
parent 9c21caa017
commit 50e23872f3
11 changed files with 771 additions and 738 deletions

View File

@@ -19,14 +19,14 @@
- PCで登録・編集、スマホで参照
### 主要機能Phase 1 / MVP
1. 作付け計画の一覧表示・編集
2. 水稲共済細目書のCSV出力
3. 中山間交付金申請書のCSV出力
1. 作付け計画の一覧表示・インライン編集
2. 水稲共済細目書のPDF出力
3. 中山間交付金申請書のPDF出力
4. 前年度作付けのコピー機能
5. 圃場情報のスマホ参照
### 技術スタック
- **バックエンド**: Django 5.0 + Django REST Framework + GeoDjango
- **バックエンド**: Django 5.2 + Django REST Framework + GeoDjango
- **フロントエンド**: Next.js 14 (App Router) + Tailwind CSS
- **データベース**: PostgreSQL 16 + PostGIS 3.4
- **インフラ**: Docker Compose
@@ -49,8 +49,8 @@
### 3. データ仕様書.md
- 3種類のデータ実圃場、共済マスタ、中山間マスタの関係
- 紐付けロジックM:1関係)の詳細
- 申請書CSV出力のアルゴリズム
- 紐付けロジックM:N関係)の詳細
- 申請書PDF出力のアルゴリズム
- **👉 読むべき理由**: データモデルの設計ミスは後から修正困難
### 4. 画面設計書.md
@@ -149,7 +149,7 @@ volumes:
#### backend/requirements.txt
```txt
Django==5.0
Django==5.2
djangorestframework==3.14
django-cors-headers==4.3
psycopg2-binary==2.9
@@ -227,25 +227,36 @@ from django.contrib.gis.db import models
class OfficialKyosaiField(models.Model):
"""共済マスタ(水稲共済細目用.ods"""
k_num = models.IntegerField("耕地番号")
s_num = models.IntegerField("分筆番号")
k_num = models.CharField("耕地番号", max_length=20)
s_num = models.CharField("分筆番号", max_length=20, blank=True)
address = models.CharField("地名地番", max_length=200)
kanji_name = models.CharField("漢字地名", max_length=200)
area = models.FloatField("本地面積(m2)")
area = models.IntegerField("本地面積(m2)")
class Meta:
unique_together = [['k_num', 's_num']]
ordering = ['k_num', 's_num']
class OfficialChusankanField(models.Model):
"""中山間マスタ(中山間.ods"""
c_id = models.IntegerField("ID", unique=True)
"""中山間マスタ(中山間.ods- 17列全て保存"""
c_id = models.CharField("ID", max_length=20, unique=True)
chusankan_flag = models.CharField("中山間", max_length=10, blank=True)
oaza = models.CharField("大字", max_length=100)
aza = models.CharField("", max_length=100)
chiban = models.IntegerField("地番")
chiban = models.CharField("地番", max_length=50)
branch_num = models.CharField("枝番", max_length=20, blank=True)
land_type = models.CharField("地目", max_length=20, blank=True)
area = models.IntegerField("農地面積(m2)")
planting_area = models.IntegerField("植栽面積(m2)", null=True, blank=True)
original_crop = models.CharField("作付け品目", max_length=100, blank=True)
manager = models.CharField("協定管理者", max_length=100, blank=True)
owner = models.CharField("所有者", max_length=100, blank=True)
slope = models.CharField("傾斜度", max_length=20, blank=True)
base_amount = models.IntegerField("基本金額", null=True, blank=True)
steep_slope_addition = models.IntegerField("超急傾斜加算額", null=True, blank=True)
smart_agri_addition = models.IntegerField("スマート農業加算額", null=True, blank=True)
payment_amount = models.IntegerField("交付金額", null=True, blank=True)
class Meta:
ordering = ['c_id']
@@ -257,22 +268,19 @@ class Field(models.Model):
area_m2 = models.IntegerField("面積(m2)") # area_tan * 1000
owner_name = models.CharField("地主", max_length=100)
# 紐付けキーraw値
raw_kyosai_k_num = models.IntegerField("細目_耕地番号")
raw_kyosai_s_num = models.IntegerField("細目_分筆番号")
raw_chusankan_id = models.IntegerField("中山間_ID", null=True, blank=True)
# 外部キー(紐付け済み)
kyosai_field = models.ForeignKey(
OfficialKyosaiField,
on_delete=models.SET_NULL,
null=True,
# グループ・表示順
group_name = models.CharField("グループ名", max_length=100, blank=True)
display_order = models.IntegerField("表示順", default=0)
# M:N紐付け1つの圃場が複数の申請区画に紐づく場合がある
kyosai_fields = models.ManyToManyField(
OfficialKyosaiField,
blank=True,
related_name='fields'
)
chusankan_field = models.ForeignKey(
OfficialChusankanField,
on_delete=models.SET_NULL,
null=True,
chusankan_fields = models.ManyToManyField(
OfficialChusankanField,
blank=True,
related_name='fields'
)
@@ -347,97 +355,52 @@ def import_kyosai_master(request):
@api_view(['POST'])
def import_yoshida_fields(request):
"""吉田農地台帳のインポート(紐付け処理含む)"""
"""吉田農地台帳のインポート(M:N紐付け処理含む)"""
file = request.FILES['file']
df = pd.read_excel(file, engine='odf')
for _, row in df.iterrows():
# 共済マスタとの紐付け
try:
kyosai = OfficialKyosaiField.objects.get(
k_num=row['細目_耕地番号'],
s_num=row['細目_分筆番号']
)
except OfficialKyosaiField.DoesNotExist:
kyosai = None
# 中山間マスタとの紐付け
chusankan = None
if pd.notna(row['中山間_ID']):
try:
chusankan = OfficialChusankanField.objects.get(
c_id=int(row['中山間_ID'])
)
except OfficialChusankanField.DoesNotExist:
pass
# 実圃場を作成
Field.objects.update_or_create(
field, created = Field.objects.update_or_create(
name=row['名称'],
defaults={
'address': row['住所'],
'area_tan': row['面積(反)'],
'area_m2': int(row['面積(反)'] * 1000),
'owner_name': row['地主'],
'raw_kyosai_k_num': row['細目_耕地番号'],
'raw_kyosai_s_num': row['細目_分筆番号'],
'raw_chusankan_id': int(row['中山間_ID']) if pd.notna(row['中山間_ID']) else None,
'kyosai_field': kyosai,
'chusankan_field': chusankan,
}
)
# 共済マスタとのM:N紐付け
try:
kyosai = OfficialKyosaiField.objects.get(
k_num=str(row['細目_耕地番号']),
s_num=str(row['細目_分筆番号'])
)
field.kyosai_fields.add(kyosai)
except OfficialKyosaiField.DoesNotExist:
pass
# 中山間マスタとのM:N紐付け
if pd.notna(row.get('中山間_ID')):
try:
chusankan = OfficialChusankanField.objects.get(
c_id=str(int(row['中山間_ID']))
)
field.chusankan_fields.add(chusankan)
except OfficialChusankanField.DoesNotExist:
pass
return Response({'status': 'success', 'imported': len(df)})
```
### Step 5: 作付け計画APIDay 5
#### 作物・品種の初期データ投入
#### 作物・品種マスタについて
**apps/plans/management/commands/init_crops.py**
```python
from django.core.management.base import BaseCommand
from apps.plans.models import Crop, Variety
初期データの自動投入は行わない。作物・品種はDjango管理画面またはUIから手動で登録する運用とする。
class Command(BaseCommand):
help = '作物・品種マスタの初期データを投入'
def handle(self, *args, **options):
# 作物マスタと品種
crops_data = {
'': ['にこまる', 'たちはるか', 'たちはるか(特栽)'],
'トウモロコシ': [],
'エンドウ': ['久留米豊'],
'野菜': [],
'その他': [
'完全休耕',
'緑肥(ヘアリーベッチ)',
'緑肥(レンゲ)',
'景観作物(コスモス)',
'景観作物(ヒマワリ)'
]
}
for crop_name, varieties in crops_data.items():
crop, created = Crop.objects.get_or_create(name=crop_name)
if created:
self.stdout.write(f'作物「{crop_name}」を作成')
for variety_name in varieties:
variety, created = Variety.objects.get_or_create(
crop=crop,
name=variety_name
)
if created:
self.stdout.write(f' 品種「{variety_name}」を追加')
self.stdout.write(self.style.SUCCESS('初期データ投入完了'))
```
**実行:**
```bash
docker-compose exec backend python manage.py init_crops
```
主な作物: 米、トウモロコシ、エンドウ、野菜、その他
#### apps/plans/views.py
```python
@@ -921,23 +884,16 @@ export default function ReportsPage() {
## ⚠️ 重要な実装上の注意点
### 1. データベース設計
- **面積単位**: DB内部は全て `m2` で保存、表示`反` に変換
- **紐付けキー**: `raw_*` フィールドと外部キー `*_field` の両方を持つ
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで
- **面積単位**: DB内部は `area_m2`IntegerFieldで保存、表示`area_tan`DecimalFieldも保持。1反=1000m2
- **紐付け**: Field ↔ OfficialKyosaiField、Field ↔ OfficialChusankanField は **M:N**ManyToManyField
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで`(k_num, s_num)` で共済区画は一意
- **品種マスタ**: `(crop, name)` で一意制約
### 2. 作物・品種の統一
- **すべての作物で品種選択UIは統一**: 作物による操作の違いなし
- **「作付けしない」系も特別扱いしない**: 「その他」という作物に統一
- **品種の追加**: その場で追加可能、データベースに永続化
- **初期データ**:
```
米: にこまる、たちはるか、たちはるか(特栽)
トウモロコシ: (ユーザーが追加)
エンドウ: 久留米豊
野菜: (ユーザーが追加)
その他: 完全休耕、緑肥(ヘアリーベッチ)、緑肥(レンゲ)、景観作物(コスモス)、景観作物(ヒマワリ)
```
- **初期データ**: なし管理画面またはUIから登録する運用
### 3. 集計サイドバー
- **リアルタイム更新**: 作付け計画を保存するたびに自動更新