from django.db import transaction from decimal import Decimal from rest_framework import serializers from apps.plans.models import Plan from apps.workrecords.services import sync_levee_work_record from .models import LeveeWorkSession, LeveeWorkSessionItem class LeveeWorkSessionItemReadSerializer(serializers.ModelSerializer): field_name = serializers.CharField(source='field.name', read_only=True) field_area_tan = serializers.DecimalField( source='field.area_tan', max_digits=6, decimal_places=4, read_only=True, ) group_name = serializers.CharField(source='field.group_name', read_only=True, allow_null=True) class Meta: model = LeveeWorkSessionItem fields = [ 'id', 'field', 'field_name', 'field_area_tan', 'group_name', 'plan', 'crop_name_snapshot', 'variety_name_snapshot', ] class LeveeWorkSessionSerializer(serializers.ModelSerializer): items = LeveeWorkSessionItemReadSerializer(many=True, read_only=True) work_record_id = serializers.IntegerField(source='work_record.id', read_only=True) item_count = serializers.SerializerMethodField() total_area_tan = serializers.SerializerMethodField() class Meta: model = LeveeWorkSession fields = [ 'id', 'year', 'date', 'title', 'notes', 'work_record_id', 'item_count', 'total_area_tan', 'items', 'created_at', 'updated_at', ] def get_item_count(self, obj): return len(obj.items.all()) def get_total_area_tan(self, obj): total = sum((item.field.area_tan or Decimal('0')) for item in obj.items.all()) return str(total) class LeveeWorkSessionItemWriteInputSerializer(serializers.Serializer): field = serializers.IntegerField() plan = serializers.IntegerField(required=False, allow_null=True) class LeveeWorkSessionWriteSerializer(serializers.ModelSerializer): items = LeveeWorkSessionItemWriteInputSerializer(many=True, write_only=True) class Meta: model = LeveeWorkSession fields = ['id', 'year', 'date', 'title', 'notes', 'items'] def validate(self, attrs): year = attrs.get('year', getattr(self.instance, 'year', None)) date = attrs.get('date', getattr(self.instance, 'date', None)) if year is not None and date is not None and year != date.year: raise serializers.ValidationError({'year': 'year は date.year と一致させてください。'}) return attrs def validate_items(self, value): if not value: raise serializers.ValidationError('items を1件以上指定してください。') seen = set() for item in value: key = item['field'] if key in seen: raise serializers.ValidationError('同一 session 内で同じ圃場を重複登録できません。') seen.add(key) return value @transaction.atomic def create(self, validated_data): items_data = validated_data.pop('items', []) validated_data['title'] = (validated_data.get('title') or '').strip() or '水稲畔塗' session = LeveeWorkSession.objects.create(**validated_data) self._replace_items(session, items_data) sync_levee_work_record(session) return session @transaction.atomic def update(self, instance, validated_data): items_data = validated_data.pop('items', None) for attr, value in validated_data.items(): if attr == 'title': value = (value or '').strip() or '水稲畔塗' setattr(instance, attr, value) if 'title' not in validated_data: instance.title = (instance.title or '').strip() or '水稲畔塗' instance.save() if items_data is not None: self._replace_items(instance, items_data) sync_levee_work_record(instance) return instance def _replace_items(self, session, items_data): session.items.all().delete() for item in items_data: plan = self._resolve_plan(session.year, item['field'], item.get('plan')) LeveeWorkSessionItem.objects.create( session=session, field_id=item['field'], plan=plan, crop_name_snapshot=plan.crop.name, variety_name_snapshot=plan.variety.name if plan.variety else '', ) def _resolve_plan(self, year, field_id, plan_id): queryset = Plan.objects.select_related('crop', 'variety').filter( year=year, field_id=field_id, crop__name='水稲', ) if plan_id is not None: try: return queryset.get(id=plan_id) except Plan.DoesNotExist as exc: raise serializers.ValidationError( {'items': f'field={field_id} に対応する水稲作付け計画(plan={plan_id})が見つかりません。'} ) from exc plan = queryset.first() if plan is None: raise serializers.ValidationError( {'items': f'field={field_id} は当年の水稲作付け圃場ではありません。'} ) return plan