施肥散布実績機能を実装し運搬・作業記録・在庫連携を追加
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.db.models import Sum
|
||||
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
|
||||
@@ -12,15 +12,14 @@ 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 apps.plans.models import Plan
|
||||
from .models import (
|
||||
Fertilizer, FertilizationPlan, FertilizationEntry,
|
||||
DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem,
|
||||
SpreadingSession, SpreadingSessionItem,
|
||||
)
|
||||
from .serializers import (
|
||||
FertilizerSerializer,
|
||||
@@ -29,7 +28,10 @@ from .serializers import (
|
||||
DeliveryPlanListSerializer,
|
||||
DeliveryPlanReadSerializer,
|
||||
DeliveryPlanWriteSerializer,
|
||||
SpreadingSessionSerializer,
|
||||
SpreadingSessionWriteSerializer,
|
||||
)
|
||||
from .services import sync_actual_bags_for_pairs
|
||||
|
||||
|
||||
class FertilizerViewSet(viewsets.ModelViewSet):
|
||||
@@ -60,8 +62,6 @@ class FertilizationPlanViewSet(viewsets.ModelViewSet):
|
||||
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)
|
||||
|
||||
@@ -123,68 +123,6 @@ 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):
|
||||
"""作付け計画から圃場候補を返す"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -421,3 +359,232 @@ class DeliveryPlanViewSet(viewsets.ModelViewSet):
|
||||
f'attachment; filename="delivery_{plan.year}_{plan.id}.pdf"'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class SpreadingSessionViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = SpreadingSession.objects.prefetch_related(
|
||||
'items',
|
||||
'items__field',
|
||||
'items__fertilizer',
|
||||
).select_related('work_record')
|
||||
year = self.request.query_params.get('year')
|
||||
if year:
|
||||
queryset = queryset.filter(year=year)
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ['create', 'update', 'partial_update']:
|
||||
return SpreadingSessionWriteSerializer
|
||||
return SpreadingSessionSerializer
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
year = instance.year
|
||||
affected_pairs = {(item.field_id, item.fertilizer_id) for item in instance.items.all()}
|
||||
instance.delete()
|
||||
sync_actual_bags_for_pairs(year, affected_pairs)
|
||||
|
||||
|
||||
class SpreadingCandidatesView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
year = request.query_params.get('year')
|
||||
session_id = request.query_params.get('session_id')
|
||||
delivery_plan_id = request.query_params.get('delivery_plan_id')
|
||||
plan_id = request.query_params.get('plan_id')
|
||||
if not year:
|
||||
return Response(
|
||||
{'detail': 'year が必要です。'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
year = int(year)
|
||||
except (TypeError, ValueError):
|
||||
return Response(
|
||||
{'detail': 'year は数値で指定してください。'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if delivery_plan_id:
|
||||
try:
|
||||
delivery_plan_id = int(delivery_plan_id)
|
||||
except (TypeError, ValueError):
|
||||
return Response(
|
||||
{'detail': 'delivery_plan_id は数値で指定してください。'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if plan_id:
|
||||
try:
|
||||
plan_id = int(plan_id)
|
||||
except (TypeError, ValueError):
|
||||
return Response(
|
||||
{'detail': 'plan_id は数値で指定してください。'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
current_session = None
|
||||
current_map = {}
|
||||
if session_id:
|
||||
try:
|
||||
current_session = SpreadingSession.objects.prefetch_related('items').get(
|
||||
pk=session_id,
|
||||
year=year,
|
||||
)
|
||||
except SpreadingSession.DoesNotExist:
|
||||
return Response(
|
||||
{'detail': '散布実績が見つかりません。'},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
current_map = {
|
||||
(item.field_id, item.fertilizer_id): {
|
||||
'actual_bags': item.actual_bags,
|
||||
'field_name': item.field.name,
|
||||
'field_area_tan': str(item.field.area_tan),
|
||||
'fertilizer_name': item.fertilizer.name,
|
||||
}
|
||||
for item in current_session.items.all()
|
||||
}
|
||||
|
||||
candidates = {}
|
||||
|
||||
plan_queryset = FertilizationEntry.objects.filter(plan__year=year)
|
||||
if plan_id:
|
||||
plan_queryset = plan_queryset.filter(plan_id=plan_id)
|
||||
plan_rows = (
|
||||
plan_queryset
|
||||
.values(
|
||||
'field_id',
|
||||
'field__name',
|
||||
'field__area_tan',
|
||||
'fertilizer_id',
|
||||
'fertilizer__name',
|
||||
)
|
||||
.annotate(planned_bags=Sum('bags'))
|
||||
)
|
||||
for row in plan_rows:
|
||||
key = (row['field_id'], row['fertilizer_id'])
|
||||
candidates.setdefault(
|
||||
key,
|
||||
{
|
||||
'field': row['field_id'],
|
||||
'field_name': row['field__name'],
|
||||
'field_area_tan': str(row['field__area_tan']),
|
||||
'fertilizer': row['fertilizer_id'],
|
||||
'fertilizer_name': row['fertilizer__name'],
|
||||
'planned_bags': Decimal('0'),
|
||||
'delivered_bags': Decimal('0'),
|
||||
'spread_bags': Decimal('0'),
|
||||
'current_session_bags': Decimal('0'),
|
||||
},
|
||||
)['planned_bags'] = row['planned_bags'] or Decimal('0')
|
||||
|
||||
delivery_queryset = DeliveryTripItem.objects.filter(trip__delivery_plan__year=year)
|
||||
if delivery_plan_id:
|
||||
delivery_queryset = delivery_queryset.filter(trip__delivery_plan_id=delivery_plan_id)
|
||||
delivery_rows = delivery_queryset.values(
|
||||
'field_id',
|
||||
'field__name',
|
||||
'field__area_tan',
|
||||
'fertilizer_id',
|
||||
'fertilizer__name',
|
||||
).annotate(delivered_bags=Sum('bags'))
|
||||
for row in delivery_rows:
|
||||
key = (row['field_id'], row['fertilizer_id'])
|
||||
candidates.setdefault(
|
||||
key,
|
||||
{
|
||||
'field': row['field_id'],
|
||||
'field_name': row['field__name'],
|
||||
'field_area_tan': str(row['field__area_tan']),
|
||||
'fertilizer': row['fertilizer_id'],
|
||||
'fertilizer_name': row['fertilizer__name'],
|
||||
'planned_bags': Decimal('0'),
|
||||
'delivered_bags': Decimal('0'),
|
||||
'spread_bags': Decimal('0'),
|
||||
'current_session_bags': Decimal('0'),
|
||||
},
|
||||
)['delivered_bags'] = row['delivered_bags'] or Decimal('0')
|
||||
|
||||
spread_queryset = SpreadingSessionItem.objects.filter(session__year=year)
|
||||
if current_session is not None:
|
||||
spread_queryset = spread_queryset.exclude(session=current_session)
|
||||
spread_rows = (
|
||||
spread_queryset
|
||||
.values(
|
||||
'field_id',
|
||||
'field__name',
|
||||
'field__area_tan',
|
||||
'fertilizer_id',
|
||||
'fertilizer__name',
|
||||
)
|
||||
.annotate(spread_bags=Sum('actual_bags'))
|
||||
)
|
||||
for row in spread_rows:
|
||||
key = (row['field_id'], row['fertilizer_id'])
|
||||
candidates.setdefault(
|
||||
key,
|
||||
{
|
||||
'field': row['field_id'],
|
||||
'field_name': row['field__name'],
|
||||
'field_area_tan': str(row['field__area_tan']),
|
||||
'fertilizer': row['fertilizer_id'],
|
||||
'fertilizer_name': row['fertilizer__name'],
|
||||
'planned_bags': Decimal('0'),
|
||||
'delivered_bags': Decimal('0'),
|
||||
'spread_bags': Decimal('0'),
|
||||
'current_session_bags': Decimal('0'),
|
||||
},
|
||||
)['spread_bags'] = row['spread_bags'] or Decimal('0')
|
||||
|
||||
for key, current_data in current_map.items():
|
||||
candidates.setdefault(
|
||||
key,
|
||||
{
|
||||
'field': key[0],
|
||||
'field_name': current_data['field_name'],
|
||||
'field_area_tan': current_data['field_area_tan'],
|
||||
'fertilizer': key[1],
|
||||
'fertilizer_name': current_data['fertilizer_name'],
|
||||
'planned_bags': Decimal('0'),
|
||||
'delivered_bags': Decimal('0'),
|
||||
'spread_bags': Decimal('0'),
|
||||
'current_session_bags': Decimal('0'),
|
||||
},
|
||||
)['current_session_bags'] = current_data['actual_bags'] or Decimal('0')
|
||||
|
||||
rows = []
|
||||
for candidate in candidates.values():
|
||||
delivered = candidate['delivered_bags']
|
||||
planned = candidate['planned_bags']
|
||||
current_bags = candidate['current_session_bags']
|
||||
if delivery_plan_id:
|
||||
include_row = delivered > 0 or current_bags > 0
|
||||
elif plan_id:
|
||||
include_row = planned > 0 or current_bags > 0
|
||||
else:
|
||||
include_row = delivered > 0 or current_bags > 0
|
||||
if not include_row:
|
||||
continue
|
||||
remaining = delivered - candidate['spread_bags']
|
||||
rows.append(
|
||||
{
|
||||
'field': candidate['field'],
|
||||
'field_name': candidate['field_name'],
|
||||
'field_area_tan': candidate['field_area_tan'],
|
||||
'fertilizer': candidate['fertilizer'],
|
||||
'fertilizer_name': candidate['fertilizer_name'],
|
||||
'planned_bags': str(planned),
|
||||
'delivered_bags': str(delivered),
|
||||
'spread_bags': str(candidate['spread_bags'] + current_bags),
|
||||
'spread_bags_other': str(candidate['spread_bags']),
|
||||
'current_session_bags': str(current_bags),
|
||||
'remaining_bags': str(remaining),
|
||||
}
|
||||
)
|
||||
|
||||
rows.sort(key=lambda row: (row['field_name'], row['fertilizer_name']))
|
||||
return Response(rows)
|
||||
|
||||
Reference in New Issue
Block a user