📚 修正したドキュメント
✅ 02_ユーザーストーリー.md - 作物マスタの定義を統一 ✅ 03_データ仕様書.md - 作物・品種マスタを更新 ✅ 04_画面設計書.md - 集計サイドバーと編集モーダルのUI ✅ 05_実装優先順位.md - Day 5-6に集計API・品種追加APIを追加 ✅ 06_Gemini向け統合指示書.md - コード例を全面更新
This commit is contained in:
@@ -291,18 +291,19 @@ from apps.fields.models import Field
|
||||
class Crop(models.Model):
|
||||
"""作物マスタ"""
|
||||
name = models.CharField("作物名", max_length=50, unique=True)
|
||||
is_planting = models.BooleanField("作付けする", default=True)
|
||||
# is_planting フィールドは削除(「その他」で統一するため)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
class Variety(models.Model):
|
||||
"""品種マスタ"""
|
||||
"""品種マスタ(すべての作物で統一)"""
|
||||
crop = models.ForeignKey(Crop, on_delete=models.CASCADE, related_name='varieties')
|
||||
name = models.CharField("品種名", max_length=100)
|
||||
|
||||
class Meta:
|
||||
ordering = ['crop', 'name']
|
||||
unique_together = [['crop', 'name']] # 同じ作物内で品種名は一意
|
||||
|
||||
class Plan(models.Model):
|
||||
"""作付け計画"""
|
||||
@@ -391,13 +392,60 @@ def import_yoshida_fields(request):
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.decorators import action, api_view
|
||||
from rest_framework.response import Response
|
||||
from .models import Plan
|
||||
from .serializers import PlanSerializer
|
||||
from .models import Plan, Crop, Variety
|
||||
from .serializers import PlanSerializer, CropSerializer, VarietySerializer
|
||||
|
||||
class PlanViewSet(viewsets.ModelViewSet):
|
||||
queryset = Plan.objects.all()
|
||||
@@ -450,6 +498,79 @@ class PlanViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
return Response({'status': 'success', 'copied': len(plans)})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def summary(self, request):
|
||||
"""集計(サイドバー用)"""
|
||||
year = int(request.query_params.get('year'))
|
||||
plans = Plan.objects.filter(year=year).select_related('crop', 'variety', 'field')
|
||||
|
||||
# 作物別集計
|
||||
crop_summary = {}
|
||||
total_area = 0
|
||||
|
||||
for plan in plans:
|
||||
crop_name = plan.crop.name if plan.crop else '未設定'
|
||||
variety_name = plan.variety.name if plan.variety else '(品種なし)'
|
||||
area = plan.field.area_tan
|
||||
total_area += area
|
||||
|
||||
if crop_name not in crop_summary:
|
||||
crop_summary[crop_name] = {'total': 0, 'varieties': {}}
|
||||
|
||||
crop_summary[crop_name]['total'] += area
|
||||
|
||||
if variety_name not in crop_summary[crop_name]['varieties']:
|
||||
crop_summary[crop_name]['varieties'][variety_name] = 0
|
||||
|
||||
crop_summary[crop_name]['varieties'][variety_name] += area
|
||||
|
||||
# 反を小数点1桁に丸める
|
||||
for crop in crop_summary.values():
|
||||
crop['total'] = round(crop['total'], 1)
|
||||
for variety in crop['varieties']:
|
||||
crop['varieties'][variety] = round(crop['varieties'][variety], 1)
|
||||
|
||||
return Response({
|
||||
'total_area': round(total_area, 1),
|
||||
'crops': crop_summary
|
||||
})
|
||||
|
||||
@api_view(['POST'])
|
||||
def add_variety(request):
|
||||
"""新しい品種を追加"""
|
||||
crop_id = request.data.get('crop_id')
|
||||
name = request.data.get('name')
|
||||
|
||||
# 重複チェック
|
||||
if Variety.objects.filter(crop_id=crop_id, name=name).exists():
|
||||
return Response({'error': 'この品種は既に登録されています'}, status=400)
|
||||
|
||||
variety = Variety.objects.create(crop_id=crop_id, name=name)
|
||||
|
||||
return Response({
|
||||
'id': variety.id,
|
||||
'name': variety.name,
|
||||
'crop_id': variety.crop_id
|
||||
})
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_crops_with_varieties(request):
|
||||
"""作物と品種の一覧を取得"""
|
||||
crops = Crop.objects.prefetch_related('varieties').all()
|
||||
|
||||
data = []
|
||||
for crop in crops:
|
||||
data.append({
|
||||
'id': crop.id,
|
||||
'name': crop.name,
|
||||
'varieties': [
|
||||
{'id': v.id, 'name': v.name}
|
||||
for v in crop.varieties.all()
|
||||
]
|
||||
})
|
||||
|
||||
return Response(data)
|
||||
```
|
||||
|
||||
### Step 6: 申請書PDF生成(Day 7)
|
||||
@@ -803,22 +924,43 @@ export default function ReportsPage() {
|
||||
- **面積単位**: DB内部は全て `m2` で保存、表示時に `反` に変換
|
||||
- **紐付けキー**: `raw_*` フィールドと外部キー `*_field` の両方を持つ
|
||||
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで
|
||||
- **品種マスタ**: `(crop, name)` で一意制約
|
||||
|
||||
### 2. 申請書CSV生成
|
||||
### 2. 作物・品種の統一
|
||||
- **すべての作物で品種選択UIは統一**: 作物による操作の違いなし
|
||||
- **「作付けしない」系も特別扱いしない**: 「その他」という作物に統一
|
||||
- **品種の追加**: その場で追加可能、データベースに永続化
|
||||
- **初期データ**:
|
||||
```
|
||||
米: にこまる、たちはるか、たちはるか(特栽)
|
||||
トウモロコシ: (ユーザーが追加)
|
||||
エンドウ: 久留米豊
|
||||
野菜: (ユーザーが追加)
|
||||
その他: 完全休耕、緑肥(ヘアリーベッチ)、緑肥(レンゲ)、景観作物(コスモス)、景観作物(ヒマワリ)
|
||||
```
|
||||
|
||||
### 3. 集計サイドバー
|
||||
- **リアルタイム更新**: 作付け計画を保存するたびに自動更新
|
||||
- **PC**: 開閉可能なサイドバー(幅200px固定)
|
||||
- **スマホ**: モーダル表示
|
||||
- **集計API**: `/api/plans/summary/?year=2025` で作物別・品種別の面積を返す
|
||||
|
||||
### 4. 申請書PDF生成
|
||||
- **共済マスタをベースにループ**: 実圃場ベースではない
|
||||
- **作物の名寄せ**: Pythonのセットで重複排除
|
||||
- **未割当の扱い**: 「未設定」として出力
|
||||
|
||||
### 3. UI/UX
|
||||
### 5. UI/UX
|
||||
- **未割当の強調**: 赤または黄色の背景色
|
||||
- **スマホ対応**: 文字サイズ16px以上、タップ領域44px以上
|
||||
- **検索のリアルタイム性**: `useState` + `filter` で実装
|
||||
- **品種追加のUI**: インライン入力、別画面に遷移しない
|
||||
|
||||
### 4. 認証
|
||||
### 6. 認証
|
||||
- **JWT認証**: アクセストークン(1日)+ リフレッシュトークン(7日)
|
||||
- **シンプルな実装**: パスワードリセットはPhase 2
|
||||
|
||||
### 5. パフォーマンス
|
||||
### 7. パフォーマンス
|
||||
- **Phase 1では最適化不要**: 圃場数39筆、共済31区画、中山間71区画 → 十分軽い
|
||||
- **Phase 2で検討**: 栽培履歴が増えたらページネーション
|
||||
|
||||
|
||||
Reference in New Issue
Block a user