Files
keinasystem/backend/apps/materials/serializers.py
akira 491f05eee8 その判断で進めました。在庫管理を先に固めるように切り替えて、手元の実装もそちらを優先して直しています。
今回入れたのは、在庫履歴の編集・削除対応と、種子資材を資材マスタ側で品種に直接結び付ける流れです。backend/apps/materials/views.py と backend/apps/materials/serializers.py で stock-transactions の更新を許可しつつ、計画や実績に紐づく履歴はロック扱いにしました。画面側は frontend/src/app/materials/page.tsx、frontend/src/app/materials/_components/StockOverview.tsx、frontend/src/app/materials/_components/StockTransactionForm.tsx を更新して、手動登録の入出庫履歴をあとから編集・削除できるようにしています。

あわせて frontend/src/app/materials/masters/page.tsx と frontend/src/app/materials/_components/MaterialForm.tsx で、種子 タブから品種を直接選んで紐付ける形にしました。重複を避けるため、作付け計画側の品種管理モーダル frontend/src/app/allocation/page.tsx から種子資材の紐付け UI は外しています。Issue #2 にもこの方針でコメント追記済みです。

確認できたのは python3 -m py_compile までで、フロントのビルドは frontend/node_modules が無いためまだ回していません。次はブラウザで在庫管理の操作感を確認してから、田植え計画側の細部を詰めるのがよさそうです。
2026-04-05 11:43:03 +09:00

226 lines
7.9 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.SEED,
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,
)
is_locked = serializers.SerializerMethodField()
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',
'spreading_item',
'is_locked',
'created_at',
]
read_only_fields = ['created_at']
def get_is_locked(self, obj):
return bool(obj.fertilization_plan_id or obj.spreading_item_id)
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)