diff --git a/backend/apps/materials/serializers.py b/backend/apps/materials/serializers.py index 6b0cf7a..3d7fd3d 100644 --- a/backend/apps/materials/serializers.py +++ b/backend/apps/materials/serializers.py @@ -183,6 +183,7 @@ class StockTransactionSerializer(serializers.ModelSerializer): source='get_transaction_type_display', read_only=True, ) + is_locked = serializers.SerializerMethodField() class Meta: model = StockTransaction @@ -199,10 +200,15 @@ class StockTransactionSerializer(serializers.ModelSerializer): '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() diff --git a/backend/apps/materials/views.py b/backend/apps/materials/views.py index 1106872..e462cc7 100644 --- a/backend/apps/materials/views.py +++ b/backend/apps/materials/views.py @@ -54,7 +54,7 @@ class StockTransactionViewSet(viewsets.ModelViewSet): serializer_class = StockTransactionSerializer permission_classes = [IsAuthenticated] - http_method_names = ['get', 'post', 'delete', 'head', 'options'] + http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] def get_queryset(self): queryset = StockTransaction.objects.select_related('material') @@ -77,6 +77,33 @@ class StockTransactionViewSet(viewsets.ModelViewSet): return queryset + def update(self, request, *args, **kwargs): + instance = self.get_object() + if instance.fertilization_plan_id or instance.spreading_item_id: + return Response( + {'detail': '計画や実績に紐づく入出庫履歴は編集できません。'}, + status=status.HTTP_400_BAD_REQUEST, + ) + return super().update(request, *args, **kwargs) + + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + if instance.fertilization_plan_id or instance.spreading_item_id: + return Response( + {'detail': '計画や実績に紐づく入出庫履歴は編集できません。'}, + status=status.HTTP_400_BAD_REQUEST, + ) + return super().partial_update(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if instance.fertilization_plan_id or instance.spreading_item_id: + return Response( + {'detail': '計画や実績に紐づく入出庫履歴は削除できません。'}, + status=status.HTTP_400_BAD_REQUEST, + ) + return super().destroy(request, *args, **kwargs) + class StockSummaryView(generics.ListAPIView): """在庫集計一覧""" diff --git a/frontend/src/app/allocation/page.tsx b/frontend/src/app/allocation/page.tsx index 5d5f216..0a7cab5 100644 --- a/frontend/src/app/allocation/page.tsx +++ b/frontend/src/app/allocation/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo } from 'react'; import { api } from '@/lib/api'; -import { Field, Crop, Material, Plan } from '@/types'; +import { Field, Crop, Plan } from '@/types'; import Navbar from '@/components/Navbar'; import { Menu, X, BarChart3, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Copy, Settings, Trash2, CheckSquare, Search } from 'lucide-react'; @@ -23,7 +23,6 @@ export default function AllocationPage() { const [fields, setFields] = useState([]); const [crops, setCrops] = useState([]); const [plans, setPlans] = useState([]); - const [seedMaterials, setSeedMaterials] = useState([]); const [year, setYear] = useState(() => { if (typeof window !== 'undefined') { const saved = localStorage.getItem('allocationYear'); @@ -61,16 +60,14 @@ export default function AllocationPage() { const fetchData = async (background = false) => { if (!background) setLoading(true); try { - const [fieldsRes, cropsRes, plansRes, seedMaterialsRes] = await Promise.all([ + const [fieldsRes, cropsRes, plansRes] = await Promise.all([ api.get('/fields/?ordering=group_name,display_order,id'), api.get('/plans/crops/'), api.get(`/plans/?year=${year}`), - api.get('/materials/materials/?material_type=seed'), ]); setFields(fieldsRes.data); setCrops(cropsRes.data); setPlans(plansRes.data); - setSeedMaterials(seedMaterialsRes.data); } catch (error) { console.error('Failed to fetch data:', error); } finally { @@ -384,20 +381,6 @@ export default function AllocationPage() { } }; - const handleUpdateVarietySeedMaterial = async (varietyId: number, seedMaterialId: string) => { - try { - const variety = crops.flatMap((crop) => crop.varieties).find((item) => item.id === varietyId); - if (!variety) return; - await api.patch(`/plans/varieties/${varietyId}/`, { - seed_material: seedMaterialId ? parseInt(seedMaterialId, 10) : null, - }); - await fetchData(true); - } catch (error) { - console.error('Failed to update variety seed material:', error); - alert('種子在庫の紐付け更新に失敗しました'); - } - }; - const toggleFieldSelection = (fieldId: number) => { setSelectedFields((prev) => { const next = new Set(prev); @@ -1079,15 +1062,6 @@ export default function AllocationPage() { initialValue={v.default_seedling_boxes_per_tan} onSave={handleUpdateVarietyDefaultBoxes} /> -
- -
))} @@ -1196,60 +1170,3 @@ function VarietyDefaultBoxesForm({ ); } - -function VarietySeedMaterialForm({ - varietyId, - initialValue, - initialLabel, - materials, - onSave, -}: { - varietyId: number; - initialValue: string; - initialLabel: string | null; - materials: Material[]; - onSave: (varietyId: number, seedMaterialId: string) => Promise; -}) { - const [value, setValue] = useState(initialValue); - const [saving, setSaving] = useState(false); - - useEffect(() => { - setValue(initialValue); - }, [initialValue]); - - const handleSave = async () => { - setSaving(true); - await onSave(varietyId, value); - setSaving(false); - }; - - return ( -
-
- - -

- 現在: {initialLabel || '未設定'} -

-
- -
- ); -} diff --git a/frontend/src/app/materials/_components/MaterialForm.tsx b/frontend/src/app/materials/_components/MaterialForm.tsx index 12c4fdb..072b6de 100644 --- a/frontend/src/app/materials/_components/MaterialForm.tsx +++ b/frontend/src/app/materials/_components/MaterialForm.tsx @@ -9,6 +9,7 @@ export type MaterialTab = 'fertilizer' | 'pesticide' | 'seed' | 'misc'; export interface MaterialFormState { name: string; material_type: Material['material_type']; + seed_variety_id: string; maker: string; stock_unit: Material['stock_unit']; is_active: boolean; @@ -33,6 +34,7 @@ interface MaterialFormProps { tab: MaterialTab; form: MaterialFormState; saving: boolean; + seedVarietyOptions?: { id: number; label: string }[]; onBaseFieldChange: ( field: keyof Omit, value: string | boolean @@ -56,6 +58,7 @@ export default function MaterialForm({ tab, form, saving, + seedVarietyOptions = [], onBaseFieldChange, onFertilizerFieldChange, onPesticideFieldChange, @@ -245,9 +248,18 @@ export default function MaterialForm({ {tab === 'seed' ? ( -
- 種子 -
+ ) : (