Day 5 の作付け計画API実装が完了しました。

実装内容
バグ修正
- fields/views.py: OfficialChusakanField → OfficialChusankanField
init_crops コマンド
 python manage.py init_crops
水稲: 5 varieties
大豆: 3 varieties
小麦: 2 varieties
そば: 2 varieties
とうきび: 1 varieties
serializers.py
- CropSerializer - 作物マスタ
- VarietySerializer - 品種マスタ
- PlanSerializer - 作付け計画(crop_name, variety_name, field_name 付き)
views.py
- CropViewSet, VarietyViewSet, PlanViewSet
- アクション: summary, copy_from_previous_year, get_crops_with_varieties
API エンドポイント
- /api/plans/crops/ - 作物一覧
- /api/plans/varieties/ - 品種一覧
- /api/plans/ - 作付け計画CRUD
- /api/plans/summary/?year=2025 - 集計
テスト結果
GET /api/plans/crops/ → 
GET /api/plans/ →  (空配列)
This commit is contained in:
Akira
2026-02-15 12:10:38 +09:00
parent d30125d0a1
commit 543de30b1c
9 changed files with 174 additions and 4 deletions

View File

@@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand
from apps.plans.models import Crop, Variety
class Command(BaseCommand):
help = 'Initialize crops and varieties master data'
def handle(self, *args, **options):
crops_data = [
{
'name': '水稲',
'varieties': ['コシヒカリ', 'ひとめぼれ', 'あきたこまち', 'つや姫', 'oniai']
},
{
'name': '大豆',
'varieties': ['タマホマレ', 'エンレイ', 'ミヤギром']
},
{
'name': '小麦',
'varieties': ['キタノカオリ', 'ホウライ']
},
{
'name': 'そば',
'varieties': ['信濃一号', 'はるか']
},
{
'name': 'とうきび',
'varieties': ['ゴールdent']
},
]
for crop_data in crops_data:
crop, _ = Crop.objects.get_or_create(name=crop_data['name'])
for variety_name in crop_data['varieties']:
Variety.objects.get_or_create(crop=crop, name=variety_name)
self.stdout.write(f'{crop.name}: {len(crop_data["varieties"])} varieties')
self.stdout.write(self.style.SUCCESS('Successfully initialized crops and varieties'))

View File

@@ -0,0 +1,36 @@
from rest_framework import serializers
from .models import Crop, Variety, Plan
class VarietySerializer(serializers.ModelSerializer):
class Meta:
model = Variety
fields = '__all__'
class CropSerializer(serializers.ModelSerializer):
varieties = VarietySerializer(many=True, read_only=True)
class Meta:
model = Crop
fields = '__all__'
class PlanSerializer(serializers.ModelSerializer):
crop_name = serializers.ReadOnlyField(source='crop.name')
variety_name = serializers.ReadOnlyField(source='variety.name')
field_name = serializers.ReadOnlyField(source='field.name')
class Meta:
model = Plan
fields = '__all__'
read_only_fields = ('id', 'created_at', 'updated_at')
def create(self, validated_data):
return Plan.objects.create(**validated_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance

View File

@@ -0,0 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'crops', views.CropViewSet)
router.register(r'varieties', views.VarietyViewSet)
router.register(r'', views.PlanViewSet)
urlpatterns = [
path('', include(router.urls)),
path('get-crops-with-varieties/', views.PlanViewSet.as_view({'get': 'get_crops_with_varieties'}), name='get_crops_with_varieties'),
]

View File

@@ -1,3 +1,85 @@
from django.shortcuts import render
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Sum
from .models import Crop, Variety, Plan
from .serializers import CropSerializer, VarietySerializer, PlanSerializer
# Create your views here.
class CropViewSet(viewsets.ModelViewSet):
queryset = Crop.objects.all()
serializer_class = CropSerializer
class VarietyViewSet(viewsets.ModelViewSet):
queryset = Variety.objects.all()
serializer_class = VarietySerializer
class PlanViewSet(viewsets.ModelViewSet):
queryset = Plan.objects.all()
serializer_class = PlanSerializer
def get_queryset(self):
queryset = Plan.objects.all()
year = self.request.query_params.get('year')
if year:
queryset = queryset.filter(year=year)
return queryset
@action(detail=False, methods=['get'])
def summary(self, request):
year = request.query_params.get('year')
if not year:
return Response({'error': 'year parameter is required'}, status=status.HTTP_400_BAD_REQUEST)
plans = Plan.objects.filter(year=year)
total_area = plans.aggregate(total=Sum('field__area_tan'))['total'] or 0
by_crop = {}
for plan in plans:
crop_name = plan.crop.name
if crop_name not in by_crop:
by_crop[crop_name] = {
'crop': crop_name,
'count': 0,
'area': 0
}
by_crop[crop_name]['count'] += 1
by_crop[crop_name]['area'] += float(plan.field.area_tan)
return Response({
'year': int(year),
'total_plans': plans.count(),
'total_area': float(total_area),
'by_crop': list(by_crop.values())
})
@action(detail=False, methods=['post'])
def copy_from_previous_year(self, request):
from_year = request.data.get('from_year')
to_year = request.data.get('to_year')
if not from_year or not to_year:
return Response({'error': 'from_year and to_year are required'}, status=status.HTTP_400_BAD_REQUEST)
previous_plans = Plan.objects.filter(year=from_year)
new_plans = []
for plan in previous_plans:
new_plans.append(Plan(
field=plan.field,
year=to_year,
crop=plan.crop,
variety=plan.variety,
notes=plan.notes
))
Plan.objects.bulk_create(new_plans, ignore_conflicts=True)
return Response({'message': f'Copied {len(new_plans)} plans from {from_year} to {to_year}'})
@action(detail=False, methods=['get'])
def get_crops_with_varieties(self, request):
crops = Crop.objects.prefetch_related('varieties').all()
return Response(CropSerializer(crops, many=True).data)