493 lines
17 KiB
Python
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
|