ドキュメント更新(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:
@@ -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: 作付け計画API(Day 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. 集計サイドバー
|
||||
- **リアルタイム更新**: 作付け計画を保存するたびに自動更新
|
||||
|
||||
Reference in New Issue
Block a user