今回入れたのは、在庫履歴の編集・削除対応と、種子資材を資材マスタ側で品種に直接結び付ける流れです。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 が無いためまだ回していません。次はブラウザで在庫管理の操作感を確認してから、田植え計画側の細部を詰めるのがよさそうです。
226 lines
7.9 KiB
Python
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)
|