在庫管理機能 Phase 1 実装(apps/materials + フロントエンド)
Backend: - apps/materials 新規作成(Material, FertilizerProfile, PesticideProfile, StockTransaction) - 資材マスタ CRUD API(/api/materials/materials/) - 入出庫履歴 API(/api/materials/stock-transactions/) - 在庫集計 API(/api/materials/stock-summary/) - 既存 Fertilizer に material OneToOneField 追加(0005マイグレーション、データ移行込み) Frontend: - /materials: 在庫一覧画面(タブフィルタ、履歴展開、入出庫モーダル) - /materials/masters: 資材マスタ管理(肥料/農薬/その他タブ、インライン編集) - Navbar に「在庫管理」メニュー追加 - Material/StockTransaction/StockSummary 型定義追加 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
212
backend/apps/materials/serializers.py
Normal file
212
backend/apps/materials/serializers.py
Normal file
@@ -0,0 +1,212 @@
|
||||
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',
|
||||
'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)
|
||||
last_transaction_date = serializers.DateField(allow_null=True)
|
||||
Reference in New Issue
Block a user