Files
keinasystem/backend/apps/plans/views.py
akira a38472e4a0 品種ごとの種子在庫前提まで実装を進めました。
主な変更は、seed 資材種別の追加と Variety.seed_material の導入です。backend/apps/materials/models.py、backend/apps/plans/models.py、backend/apps/plans/serializers.py で、田植え計画が作物在庫ではなく品種に紐づく種子資材の現在庫を参照するように切り替えました。マイグレーションは backend/apps/materials/migrations/0005_material_seed_type.py と backend/apps/plans/migrations/0008_variety_seed_material.py を追加しています。

画面側は、frontend/src/app/materials/page.tsx と frontend/src/app/materials/masters/page.tsx に「種子」タブを追加し、frontend/src/app/allocation/page.tsx の品種管理モーダルで品種ごとに種子在庫資材を設定できるようにしました。田植え計画画面 frontend/src/app/rice-transplant/_components/RiceTransplantEditPage.tsx も、苗箱数 列中心に整理し、種もみkg 列を削除、反当苗箱枚数 の列反映と ≈ / ↩ の四捨五入トグルを施肥計画寄りの操作感に寄せています。仕様書 document/16_マスタードキュメント_田植え計画編.md も更新済みです。

確認できたのは python3 -m py_compile backend/apps/materials/models.py backend/apps/materials/serializers.py backend/apps/plans/models.py backend/apps/plans/serializers.py backend/apps/plans/views.py までです。frontend/node_modules が無いためフロントのビルド確認はまだできていません。Issue #2 にも反映内容をコメント済みです。必要なら次にコミットします。
2026-04-05 11:22:07 +09:00

190 lines
6.4 KiB
Python

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, RiceTransplantPlan
from .serializers import (
CropSerializer,
VarietySerializer,
PlanSerializer,
RiceTransplantPlanSerializer,
RiceTransplantPlanWriteSerializer,
)
from apps.fields.models import Field
class CropViewSet(viewsets.ModelViewSet):
queryset = Crop.objects.all()
serializer_class = CropSerializer
class VarietyViewSet(viewsets.ModelViewSet):
queryset = Variety.objects.select_related('seed_material', 'crop').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)
total_fields = Field.objects.count()
assigned_field_ids = plans.values_list('field_id', flat=True).distinct()
assigned_count = assigned_field_ids.count()
unassigned_count = total_fields - assigned_count
return Response({
'year': int(year),
'total_fields': total_fields,
'assigned_fields': assigned_count,
'unassigned_fields': unassigned_count,
'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=['post'])
def bulk_update(self, request):
"""複数圃場の作付け計画を一括更新"""
field_ids = request.data.get('field_ids', [])
year = request.data.get('year')
crop_id = request.data.get('crop')
variety_id = request.data.get('variety')
if not field_ids or not year or not crop_id:
return Response({'error': 'field_ids, year, crop are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
crop = Crop.objects.get(id=crop_id)
except Crop.DoesNotExist:
return Response({'error': 'Crop not found'}, status=status.HTTP_400_BAD_REQUEST)
variety = None
if variety_id:
try:
variety = Variety.objects.get(id=variety_id)
except Variety.DoesNotExist:
pass
updated = 0
created = 0
for field_id in field_ids:
plan, was_created = Plan.objects.update_or_create(
field_id=field_id,
year=year,
defaults={'crop': crop, 'variety': variety}
)
if was_created:
created += 1
else:
updated += 1
return Response({'created': created, 'updated': updated, 'total': created + updated})
@action(detail=False, methods=['get'])
def get_crops_with_varieties(self, request):
crops = Crop.objects.prefetch_related('varieties__seed_material').all()
return Response(CropSerializer(crops, many=True).data)
class RiceTransplantPlanViewSet(viewsets.ModelViewSet):
queryset = RiceTransplantPlan.objects.select_related(
'variety',
'variety__crop',
'variety__seed_material',
).prefetch_related(
'variety__seed_material__stock_transactions',
'entries',
'entries__field',
)
def get_queryset(self):
queryset = self.queryset
year = self.request.query_params.get('year')
if year:
queryset = queryset.filter(year=year)
return queryset
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return RiceTransplantPlanWriteSerializer
return RiceTransplantPlanSerializer
@action(detail=False, methods=['get'])
def candidate_fields(self, request):
year = request.query_params.get('year')
variety_id = request.query_params.get('variety_id')
if not year or not variety_id:
return Response(
{'error': 'year と variety_id が必要です'},
status=status.HTTP_400_BAD_REQUEST,
)
field_ids = Plan.objects.filter(
year=year,
variety_id=variety_id,
).values_list('field_id', flat=True)
fields = Field.objects.filter(id__in=field_ids).order_by('display_order', 'id')
data = [
{
'id': field.id,
'name': field.name,
'area_tan': str(field.area_tan),
'area_m2': field.area_m2,
'group_name': field.group_name,
}
for field in fields
]
return Response(data)