diff --git a/backend/apps/fertilizer/services.py b/backend/apps/fertilizer/services.py index 0b125d8..19748f0 100644 --- a/backend/apps/fertilizer/services.py +++ b/backend/apps/fertilizer/services.py @@ -3,9 +3,10 @@ from decimal import Decimal from django.db import transaction from django.db.models import Sum +from apps.materials.stock_service import create_reserves_for_plan from apps.materials.models import StockTransaction from apps.workrecords.services import sync_spreading_work_record -from .models import FertilizationEntry, SpreadingSessionItem +from .models import FertilizationEntry, FertilizationPlan, SpreadingSessionItem def sync_actual_bags_for_pairs(year, field_fertilizer_pairs): @@ -56,3 +57,51 @@ def sync_stock_uses_for_spreading_session(session): fertilization_plan=None, spreading_item=item, ) + + +@transaction.atomic +def move_unspread_entries_for_variety_change(change): + moved_count = 0 + old_variety_id = change.old_variety_id + new_variety = change.new_variety + if old_variety_id is None or new_variety is None: + return 0 + + old_plans = ( + FertilizationPlan.objects + .filter( + year=change.year, + variety_id=old_variety_id, + entries__field_id=change.field_id, + entries__actual_bags__isnull=True, + ) + .distinct() + .prefetch_related('entries') + ) + + for old_plan in old_plans: + entries_to_move = list( + old_plan.entries.filter( + field_id=change.field_id, + actual_bags__isnull=True, + ).order_by('id') + ) + if not entries_to_move: + continue + + new_plan = FertilizationPlan.objects.create( + name=f'{change.year}年度 {new_variety.name} 施肥計画(品種変更移動)', + year=change.year, + variety=new_variety, + calc_settings=old_plan.calc_settings, + ) + + FertilizationEntry.objects.filter( + id__in=[entry.id for entry in entries_to_move] + ).update(plan=new_plan) + + create_reserves_for_plan(old_plan) + create_reserves_for_plan(new_plan) + moved_count += len(entries_to_move) + + return moved_count diff --git a/backend/apps/plans/services.py b/backend/apps/plans/services.py index 96c2527..551f018 100644 --- a/backend/apps/plans/services.py +++ b/backend/apps/plans/services.py @@ -59,7 +59,12 @@ def handle_plan_variety_change(plan: Plan, *, old_variety, new_variety, reason: def process_plan_variety_change(change: PlanVarietyChange): - """後続 issue で施肥計画・田植え計画への移動処理を追加する入口。""" + from apps.fertilizer.services import move_unspread_entries_for_variety_change + + moved_count = move_unspread_entries_for_variety_change(change) + if moved_count != change.moved_entry_count: + change.moved_entry_count = moved_count + change.save(update_fields=['moved_entry_count']) return change diff --git a/backend/apps/plans/tests.py b/backend/apps/plans/tests.py index f6d1f44..7e6fb82 100644 --- a/backend/apps/plans/tests.py +++ b/backend/apps/plans/tests.py @@ -2,7 +2,10 @@ from django.contrib.auth import get_user_model from django.test import TestCase from rest_framework.test import APIRequestFactory, force_authenticate +from apps.fertilizer.models import FertilizationEntry, FertilizationPlan, Fertilizer from apps.fields.models import Field +from apps.materials.models import Material, StockTransaction +from apps.materials.stock_service import create_reserves_for_plan from .models import Crop, Plan, PlanVarietyChange, Variety from .serializers import PlanSerializer from .views import PlanViewSet @@ -34,6 +37,15 @@ class PlanVarietyChangeTests(TestCase): variety=self.old_variety, notes='', ) + self.other_field = Field.objects.create( + name='足川南', + address='高知県高岡郡', + area_tan='0.8000', + area_m2=800, + owner_name='吉田', + group_name='南', + display_order=2, + ) def test_serializer_update_creates_history_when_variety_changes(self): serializer = PlanSerializer( @@ -90,3 +102,102 @@ class PlanVarietyChangeTests(TestCase): change = PlanVarietyChange.objects.get(plan=self.plan) self.assertEqual(change.old_variety_id, self.old_variety.id) self.assertEqual(change.new_variety_id, self.new_variety.id) + + def test_serializer_update_moves_only_unspread_fertilizer_entries(self): + material_unspread = Material.objects.create( + name='高度化成14号', + material_type=Material.MaterialType.FERTILIZER, + ) + material_partial = Material.objects.create( + name='分げつ一発', + material_type=Material.MaterialType.FERTILIZER, + ) + fertilizer_unspread = Fertilizer.objects.create( + name='高度化成14号', + material=material_unspread, + ) + fertilizer_partial = Fertilizer.objects.create( + name='分げつ一発', + material=material_partial, + ) + old_fertilization_plan = FertilizationPlan.objects.create( + name='2026年度 にこまる 元肥', + year=2026, + variety=self.old_variety, + calc_settings=[{'fertilizer_id': fertilizer_unspread.id, 'method': 'per_tan', 'param': '1.0'}], + ) + unspread_entry = FertilizationEntry.objects.create( + plan=old_fertilization_plan, + field=self.field, + fertilizer=fertilizer_unspread, + bags='4.00', + actual_bags=None, + ) + partial_entry = FertilizationEntry.objects.create( + plan=old_fertilization_plan, + field=self.field, + fertilizer=fertilizer_partial, + bags='3.00', + actual_bags='1.0000', + ) + untouched_entry = FertilizationEntry.objects.create( + plan=old_fertilization_plan, + field=self.other_field, + fertilizer=fertilizer_unspread, + bags='2.00', + actual_bags=None, + ) + create_reserves_for_plan(old_fertilization_plan) + + serializer = PlanSerializer( + instance=self.plan, + data={'variety': self.new_variety.id}, + partial=True, + ) + self.assertTrue(serializer.is_valid(), serializer.errors) + serializer.save() + + change = PlanVarietyChange.objects.get(plan=self.plan) + self.assertEqual(change.moved_entry_count, 1) + + old_fertilization_plan.refresh_from_db() + new_plan = FertilizationPlan.objects.exclude(id=old_fertilization_plan.id).get( + year=2026, + variety=self.new_variety, + ) + self.assertEqual( + new_plan.name, + f'2026年度 {self.new_variety.name} 施肥計画(品種変更移動)', + ) + self.assertEqual(new_plan.calc_settings, old_fertilization_plan.calc_settings) + + unspread_entry.refresh_from_db() + partial_entry.refresh_from_db() + untouched_entry.refresh_from_db() + + self.assertEqual(unspread_entry.plan_id, new_plan.id) + self.assertEqual(partial_entry.plan_id, old_fertilization_plan.id) + self.assertEqual(untouched_entry.plan_id, old_fertilization_plan.id) + + old_reserves = list( + StockTransaction.objects.filter( + fertilization_plan=old_fertilization_plan, + transaction_type=StockTransaction.TransactionType.RESERVE, + ).order_by('material__name') + ) + new_reserves = list( + StockTransaction.objects.filter( + fertilization_plan=new_plan, + transaction_type=StockTransaction.TransactionType.RESERVE, + ).order_by('material__name') + ) + self.assertEqual(len(old_reserves), 2) + self.assertEqual(len(new_reserves), 1) + self.assertEqual( + {(reserve.material_id, reserve.quantity) for reserve in old_reserves}, + { + (material_partial.id, partial_entry.bags), + (material_unspread.id, untouched_entry.bags), + }, + ) + self.assertEqual(new_reserves[0].quantity, unspread_entry.bags)