From 491f05eee85ade5285f6d962d12cc4bad211ea8c Mon Sep 17 00:00:00 2001 From: akira Date: Sun, 5 Apr 2026 11:43:03 +0900 Subject: [PATCH] =?UTF-8?q?=E3=81=9D=E3=81=AE=E5=88=A4=E6=96=AD=E3=81=A7?= =?UTF-8?q?=E9=80=B2=E3=82=81=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82=E5=9C=A8?= =?UTF-8?q?=E5=BA=AB=E7=AE=A1=E7=90=86=E3=82=92=E5=85=88=E3=81=AB=E5=9B=BA?= =?UTF-8?q?=E3=82=81=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=88=87=E3=82=8A?= =?UTF-8?q?=E6=9B=BF=E3=81=88=E3=81=A6=E3=80=81=E6=89=8B=E5=85=83=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=82=82=E3=81=9D=E3=81=A1=E3=82=89=E3=82=92?= =?UTF-8?q?=E5=84=AA=E5=85=88=E3=81=97=E3=81=A6=E7=9B=B4=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=BE=E3=81=99=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 今回入れたのは、在庫履歴の編集・削除対応と、種子資材を資材マスタ側で品種に直接結び付ける流れです。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 が無いためまだ回していません。次はブラウザで在庫管理の操作感を確認してから、田植え計画側の細部を詰めるのがよさそうです。 --- backend/apps/materials/serializers.py | 6 + backend/apps/materials/views.py | 29 ++- frontend/src/app/allocation/page.tsx | 87 +------- .../materials/_components/MaterialForm.tsx | 18 +- .../materials/_components/StockOverview.tsx | 24 ++- .../_components/StockTransactionForm.tsx | 35 +++- frontend/src/app/materials/masters/page.tsx | 187 ++++++++++-------- frontend/src/app/materials/page.tsx | 39 +++- frontend/src/types/index.ts | 2 + 9 files changed, 249 insertions(+), 178 deletions(-) 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' ? ( -
- 種子 -
+ ) : (