テスト 結果 確定取消 API ✅ is_confirmed: false, confirmed_at: null USE トランザクション削除 ✅ current_stock が 27.5→32 に復帰 引当再作成 ✅ reserved_stock = 5.000 に復帰 追加した変更: stock_service.py:81-93 — unconfirm_spreading(): USE削除→確定フラグリセット→引当再作成 fertilizer/views.py — unconfirm アクション(POST /api/fertilizer/plans/{id}/unconfirm/) fertilizer/page.tsx — 一覧に「確定取消」ボタン(確定済み計画のみ表示) FertilizerEditPage.tsx — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
238 lines
9.1 KiB
Python
238 lines
9.1 KiB
Python
from rest_framework import serializers
|
||
from .models import Fertilizer, FertilizationPlan, FertilizationEntry, DistributionPlan, DistributionGroup, DistributionGroupField
|
||
|
||
|
||
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 DistributionGroupFieldSerializer(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 = DistributionGroupField
|
||
fields = ['id', 'name', 'area_tan']
|
||
|
||
|
||
class DistributionGroupReadSerializer(serializers.ModelSerializer):
|
||
fields = DistributionGroupFieldSerializer(source='field_assignments', many=True, read_only=True)
|
||
|
||
class Meta:
|
||
model = DistributionGroup
|
||
fields = ['id', 'name', 'order', 'fields']
|
||
|
||
|
||
class FertilizationPlanForDistributionSerializer(serializers.ModelSerializer):
|
||
"""分配計画詳細に埋め込む施肥計画情報(肥料一覧・entries 含む)"""
|
||
variety_name = serializers.SerializerMethodField()
|
||
crop_name = serializers.SerializerMethodField()
|
||
fertilizers = serializers.SerializerMethodField()
|
||
entries = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = FertilizationPlan
|
||
fields = ['id', 'name', 'year', 'variety_name', 'crop_name', 'fertilizers', 'entries']
|
||
|
||
def get_variety_name(self, obj):
|
||
return obj.variety.name
|
||
|
||
def get_crop_name(self, obj):
|
||
return obj.variety.crop.name
|
||
|
||
def get_fertilizers(self, obj):
|
||
fert_ids = obj.entries.values_list('fertilizer_id', flat=True).distinct()
|
||
from .models import Fertilizer as F
|
||
fertilizers = F.objects.filter(id__in=fert_ids).order_by('name')
|
||
return [{'id': f.id, 'name': f.name} for f in fertilizers]
|
||
|
||
def get_entries(self, obj):
|
||
return [
|
||
{'field': e.field_id, 'fertilizer': e.fertilizer_id, 'bags': str(e.bags)}
|
||
for e in obj.entries.all()
|
||
]
|
||
|
||
|
||
class DistributionPlanListSerializer(serializers.ModelSerializer):
|
||
fertilization_plan_id = serializers.IntegerField(source='fertilization_plan.id', read_only=True)
|
||
fertilization_plan_name = serializers.CharField(source='fertilization_plan.name', read_only=True)
|
||
year = serializers.IntegerField(source='fertilization_plan.year', read_only=True)
|
||
variety_name = serializers.SerializerMethodField()
|
||
crop_name = serializers.SerializerMethodField()
|
||
group_count = serializers.SerializerMethodField()
|
||
field_count = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = DistributionPlan
|
||
fields = [
|
||
'id', 'name', 'fertilization_plan_id', 'fertilization_plan_name',
|
||
'year', 'variety_name', 'crop_name', 'group_count', 'field_count',
|
||
'created_at', 'updated_at',
|
||
]
|
||
|
||
def get_variety_name(self, obj):
|
||
return obj.fertilization_plan.variety.name
|
||
|
||
def get_crop_name(self, obj):
|
||
return obj.fertilization_plan.variety.crop.name
|
||
|
||
def get_group_count(self, obj):
|
||
return obj.groups.count()
|
||
|
||
def get_field_count(self, obj):
|
||
return obj.distributiongroupfield_set.count()
|
||
|
||
|
||
class DistributionPlanReadSerializer(serializers.ModelSerializer):
|
||
fertilization_plan = FertilizationPlanForDistributionSerializer(read_only=True)
|
||
groups = DistributionGroupReadSerializer(many=True, read_only=True)
|
||
unassigned_fields = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = DistributionPlan
|
||
fields = ['id', 'name', 'fertilization_plan', 'groups', 'unassigned_fields', 'created_at', 'updated_at']
|
||
|
||
def get_unassigned_fields(self, obj):
|
||
assigned_ids = obj.distributiongroupfield_set.values_list('field_id', flat=True)
|
||
plan_field_ids = obj.fertilization_plan.entries.values_list('field_id', flat=True).distinct()
|
||
from apps.fields.models import Field as F
|
||
unassigned = F.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]
|
||
|
||
|
||
class DistributionPlanWriteSerializer(serializers.ModelSerializer):
|
||
fertilization_plan_id = serializers.IntegerField(write_only=True)
|
||
groups = serializers.ListField(child=serializers.DictField(), write_only=True, required=False)
|
||
|
||
class Meta:
|
||
model = DistributionPlan
|
||
fields = ['id', 'name', 'fertilization_plan_id', 'groups']
|
||
|
||
def create(self, validated_data):
|
||
groups_data = validated_data.pop('groups', [])
|
||
plan = DistributionPlan.objects.create(**validated_data)
|
||
self._save_groups(plan, groups_data)
|
||
return plan
|
||
|
||
def update(self, instance, validated_data):
|
||
groups_data = validated_data.pop('groups', None)
|
||
instance.name = validated_data.get('name', instance.name)
|
||
instance.save()
|
||
if groups_data is not None:
|
||
instance.groups.all().delete()
|
||
self._save_groups(instance, groups_data)
|
||
return instance
|
||
|
||
def _save_groups(self, plan, groups_data):
|
||
for g_data in groups_data:
|
||
group = DistributionGroup.objects.create(
|
||
distribution_plan=plan,
|
||
name=g_data['name'],
|
||
order=g_data.get('order', 0),
|
||
)
|
||
for field_id in g_data.get('field_ids', []):
|
||
DistributionGroupField.objects.create(
|
||
distribution_plan=plan,
|
||
group=group,
|
||
field_id=field_id,
|
||
)
|