Files
keinasystem/backend/apps/fertilizer/services.py
2026-04-06 16:49:44 +09:00

197 lines
6.5 KiB
Python

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