実運用のワークフロー(複数施肥計画混在・軽トラ複数回・肥料指定)に合わせ、 旧 DistributionPlan/Group/GroupField を DeliveryPlan/Group/GroupField/Trip/TripItem に置き換え。 施肥計画への直接FK廃止→年度ベースで全施肥計画を横断。 回ごとの日付記録、圃場の回間移動、対象肥料フィルタ、回ごとPDF出力に対応。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
276 lines
10 KiB
Python
276 lines
10 KiB
Python
from rest_framework import serializers
|
||
from .models import (
|
||
Fertilizer, FertilizationPlan, FertilizationEntry,
|
||
DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem,
|
||
)
|
||
|
||
|
||
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']
|
||
|
||
|
||
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()
|
||
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',
|
||
'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()
|
||
|
||
|
||
class FertilizationPlanWriteSerializer(serializers.ModelSerializer):
|
||
"""保存用(entries を一括で受け取る)"""
|
||
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)
|
||
self._save_entries(plan, entries_data)
|
||
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()
|
||
self._save_entries(instance, entries_data)
|
||
return instance
|
||
|
||
def _save_entries(self, plan, entries_data):
|
||
for entry in entries_data:
|
||
FertilizationEntry.objects.create(
|
||
plan=plan,
|
||
field_id=entry['field_id'],
|
||
fertilizer_id=entry['fertilizer_id'],
|
||
bags=entry['bags'],
|
||
)
|
||
|
||
|
||
# ─── 運搬計画 ────────────────────────────────────────────────────────────
|
||
|
||
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)
|
||
|
||
class Meta:
|
||
model = DeliveryTripItem
|
||
fields = ['id', 'field', 'field_name', 'fertilizer', 'fertilizer_name', 'bags']
|
||
|
||
|
||
class DeliveryTripReadSerializer(serializers.ModelSerializer):
|
||
items = DeliveryTripItemSerializer(many=True, read_only=True)
|
||
|
||
class Meta:
|
||
model = DeliveryTrip
|
||
fields = ['id', 'order', 'name', 'date', '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': e.field_id,
|
||
'field_name': e.field.name,
|
||
'field_area_tan': str(e.field.area_tan),
|
||
'fertilizer': e.fertilizer_id,
|
||
'fertilizer_name': e.fertilizer.name,
|
||
'bags': str(e.bags),
|
||
}
|
||
for e 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 g_data in groups_data:
|
||
group = DeliveryGroup.objects.create(
|
||
delivery_plan=plan,
|
||
name=g_data['name'],
|
||
order=g_data.get('order', 0),
|
||
)
|
||
for field_id in g_data.get('field_ids', []):
|
||
DeliveryGroupField.objects.create(
|
||
delivery_plan=plan,
|
||
group=group,
|
||
field_id=field_id,
|
||
)
|
||
|
||
def _save_trips(self, plan, trips_data):
|
||
for t_data in trips_data:
|
||
trip = DeliveryTrip.objects.create(
|
||
delivery_plan=plan,
|
||
order=t_data.get('order', 0),
|
||
name=t_data.get('name', ''),
|
||
date=t_data.get('date'),
|
||
)
|
||
for item in t_data.get('items', []):
|
||
DeliveryTripItem.objects.create(
|
||
trip=trip,
|
||
field_id=item['field_id'],
|
||
fertilizer_id=item['fertilizer_id'],
|
||
bags=item['bags'],
|
||
)
|