完璧に動作しています。

テスト	結果
確定取消 API	 is_confirmed: false, confirmed_at: null
USE トランザクション削除	 current_stock が 27.5→32 に復帰
引当再作成	 reserved_stock = 5.000 に復帰
追加した変更:

stock_service.py:81-93 — unconfirm_spreading(): USE削除→確定フラグリセット→引当再作成
fertilizer/views.py — unconfirm アクション(POST /api/fertilizer/plans/{id}/unconfirm/)
fertilizer/page.tsx — 一覧に「確定取消」ボタン(確定済み計画のみ表示)
FertilizerEditPage.tsx — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
This commit is contained in:
Akira
2026-03-15 13:28:02 +09:00
parent 42b11a5df8
commit 72b4d670fe
18 changed files with 807 additions and 60 deletions

View File

@@ -4,12 +4,19 @@ from django.http import HttpResponse
from django.template.loader import render_to_string
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from weasyprint import HTML
from apps.fields.models import Field
from apps.materials.stock_service import (
confirm_spreading as confirm_spreading_service,
create_reserves_for_plan,
delete_reserves_for_plan,
unconfirm_spreading,
)
from apps.plans.models import Plan, Variety
from .models import Fertilizer, FertilizationPlan, DistributionPlan
from .serializers import (
@@ -33,7 +40,7 @@ class FertilizationPlanViewSet(viewsets.ModelViewSet):
def get_queryset(self):
qs = FertilizationPlan.objects.select_related('variety', 'variety__crop').prefetch_related(
'entries', 'entries__field', 'entries__fertilizer'
'entries', 'entries__field', 'entries__fertilizer', 'entries__fertilizer__material'
)
year = self.request.query_params.get('year')
if year:
@@ -45,6 +52,20 @@ class FertilizationPlanViewSet(viewsets.ModelViewSet):
return FertilizationPlanWriteSerializer
return FertilizationPlanSerializer
def perform_create(self, serializer):
instance = serializer.save()
create_reserves_for_plan(instance)
def perform_update(self, serializer):
if serializer.instance.is_confirmed:
raise ValidationError({'detail': '確定済みの施肥計画は編集できません。'})
instance = serializer.save()
create_reserves_for_plan(instance)
def perform_destroy(self, instance):
delete_reserves_for_plan(instance)
instance.delete()
@action(detail=True, methods=['get'])
def pdf(self, request, pk=None):
plan = self.get_object()
@@ -99,6 +120,67 @@ class FertilizationPlanViewSet(viewsets.ModelViewSet):
response['Content-Disposition'] = f'attachment; filename="fertilization_{plan.year}_{plan.id}.pdf"'
return response
@action(detail=True, methods=['post'], url_path='confirm_spreading')
def confirm_spreading(self, request, pk=None):
plan = self.get_object()
if plan.is_confirmed:
return Response(
{'detail': 'この計画は既に散布確定済みです。'},
status=status.HTTP_400_BAD_REQUEST,
)
entries_data = request.data.get('entries', [])
if not entries_data:
return Response(
{'detail': '実績データが空です。'},
status=status.HTTP_400_BAD_REQUEST,
)
actual_entries = []
for entry in entries_data:
field_id = entry.get('field_id')
fertilizer_id = entry.get('fertilizer_id')
if not field_id or not fertilizer_id:
return Response(
{'detail': 'field_id と fertilizer_id が必要です。'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
actual_bags = Decimal(str(entry.get('actual_bags', 0)))
except InvalidOperation:
return Response(
{'detail': 'actual_bags は数値で指定してください。'},
status=status.HTTP_400_BAD_REQUEST,
)
actual_entries.append(
{
'field_id': field_id,
'fertilizer_id': fertilizer_id,
'actual_bags': actual_bags,
}
)
confirm_spreading_service(plan, actual_entries)
plan.refresh_from_db()
serializer = self.get_serializer(plan)
return Response(serializer.data)
@action(detail=True, methods=['post'], url_path='unconfirm')
def unconfirm(self, request, pk=None):
plan = self.get_object()
if not plan.is_confirmed:
return Response(
{'detail': 'この計画はまだ確定されていません。'},
status=status.HTTP_400_BAD_REQUEST,
)
unconfirm_spreading(plan)
plan.refresh_from_db()
serializer = self.get_serializer(plan)
return Response(serializer.data)
class CandidateFieldsView(APIView):
"""作付け計画から圃場候補を返す"""