Files
keinasystem/backend/apps/fertilizer/serializers.py
2026-04-06 16:49:44 +09:00

493 lines
17 KiB
Python

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)
is_variety_change_plan = serializers.SerializerMethodField()
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',
'is_variety_change_plan',
'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'
def get_is_variety_change_plan(self, obj):
return obj.name.endswith('(品種変更移動)')
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