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, delete_reserves_for_plan from apps.materials.models import StockTransaction from apps.workrecords.services import sync_spreading_work_record from .models import FertilizationEntry, FertilizationPlan, SpreadingSessionItem class FertilizationPlanMergeError(Exception): pass class FertilizationPlanMergeConflict(FertilizationPlanMergeError): def __init__(self, conflicts): super().__init__('merge conflict') self.conflicts = conflicts def sync_actual_bags_for_pairs(year, field_fertilizer_pairs): pairs = { (int(field_id), int(fertilizer_id)) for field_id, fertilizer_id in field_fertilizer_pairs } if not pairs: return for field_id, fertilizer_id in pairs: total = ( SpreadingSessionItem.objects.filter( session__year=year, field_id=field_id, fertilizer_id=fertilizer_id, ).aggregate(total=Sum('actual_bags'))['total'] ) FertilizationEntry.objects.filter( plan__year=year, field_id=field_id, fertilizer_id=fertilizer_id, ).update(actual_bags=total) @transaction.atomic def sync_spreading_session_side_effects(session, field_fertilizer_pairs): sync_actual_bags_for_pairs(session.year, field_fertilizer_pairs) sync_stock_uses_for_spreading_session(session) sync_spreading_work_record(session) @transaction.atomic def sync_stock_uses_for_spreading_session(session): StockTransaction.objects.filter(spreading_item__session=session).delete() session_items = session.items.select_related('fertilizer__material') for item in session_items: material = getattr(item.fertilizer, 'material', None) if material is None: continue StockTransaction.objects.create( material=material, transaction_type=StockTransaction.TransactionType.USE, quantity=item.actual_bags, occurred_on=session.date, note=f'散布実績「{session.name.strip() or session.date}」', fertilization_plan=None, spreading_item=item, ) @transaction.atomic def move_fertilization_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, ) .distinct() .prefetch_related('entries') ) for old_plan in old_plans: entries_to_move = list( old_plan.entries.filter( field_id=change.field_id, ).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 @transaction.atomic def merge_fertilization_plan_into(source_plan, target_plan): if source_plan.id == target_plan.id: raise FertilizationPlanMergeError('同じ施肥計画にはマージできません。') if source_plan.year != target_plan.year: raise FertilizationPlanMergeError('年度が異なる施肥計画にはマージできません。') if source_plan.variety_id != target_plan.variety_id: raise FertilizationPlanMergeError('品種が異なる施肥計画にはマージできません。') if source_plan.is_confirmed or target_plan.is_confirmed: raise FertilizationPlanMergeError('散布確定済みの施肥計画はマージできません。') source_entries = list( source_plan.entries.select_related('field', 'fertilizer').order_by('field_id', 'fertilizer_id') ) if not source_entries: raise FertilizationPlanMergeError('移動元の施肥計画にマージ対象の entry がありません。') source_pairs = {(entry.field_id, entry.fertilizer_id) for entry in source_entries} target_entries = list( target_plan.entries.select_related('field', 'fertilizer').order_by('field_id', 'fertilizer_id') ) target_pairs = {(entry.field_id, entry.fertilizer_id): entry for entry in target_entries} conflicts = [ { 'field_id': entry.field_id, 'field_name': entry.field.name, 'fertilizer_id': entry.fertilizer_id, 'fertilizer_name': entry.fertilizer.name, } for entry in source_entries if (entry.field_id, entry.fertilizer_id) in target_pairs ] if conflicts: raise FertilizationPlanMergeConflict(conflicts) FertilizationEntry.objects.filter( id__in=[entry.id for entry in source_entries] ).update(plan=target_plan) target_plan.calc_settings = _merge_calc_settings( target_plan.calc_settings, source_plan.calc_settings, ) target_plan.save() create_reserves_for_plan(target_plan) moved_count = len(source_entries) deleted_source_plan = False if not FertilizationEntry.objects.filter(plan=source_plan).exists(): delete_reserves_for_plan(source_plan) source_plan.delete() deleted_source_plan = True else: create_reserves_for_plan(source_plan) return { 'moved_entry_count': moved_count, 'deleted_source_plan': deleted_source_plan, } def _merge_calc_settings(target_settings, source_settings): merged = list(target_settings or []) existing_fertilizer_ids = { setting.get('fertilizer_id') for setting in merged if isinstance(setting, dict) } for setting in source_settings or []: if not isinstance(setting, dict): continue fertilizer_id = setting.get('fertilizer_id') if fertilizer_id in existing_fertilizer_ids: continue merged.append(setting) existing_fertilizer_ids.add(fertilizer_id) return merged