from decimal import Decimal from django.db.models import Sum from rest_framework import serializers from apps.workrecords.services import sync_delivery_work_record from .models import ( DeliveryGroup, DeliveryGroupField, DeliveryPlan, DeliveryTrip, DeliveryTripItem, FertilizationEntry, FertilizationPlan, Fertilizer, SpreadingSession, SpreadingSessionItem, ) from .services import sync_actual_bags_for_pairs, sync_spreading_session_side_effects class FertilizerSerializer(serializers.ModelSerializer): material_id = serializers.SerializerMethodField() class Meta: model = Fertilizer fields = [ 'id', 'name', 'maker', 'capacity_kg', 'nitrogen_pct', 'phosphorus_pct', 'potassium_pct', 'notes', 'material', 'material_id', ] def get_material_id(self, obj): return obj.material_id class FertilizationEntrySerializer(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 ) fertilizer_name = serializers.CharField(source='fertilizer.name', read_only=True) class Meta: model = FertilizationEntry fields = [ 'id', 'field', 'field_name', 'field_area_tan', 'fertilizer', 'fertilizer_name', 'bags', 'actual_bags', ] class FertilizationPlanSerializer(serializers.ModelSerializer): variety_name = serializers.SerializerMethodField() crop_name = serializers.SerializerMethodField() entries = FertilizationEntrySerializer(many=True, read_only=True) field_count = serializers.SerializerMethodField() fertilizer_count = serializers.SerializerMethodField() planned_total_bags = serializers.SerializerMethodField() spread_total_bags = serializers.SerializerMethodField() remaining_total_bags = serializers.SerializerMethodField() spread_status = serializers.SerializerMethodField() is_confirmed = serializers.BooleanField(read_only=True) confirmed_at = serializers.DateTimeField(read_only=True) class Meta: model = FertilizationPlan fields = [ 'id', 'name', 'year', 'variety', 'variety_name', 'crop_name', 'calc_settings', 'entries', 'field_count', 'fertilizer_count', 'planned_total_bags', 'spread_total_bags', 'remaining_total_bags', 'spread_status', 'is_confirmed', 'confirmed_at', 'created_at', 'updated_at', ] def get_variety_name(self, obj): return obj.variety.name def get_crop_name(self, obj): return obj.variety.crop.name def get_field_count(self, obj): return obj.entries.values('field').distinct().count() def get_fertilizer_count(self, obj): return obj.entries.values('fertilizer').distinct().count() def get_planned_total_bags(self, obj): total = sum((entry.bags or Decimal('0')) for entry in obj.entries.all()) return str(total) def get_spread_total_bags(self, obj): total = sum((entry.actual_bags or Decimal('0')) for entry in obj.entries.all()) return str(total) def get_remaining_total_bags(self, obj): planned = sum((entry.bags or Decimal('0')) for entry in obj.entries.all()) actual = sum((entry.actual_bags or Decimal('0')) for entry in obj.entries.all()) return str(planned - actual) def get_spread_status(self, obj): planned = sum((entry.bags or Decimal('0')) for entry in obj.entries.all()) actual = sum((entry.actual_bags or Decimal('0')) for entry in obj.entries.all()) if actual <= 0: return 'unspread' if actual > planned: return 'over_applied' if actual < planned: return 'partial' return 'completed' class FertilizationPlanWriteSerializer(serializers.ModelSerializer): entries = serializers.ListField(child=serializers.DictField(), write_only=True, required=False) class Meta: model = FertilizationPlan fields = ['id', 'name', 'year', 'variety', 'calc_settings', 'entries'] def create(self, validated_data): entries_data = validated_data.pop('entries', []) plan = FertilizationPlan.objects.create(**validated_data) pairs = self._save_entries(plan, entries_data) sync_actual_bags_for_pairs(plan.year, pairs) return plan def update(self, instance, validated_data): entries_data = validated_data.pop('entries', None) for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() if entries_data is not None: instance.entries.all().delete() pairs = self._save_entries(instance, entries_data) sync_actual_bags_for_pairs(instance.year, pairs) return instance def _save_entries(self, plan, entries_data): pairs = set() for entry in entries_data: pairs.add((entry['field_id'], entry['fertilizer_id'])) FertilizationEntry.objects.create( plan=plan, field_id=entry['field_id'], fertilizer_id=entry['fertilizer_id'], bags=entry['bags'], ) return pairs class DeliveryGroupFieldSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source='field.id', read_only=True) name = serializers.CharField(source='field.name', read_only=True) area_tan = serializers.DecimalField( source='field.area_tan', max_digits=6, decimal_places=4, read_only=True ) class Meta: model = DeliveryGroupField fields = ['id', 'name', 'area_tan'] class DeliveryGroupReadSerializer(serializers.ModelSerializer): fields = DeliveryGroupFieldSerializer(source='field_assignments', many=True, read_only=True) class Meta: model = DeliveryGroup fields = ['id', 'name', 'order', 'fields'] class DeliveryTripItemSerializer(serializers.ModelSerializer): field_name = serializers.CharField(source='field.name', read_only=True) fertilizer_name = serializers.CharField(source='fertilizer.name', read_only=True) spread_bags = serializers.SerializerMethodField() remaining_bags = serializers.SerializerMethodField() class Meta: model = DeliveryTripItem fields = [ 'id', 'field', 'field_name', 'fertilizer', 'fertilizer_name', 'bags', 'spread_bags', 'remaining_bags', ] def get_spread_bags(self, obj): total = ( SpreadingSessionItem.objects.filter( session__year=obj.trip.delivery_plan.year, field_id=obj.field_id, fertilizer_id=obj.fertilizer_id, ).aggregate(total=Sum('actual_bags'))['total'] ) return str(total or Decimal('0')) def get_remaining_bags(self, obj): total = ( SpreadingSessionItem.objects.filter( session__year=obj.trip.delivery_plan.year, field_id=obj.field_id, fertilizer_id=obj.fertilizer_id, ).aggregate(total=Sum('actual_bags'))['total'] ) spread_total = total or Decimal('0') return str(obj.bags - spread_total) class DeliveryTripReadSerializer(serializers.ModelSerializer): items = DeliveryTripItemSerializer(many=True, read_only=True) work_record_id = serializers.IntegerField(source='work_record.id', read_only=True) class Meta: model = DeliveryTrip fields = ['id', 'order', 'name', 'date', 'work_record_id', 'items'] class DeliveryPlanListSerializer(serializers.ModelSerializer): group_count = serializers.SerializerMethodField() trip_count = serializers.SerializerMethodField() class Meta: model = DeliveryPlan fields = [ 'id', 'year', 'name', 'group_count', 'trip_count', 'created_at', 'updated_at', ] def get_group_count(self, obj): return obj.groups.count() def get_trip_count(self, obj): return obj.trips.count() class DeliveryPlanReadSerializer(serializers.ModelSerializer): groups = DeliveryGroupReadSerializer(many=True, read_only=True) trips = DeliveryTripReadSerializer(many=True, read_only=True) unassigned_fields = serializers.SerializerMethodField() available_fertilizers = serializers.SerializerMethodField() all_entries = serializers.SerializerMethodField() class Meta: model = DeliveryPlan fields = [ 'id', 'year', 'name', 'groups', 'trips', 'unassigned_fields', 'available_fertilizers', 'all_entries', 'created_at', 'updated_at', ] def get_unassigned_fields(self, obj): assigned_ids = DeliveryGroupField.objects.filter( delivery_plan=obj ).values_list('field_id', flat=True) plan_field_ids = FertilizationEntry.objects.filter( plan__year=obj.year ).values_list('field_id', flat=True).distinct() from apps.fields.models import Field unassigned = Field.objects.filter( id__in=plan_field_ids ).exclude(id__in=assigned_ids).order_by('display_order', 'id') return [{'id': f.id, 'name': f.name, 'area_tan': str(f.area_tan)} for f in unassigned] def get_available_fertilizers(self, obj): fert_ids = FertilizationEntry.objects.filter( plan__year=obj.year ).values_list('fertilizer_id', flat=True).distinct() fertilizers = Fertilizer.objects.filter(id__in=fert_ids).order_by('name') return [{'id': f.id, 'name': f.name} for f in fertilizers] def get_all_entries(self, obj): entries = FertilizationEntry.objects.filter( plan__year=obj.year ).select_related('field', 'fertilizer') return [ { 'field': entry.field_id, 'field_name': entry.field.name, 'field_area_tan': str(entry.field.area_tan), 'fertilizer': entry.fertilizer_id, 'fertilizer_name': entry.fertilizer.name, 'bags': str(entry.bags), 'actual_bags': str(entry.actual_bags) if entry.actual_bags is not None else None, } for entry in entries ] class DeliveryPlanWriteSerializer(serializers.ModelSerializer): groups = serializers.ListField(child=serializers.DictField(), write_only=True, required=False) trips = serializers.ListField(child=serializers.DictField(), write_only=True, required=False) class Meta: model = DeliveryPlan fields = ['id', 'year', 'name', 'groups', 'trips'] def create(self, validated_data): groups_data = validated_data.pop('groups', []) trips_data = validated_data.pop('trips', []) plan = DeliveryPlan.objects.create(**validated_data) self._save_groups(plan, groups_data) self._save_trips(plan, trips_data) return plan def update(self, instance, validated_data): groups_data = validated_data.pop('groups', None) trips_data = validated_data.pop('trips', None) instance.name = validated_data.get('name', instance.name) instance.year = validated_data.get('year', instance.year) instance.save() if groups_data is not None: instance.groups.all().delete() self._save_groups(instance, groups_data) if trips_data is not None: instance.trips.all().delete() self._save_trips(instance, trips_data) return instance def _save_groups(self, plan, groups_data): for group_data in groups_data: group = DeliveryGroup.objects.create( delivery_plan=plan, name=group_data['name'], order=group_data.get('order', 0), ) for field_id in group_data.get('field_ids', []): DeliveryGroupField.objects.create( delivery_plan=plan, group=group, field_id=field_id, ) def _save_trips(self, plan, trips_data): for trip_data in trips_data: trip = DeliveryTrip.objects.create( delivery_plan=plan, order=trip_data.get('order', 0), name=trip_data.get('name', ''), date=trip_data.get('date'), ) for item in trip_data.get('items', []): DeliveryTripItem.objects.create( trip=trip, field_id=item['field_id'], fertilizer_id=item['fertilizer_id'], bags=item['bags'], ) sync_delivery_work_record(trip) class SpreadingSessionItemReadSerializer(serializers.ModelSerializer): field_name = serializers.CharField(source='field.name', read_only=True) fertilizer_name = serializers.CharField(source='fertilizer.name', read_only=True) class Meta: model = SpreadingSessionItem fields = [ 'id', 'field', 'field_name', 'fertilizer', 'fertilizer_name', 'actual_bags', 'planned_bags_snapshot', 'delivered_bags_snapshot', ] class SpreadingSessionSerializer(serializers.ModelSerializer): items = SpreadingSessionItemReadSerializer(many=True, read_only=True) work_record_id = serializers.IntegerField(source='work_record.id', read_only=True) class Meta: model = SpreadingSession fields = [ 'id', 'year', 'date', 'name', 'notes', 'work_record_id', 'items', 'created_at', 'updated_at', ] class SpreadingSessionItemWriteInputSerializer(serializers.Serializer): field_id = serializers.IntegerField() fertilizer_id = serializers.IntegerField() actual_bags = serializers.DecimalField(max_digits=10, decimal_places=4) planned_bags_snapshot = serializers.DecimalField(max_digits=10, decimal_places=4) delivered_bags_snapshot = serializers.DecimalField(max_digits=10, decimal_places=4) class SpreadingSessionWriteSerializer(serializers.ModelSerializer): items = SpreadingSessionItemWriteInputSerializer(many=True, write_only=True) class Meta: model = SpreadingSession fields = ['id', 'year', 'date', 'name', 'notes', 'items'] def validate_items(self, value): if not value: raise serializers.ValidationError('items を1件以上指定してください。') seen = set() for item in value: if item['actual_bags'] <= 0: raise serializers.ValidationError('actual_bags は 0 より大きい値を指定してください。') key = (item['field_id'], item['fertilizer_id']) if key in seen: raise serializers.ValidationError('同一 session 内で field + fertilizer を重複登録できません。') seen.add(key) return value def create(self, validated_data): items_data = validated_data.pop('items', []) session = SpreadingSession.objects.create(**validated_data) new_pairs = self._replace_items(session, items_data) sync_spreading_session_side_effects(session, new_pairs) return session def update(self, instance, validated_data): items_data = validated_data.pop('items', []) old_pairs = {(item.field_id, item.fertilizer_id) for item in instance.items.all()} for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() new_pairs = self._replace_items(instance, items_data) sync_spreading_session_side_effects(instance, old_pairs | new_pairs) return instance def _replace_items(self, session, items_data): session.items.all().delete() new_pairs = set() for item in items_data: new_pairs.add((item['field_id'], item['fertilizer_id'])) SpreadingSessionItem.objects.create( session=session, field_id=item['field_id'], fertilizer_id=item['fertilizer_id'], actual_bags=item['actual_bags'], planned_bags_snapshot=item['planned_bags_snapshot'], delivered_bags_snapshot=item['delivered_bags_snapshot'], ) return new_pairs