649 lines
25 KiB
Python
649 lines
25 KiB
Python
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.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 (
|
||
create_reserves_for_plan,
|
||
delete_reserves_for_plan,
|
||
)
|
||
from apps.plans.models import Plan
|
||
from .models import (
|
||
Fertilizer, FertilizationPlan, FertilizationEntry,
|
||
DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem,
|
||
SpreadingSession, SpreadingSessionItem,
|
||
)
|
||
from .serializers import (
|
||
FertilizerSerializer,
|
||
FertilizationPlanSerializer,
|
||
FertilizationPlanWriteSerializer,
|
||
DeliveryPlanListSerializer,
|
||
DeliveryPlanReadSerializer,
|
||
DeliveryPlanWriteSerializer,
|
||
SpreadingSessionSerializer,
|
||
SpreadingSessionWriteSerializer,
|
||
)
|
||
from .services import (
|
||
FertilizationPlanMergeConflict,
|
||
FertilizationPlanMergeError,
|
||
merge_fertilization_plan_into,
|
||
sync_actual_bags_for_pairs,
|
||
)
|
||
|
||
|
||
class FertilizerViewSet(viewsets.ModelViewSet):
|
||
permission_classes = [IsAuthenticated]
|
||
queryset = Fertilizer.objects.all()
|
||
serializer_class = FertilizerSerializer
|
||
|
||
|
||
class FertilizationPlanViewSet(viewsets.ModelViewSet):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
qs = FertilizationPlan.objects.select_related('variety', 'variety__crop').prefetch_related(
|
||
'entries', 'entries__field', 'entries__fertilizer', 'entries__fertilizer__material'
|
||
)
|
||
year = self.request.query_params.get('year')
|
||
if year:
|
||
qs = qs.filter(year=year)
|
||
return qs
|
||
|
||
def get_serializer_class(self):
|
||
if self.action in ['create', 'update', 'partial_update']:
|
||
return FertilizationPlanWriteSerializer
|
||
return FertilizationPlanSerializer
|
||
|
||
def perform_create(self, serializer):
|
||
instance = serializer.save()
|
||
create_reserves_for_plan(instance)
|
||
|
||
def perform_update(self, serializer):
|
||
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()
|
||
entries = plan.entries.select_related('field', 'fertilizer').order_by(
|
||
'field__display_order', 'field__id', 'fertilizer__name'
|
||
)
|
||
|
||
# 圃場・肥料の一覧を整理
|
||
fields_map = {}
|
||
fertilizers_map = {}
|
||
for entry in entries:
|
||
fields_map[entry.field_id] = entry.field
|
||
fertilizers_map[entry.fertilizer_id] = entry.fertilizer
|
||
|
||
fields = sorted(fields_map.values(), key=lambda f: (f.display_order, f.id))
|
||
fertilizers = sorted(fertilizers_map.values(), key=lambda f: f.name)
|
||
|
||
# マトリクスデータ生成
|
||
matrix = {}
|
||
for entry in entries:
|
||
matrix[(entry.field_id, entry.fertilizer_id)] = entry.bags
|
||
|
||
rows = []
|
||
for field in fields:
|
||
cells = [matrix.get((field.id, fert.id), '') for fert in fertilizers]
|
||
total = sum(v for v in cells if v != '')
|
||
rows.append({
|
||
'field': field,
|
||
'cells': cells,
|
||
'total': total,
|
||
})
|
||
|
||
# 肥料ごとの合計
|
||
fert_totals = []
|
||
for fert in fertilizers:
|
||
total = sum(
|
||
matrix.get((field.id, fert.id), Decimal('0'))
|
||
for field in fields
|
||
)
|
||
fert_totals.append(total)
|
||
|
||
context = {
|
||
'plan': plan,
|
||
'fertilizers': fertilizers,
|
||
'rows': rows,
|
||
'fert_totals': fert_totals,
|
||
'grand_total': sum(fert_totals),
|
||
}
|
||
html_string = render_to_string('fertilizer/pdf.html', context)
|
||
pdf_file = HTML(string=html_string).write_pdf()
|
||
response = HttpResponse(pdf_file, content_type='application/pdf')
|
||
response['Content-Disposition'] = f'attachment; filename="fertilization_{plan.year}_{plan.id}.pdf"'
|
||
return response
|
||
|
||
@action(detail=True, methods=['get'])
|
||
def merge_targets(self, request, pk=None):
|
||
source_plan = self.get_object()
|
||
targets = (
|
||
FertilizationPlan.objects
|
||
.filter(year=source_plan.year, variety_id=source_plan.variety_id)
|
||
.exclude(id=source_plan.id)
|
||
.prefetch_related('entries')
|
||
.order_by('-updated_at', 'id')
|
||
)
|
||
data = [
|
||
{
|
||
'id': plan.id,
|
||
'name': plan.name,
|
||
'field_count': plan.entries.values('field').distinct().count(),
|
||
'planned_total_bags': str(sum((entry.bags or Decimal('0')) for entry in plan.entries.all())),
|
||
'is_confirmed': plan.is_confirmed,
|
||
}
|
||
for plan in targets
|
||
]
|
||
return Response(data)
|
||
|
||
@action(detail=True, methods=['post'])
|
||
def merge_into(self, request, pk=None):
|
||
source_plan = self.get_object()
|
||
target_plan_id = request.data.get('target_plan_id')
|
||
if not target_plan_id:
|
||
return Response({'error': 'target_plan_id が必要です。'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
target_plan = FertilizationPlan.objects.get(id=target_plan_id)
|
||
except FertilizationPlan.DoesNotExist:
|
||
return Response({'error': 'マージ先の施肥計画が見つかりません。'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
try:
|
||
result = merge_fertilization_plan_into(source_plan, target_plan)
|
||
except FertilizationPlanMergeConflict as exc:
|
||
return Response(
|
||
{
|
||
'error': '競合する圃場・肥料があるためマージできません。',
|
||
'conflicts': exc.conflicts,
|
||
},
|
||
status=status.HTTP_409_CONFLICT,
|
||
)
|
||
except FertilizationPlanMergeError as exc:
|
||
return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
return Response(result)
|
||
|
||
class CandidateFieldsView(APIView):
|
||
"""作付け計画から圃場候補を返す"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get(self, request):
|
||
year = request.query_params.get('year')
|
||
variety_id = request.query_params.get('variety_id')
|
||
if not year or not variety_id:
|
||
return Response({'error': 'year と variety_id が必要です'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
field_ids = Plan.objects.filter(
|
||
year=year, variety_id=variety_id
|
||
).values_list('field_id', flat=True)
|
||
|
||
fields = Field.objects.filter(id__in=field_ids).order_by('display_order', 'id')
|
||
data = [
|
||
{
|
||
'id': f.id,
|
||
'name': f.name,
|
||
'area_tan': str(f.area_tan),
|
||
'area_m2': f.area_m2,
|
||
'group_name': f.group_name,
|
||
}
|
||
for f in fields
|
||
]
|
||
return Response(data)
|
||
|
||
|
||
class CalculateView(APIView):
|
||
"""自動計算(保存しない)"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
method = request.data.get('method') # 'nitrogen' | 'even' | 'per_tan'
|
||
param = request.data.get('param') # 数値パラメータ
|
||
fertilizer_id = request.data.get('fertilizer_id')
|
||
field_ids = request.data.get('field_ids', [])
|
||
|
||
if not method or param is None or not field_ids:
|
||
return Response({'error': 'method, param, field_ids が必要です'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
param = Decimal(str(param))
|
||
except InvalidOperation:
|
||
return Response({'error': 'param は数値で指定してください'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
fields = Field.objects.filter(id__in=field_ids)
|
||
if not fields.exists():
|
||
return Response({'error': '圃場が見つかりません'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
results = []
|
||
|
||
if method == 'per_tan':
|
||
# 反当袋数配分: S = Sa × A
|
||
for field in fields:
|
||
area = Decimal(str(field.area_tan))
|
||
bags = (param * area).quantize(Decimal('0.01'))
|
||
results.append({'field_id': field.id, 'bags': float(bags)})
|
||
|
||
elif method == 'even':
|
||
# 在庫/指定数量均等配分: S = (SS / Sum(A)) × A
|
||
total_area = sum(Decimal(str(f.area_tan)) for f in fields)
|
||
if total_area == 0:
|
||
return Response({'error': '圃場の面積が0です'}, status=status.HTTP_400_BAD_REQUEST)
|
||
for field in fields:
|
||
area = Decimal(str(field.area_tan))
|
||
bags = (param * area / total_area).quantize(Decimal('0.01'))
|
||
results.append({'field_id': field.id, 'bags': float(bags)})
|
||
|
||
elif method == 'nitrogen':
|
||
# 反当チッソ成分量配分: S = (Nr / (C × Nd/100)) × A
|
||
if not fertilizer_id:
|
||
return Response({'error': 'nitrogen 方式には fertilizer_id が必要です'}, status=status.HTTP_400_BAD_REQUEST)
|
||
try:
|
||
fertilizer = Fertilizer.objects.get(id=fertilizer_id)
|
||
except Fertilizer.DoesNotExist:
|
||
return Response({'error': '肥料が見つかりません'}, status=status.HTTP_404_NOT_FOUND)
|
||
if not fertilizer.capacity_kg or not fertilizer.nitrogen_pct:
|
||
return Response(
|
||
{'error': 'この肥料には1袋重量(kg)と窒素含有率(%)の登録が必要です'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
c = Decimal(str(fertilizer.capacity_kg))
|
||
nd = Decimal(str(fertilizer.nitrogen_pct))
|
||
# 1袋あたりの窒素量 (kg)
|
||
nc = c * nd / Decimal('100')
|
||
if nc == 0:
|
||
return Response({'error': '窒素含有量が0のため計算できません'}, status=status.HTTP_400_BAD_REQUEST)
|
||
for field in fields:
|
||
area = Decimal(str(field.area_tan))
|
||
bags = (param / nc * area).quantize(Decimal('0.01'))
|
||
results.append({'field_id': field.id, 'bags': float(bags)})
|
||
|
||
else:
|
||
return Response({'error': 'method は nitrogen / even / per_tan のいずれかです'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
return Response(results)
|
||
|
||
|
||
class DeliveryPlanViewSet(viewsets.ModelViewSet):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
qs = DeliveryPlan.objects.prefetch_related(
|
||
'groups', 'groups__field_assignments', 'groups__field_assignments__field',
|
||
'trips', 'trips__items', 'trips__items__field', 'trips__items__fertilizer',
|
||
)
|
||
year = self.request.query_params.get('year')
|
||
if year:
|
||
qs = qs.filter(year=year)
|
||
return qs
|
||
|
||
def get_serializer_class(self):
|
||
if self.action in ['create', 'update', 'partial_update']:
|
||
return DeliveryPlanWriteSerializer
|
||
if self.action == 'list':
|
||
return DeliveryPlanListSerializer
|
||
return DeliveryPlanReadSerializer
|
||
|
||
@action(detail=True, methods=['get'])
|
||
def pdf(self, request, pk=None):
|
||
plan = self.get_object()
|
||
|
||
# 全tripのitemから使用肥料を収集
|
||
all_items = DeliveryTripItem.objects.filter(
|
||
trip__delivery_plan=plan
|
||
).select_related('field', 'fertilizer')
|
||
|
||
fert_ids = all_items.values_list('fertilizer_id', flat=True).distinct()
|
||
fertilizers = sorted(
|
||
Fertilizer.objects.filter(id__in=fert_ids),
|
||
key=lambda f: f.name
|
||
)
|
||
|
||
# グループ情報: field_id → group_name
|
||
field_group_map = {}
|
||
for gf in DeliveryGroupField.objects.filter(
|
||
delivery_plan=plan
|
||
).select_related('group', 'field'):
|
||
field_group_map[gf.field_id] = gf.group
|
||
|
||
# 回ごとにページを構築
|
||
trip_pages = []
|
||
for trip in plan.trips.prefetch_related('items__field', 'items__fertilizer').all():
|
||
items = trip.items.all()
|
||
if not items:
|
||
continue
|
||
|
||
# この回の肥料一覧
|
||
trip_fert_ids = set(item.fertilizer_id for item in items)
|
||
trip_fertilizers = [f for f in fertilizers if f.id in trip_fert_ids]
|
||
|
||
# items を (field_id, fertilizer_id) → bags のマトリクスに変換
|
||
item_map = {}
|
||
for item in items:
|
||
item_map[(item.field_id, item.fertilizer_id)] = item.bags
|
||
|
||
# グループごとにまとめる
|
||
groups_dict = {} # group_name → {'group': group, 'fields': [field, ...]}
|
||
ungrouped_fields = []
|
||
for item in items:
|
||
group = field_group_map.get(item.field_id)
|
||
if group:
|
||
if group.name not in groups_dict:
|
||
groups_dict[group.name] = {'group': group, 'fields': []}
|
||
if item.field not in groups_dict[group.name]['fields']:
|
||
groups_dict[group.name]['fields'].append(item.field)
|
||
else:
|
||
if item.field not in ungrouped_fields:
|
||
ungrouped_fields.append(item.field)
|
||
|
||
# グループを order 順にソート
|
||
sorted_groups = sorted(groups_dict.values(), key=lambda g: (g['group'].order, g['group'].id))
|
||
|
||
group_rows = []
|
||
for g_data in sorted_groups:
|
||
fields_in_group = sorted(g_data['fields'], key=lambda f: (f.display_order, f.id))
|
||
group_totals = []
|
||
for fert in trip_fertilizers:
|
||
total = sum(
|
||
item_map.get((f.id, fert.id), Decimal('0'))
|
||
for f in fields_in_group
|
||
)
|
||
group_totals.append(total)
|
||
|
||
field_rows = []
|
||
for field in fields_in_group:
|
||
cells = [item_map.get((field.id, fert.id), '') for fert in trip_fertilizers]
|
||
field_rows.append({'field': field, 'cells': cells})
|
||
|
||
group_rows.append({
|
||
'name': g_data['group'].name,
|
||
'totals': group_totals,
|
||
'field_rows': field_rows,
|
||
})
|
||
|
||
# 未グループ圃場
|
||
if ungrouped_fields:
|
||
ungrouped_fields = sorted(ungrouped_fields, key=lambda f: (f.display_order, f.id))
|
||
ua_totals = [
|
||
sum(item_map.get((f.id, fert.id), Decimal('0')) for f in ungrouped_fields)
|
||
for fert in trip_fertilizers
|
||
]
|
||
field_rows = []
|
||
for field in ungrouped_fields:
|
||
cells = [item_map.get((field.id, fert.id), '') for fert in trip_fertilizers]
|
||
field_rows.append({'field': field, 'cells': cells})
|
||
group_rows.append({
|
||
'name': '未グループ',
|
||
'totals': ua_totals,
|
||
'field_rows': field_rows,
|
||
})
|
||
|
||
fert_totals = [
|
||
sum(r['totals'][i] for r in group_rows)
|
||
for i in range(len(trip_fertilizers))
|
||
]
|
||
|
||
trip_pages.append({
|
||
'trip': trip,
|
||
'fertilizers': trip_fertilizers,
|
||
'group_rows': group_rows,
|
||
'fert_totals': fert_totals,
|
||
})
|
||
|
||
context = {
|
||
'plan': plan,
|
||
'trip_pages': trip_pages,
|
||
}
|
||
html_string = render_to_string('fertilizer/delivery_pdf.html', context)
|
||
pdf_file = HTML(string=html_string).write_pdf()
|
||
response = HttpResponse(pdf_file, content_type='application/pdf')
|
||
response['Content-Disposition'] = (
|
||
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):
|
||
from apps.materials.models import StockTransaction
|
||
year = instance.year
|
||
affected_pairs = {(item.field_id, item.fertilizer_id) for item in instance.items.all()}
|
||
StockTransaction.objects.filter(spreading_item__session=instance).delete()
|
||
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)
|
||
else:
|
||
delivery_queryset = delivery_queryset.filter(trip__date__isnull=False)
|
||
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)
|