テスト 結果 確定取消 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 — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
216 lines
7.6 KiB
Python
216 lines
7.6 KiB
Python
from decimal import Decimal
|
|
|
|
from django.db import transaction
|
|
from rest_framework import serializers
|
|
|
|
from .models import (
|
|
FertilizerProfile,
|
|
Material,
|
|
PesticideProfile,
|
|
StockTransaction,
|
|
)
|
|
|
|
|
|
class FertilizerProfileSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = FertilizerProfile
|
|
fields = ['capacity_kg', 'nitrogen_pct', 'phosphorus_pct', 'potassium_pct']
|
|
|
|
|
|
class PesticideProfileSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = PesticideProfile
|
|
fields = [
|
|
'registration_no',
|
|
'formulation',
|
|
'usage_unit',
|
|
'dilution_ratio',
|
|
'active_ingredient',
|
|
'category',
|
|
]
|
|
|
|
|
|
class MaterialReadSerializer(serializers.ModelSerializer):
|
|
material_type_display = serializers.CharField(
|
|
source='get_material_type_display',
|
|
read_only=True,
|
|
)
|
|
stock_unit_display = serializers.CharField(
|
|
source='get_stock_unit_display',
|
|
read_only=True,
|
|
)
|
|
fertilizer_profile = FertilizerProfileSerializer(read_only=True)
|
|
pesticide_profile = PesticideProfileSerializer(read_only=True)
|
|
current_stock = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Material
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'material_type',
|
|
'material_type_display',
|
|
'maker',
|
|
'stock_unit',
|
|
'stock_unit_display',
|
|
'is_active',
|
|
'notes',
|
|
'fertilizer_profile',
|
|
'pesticide_profile',
|
|
'current_stock',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
|
|
def get_current_stock(self, obj):
|
|
transactions = list(obj.stock_transactions.all())
|
|
increase = sum(
|
|
transaction.quantity
|
|
for transaction in transactions
|
|
if transaction.transaction_type in StockTransaction.INCREASE_TYPES
|
|
)
|
|
decrease = sum(
|
|
transaction.quantity
|
|
for transaction in transactions
|
|
if transaction.transaction_type in StockTransaction.DECREASE_TYPES
|
|
)
|
|
return increase - decrease
|
|
|
|
|
|
class MaterialWriteSerializer(serializers.ModelSerializer):
|
|
fertilizer_profile = FertilizerProfileSerializer(required=False, allow_null=True)
|
|
pesticide_profile = PesticideProfileSerializer(required=False, allow_null=True)
|
|
|
|
class Meta:
|
|
model = Material
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'material_type',
|
|
'maker',
|
|
'stock_unit',
|
|
'is_active',
|
|
'notes',
|
|
'fertilizer_profile',
|
|
'pesticide_profile',
|
|
]
|
|
|
|
def validate(self, attrs):
|
|
material_type = attrs.get('material_type')
|
|
if self.instance is not None and material_type is None:
|
|
material_type = self.instance.material_type
|
|
|
|
fertilizer_profile = attrs.get('fertilizer_profile')
|
|
pesticide_profile = attrs.get('pesticide_profile')
|
|
|
|
if material_type == Material.MaterialType.FERTILIZER and pesticide_profile:
|
|
raise serializers.ValidationError(
|
|
{'pesticide_profile': '肥料には農薬詳細を設定できません。'}
|
|
)
|
|
if material_type == Material.MaterialType.PESTICIDE and fertilizer_profile:
|
|
raise serializers.ValidationError(
|
|
{'fertilizer_profile': '農薬には肥料詳細を設定できません。'}
|
|
)
|
|
if (
|
|
material_type in {Material.MaterialType.SEEDLING, Material.MaterialType.OTHER}
|
|
and (fertilizer_profile or pesticide_profile)
|
|
):
|
|
raise serializers.ValidationError(
|
|
'種苗・その他には詳細プロファイルを設定できません。'
|
|
)
|
|
return attrs
|
|
|
|
@transaction.atomic
|
|
def create(self, validated_data):
|
|
fertilizer_profile_data = validated_data.pop('fertilizer_profile', None)
|
|
pesticide_profile_data = validated_data.pop('pesticide_profile', None)
|
|
|
|
material = Material.objects.create(**validated_data)
|
|
self._save_profiles(material, fertilizer_profile_data, pesticide_profile_data)
|
|
return material
|
|
|
|
@transaction.atomic
|
|
def update(self, instance, validated_data):
|
|
fertilizer_profile_data = validated_data.pop('fertilizer_profile', None)
|
|
pesticide_profile_data = validated_data.pop('pesticide_profile', None)
|
|
|
|
for attr, value in validated_data.items():
|
|
setattr(instance, attr, value)
|
|
instance.save()
|
|
|
|
self._save_profiles(instance, fertilizer_profile_data, pesticide_profile_data)
|
|
return instance
|
|
|
|
def to_representation(self, instance):
|
|
return MaterialReadSerializer(instance, context=self.context).data
|
|
|
|
def _save_profiles(self, material, fertilizer_profile_data, pesticide_profile_data):
|
|
if material.material_type == Material.MaterialType.FERTILIZER:
|
|
if fertilizer_profile_data is not None:
|
|
profile, _ = FertilizerProfile.objects.get_or_create(material=material)
|
|
for attr, value in fertilizer_profile_data.items():
|
|
setattr(profile, attr, value)
|
|
profile.save()
|
|
PesticideProfile.objects.filter(material=material).delete()
|
|
return
|
|
|
|
if material.material_type == Material.MaterialType.PESTICIDE:
|
|
if pesticide_profile_data is not None:
|
|
profile, _ = PesticideProfile.objects.get_or_create(material=material)
|
|
for attr, value in pesticide_profile_data.items():
|
|
setattr(profile, attr, value)
|
|
profile.save()
|
|
FertilizerProfile.objects.filter(material=material).delete()
|
|
return
|
|
|
|
FertilizerProfile.objects.filter(material=material).delete()
|
|
PesticideProfile.objects.filter(material=material).delete()
|
|
|
|
|
|
class StockTransactionSerializer(serializers.ModelSerializer):
|
|
material_name = serializers.CharField(source='material.name', read_only=True)
|
|
material_type = serializers.CharField(source='material.material_type', read_only=True)
|
|
stock_unit = serializers.CharField(source='material.stock_unit', read_only=True)
|
|
stock_unit_display = serializers.CharField(
|
|
source='material.get_stock_unit_display',
|
|
read_only=True,
|
|
)
|
|
transaction_type_display = serializers.CharField(
|
|
source='get_transaction_type_display',
|
|
read_only=True,
|
|
)
|
|
|
|
class Meta:
|
|
model = StockTransaction
|
|
fields = [
|
|
'id',
|
|
'material',
|
|
'material_name',
|
|
'material_type',
|
|
'transaction_type',
|
|
'transaction_type_display',
|
|
'quantity',
|
|
'stock_unit',
|
|
'stock_unit_display',
|
|
'occurred_on',
|
|
'note',
|
|
'fertilization_plan',
|
|
'created_at',
|
|
]
|
|
read_only_fields = ['created_at']
|
|
|
|
|
|
class StockSummarySerializer(serializers.Serializer):
|
|
material_id = serializers.IntegerField()
|
|
name = serializers.CharField()
|
|
material_type = serializers.CharField()
|
|
material_type_display = serializers.CharField()
|
|
maker = serializers.CharField()
|
|
stock_unit = serializers.CharField()
|
|
stock_unit_display = serializers.CharField()
|
|
is_active = serializers.BooleanField()
|
|
current_stock = serializers.DecimalField(max_digits=10, decimal_places=3)
|
|
reserved_stock = serializers.DecimalField(max_digits=10, decimal_places=3)
|
|
available_stock = serializers.DecimalField(max_digits=10, decimal_places=3)
|
|
last_transaction_date = serializers.DateField(allow_null=True)
|