分配計画機能を実装
施肥計画の圃場を配置場所単位でグループ化し、グループ×肥料の集計表を 表示・PDF出力できる機能を追加。 - Backend: DistributionPlan/Group/GroupField モデル (migration 0003) - API: GET/POST/PUT/DELETE/PDF (/api/fertilizer/distribution/) - Frontend: 一覧・新規作成・編集画面 (/distribution) - Navbar に分配計画メニューを追加 - 集計プレビューはクライアントサイド計算(API不要) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,14 @@ from weasyprint import HTML
|
||||
|
||||
from apps.fields.models import Field
|
||||
from apps.plans.models import Plan, Variety
|
||||
from .models import Fertilizer, FertilizationPlan
|
||||
from .models import Fertilizer, FertilizationPlan, DistributionPlan
|
||||
from .serializers import (
|
||||
FertilizerSerializer,
|
||||
FertilizationPlanSerializer,
|
||||
FertilizationPlanWriteSerializer,
|
||||
DistributionPlanListSerializer,
|
||||
DistributionPlanReadSerializer,
|
||||
DistributionPlanWriteSerializer,
|
||||
)
|
||||
|
||||
|
||||
@@ -194,3 +197,128 @@ class CalculateView(APIView):
|
||||
return Response({'error': 'method は nitrogen / even / per_tan のいずれかです'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(results)
|
||||
|
||||
|
||||
class DistributionPlanViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = DistributionPlan.objects.select_related(
|
||||
'fertilization_plan', 'fertilization_plan__variety', 'fertilization_plan__variety__crop'
|
||||
).prefetch_related(
|
||||
'groups', 'groups__field_assignments', 'groups__field_assignments__field',
|
||||
'fertilization_plan__entries', 'fertilization_plan__entries__field',
|
||||
'fertilization_plan__entries__fertilizer',
|
||||
'distributiongroupfield_set',
|
||||
)
|
||||
year = self.request.query_params.get('year')
|
||||
if year:
|
||||
qs = qs.filter(fertilization_plan__year=year)
|
||||
return qs
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ['create', 'update', 'partial_update']:
|
||||
return DistributionPlanWriteSerializer
|
||||
if self.action == 'list':
|
||||
return DistributionPlanListSerializer
|
||||
return DistributionPlanReadSerializer
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def pdf(self, request, pk=None):
|
||||
dist_plan = self.get_object()
|
||||
fert_plan = dist_plan.fertilization_plan
|
||||
|
||||
# 施肥計画の肥料一覧(名前順)
|
||||
fert_ids = fert_plan.entries.values_list('fertilizer_id', flat=True).distinct()
|
||||
fertilizers = sorted(
|
||||
Fertilizer.objects.filter(id__in=fert_ids),
|
||||
key=lambda f: f.name
|
||||
)
|
||||
|
||||
# entries を (field_id, fertilizer_id) → bags のマトリクスに変換
|
||||
entry_map = {}
|
||||
for e in fert_plan.entries.all():
|
||||
entry_map[(e.field_id, e.fertilizer_id)] = e.bags
|
||||
|
||||
# グループ行の構築
|
||||
groups = dist_plan.groups.prefetch_related('field_assignments__field').all()
|
||||
group_rows = []
|
||||
for group in groups:
|
||||
fields_in_group = [
|
||||
a.field for a in group.field_assignments.select_related('field').order_by('field__display_order', 'field__id')
|
||||
]
|
||||
# グループ合計(肥料ごと)
|
||||
group_totals = []
|
||||
for fert in fertilizers:
|
||||
total = sum(
|
||||
entry_map.get((f.id, fert.id), Decimal('0'))
|
||||
for f in fields_in_group
|
||||
)
|
||||
group_totals.append(total)
|
||||
group_row_total = sum(group_totals)
|
||||
|
||||
# 圃場サブ行
|
||||
field_rows = []
|
||||
for field in fields_in_group:
|
||||
cells = [entry_map.get((field.id, fert.id), '') for fert in fertilizers]
|
||||
row_total = sum(v for v in cells if v != '')
|
||||
field_rows.append({'field': field, 'cells': cells, 'total': row_total})
|
||||
|
||||
group_rows.append({
|
||||
'name': group.name,
|
||||
'totals': group_totals,
|
||||
'row_total': group_row_total,
|
||||
'field_rows': field_rows,
|
||||
})
|
||||
|
||||
# 未割り当て圃場
|
||||
assigned_ids = dist_plan.distributiongroupfield_set.values_list('field_id', flat=True)
|
||||
plan_field_ids = fert_plan.entries.values_list('field_id', flat=True).distinct()
|
||||
unassigned_fields = Field.objects.filter(
|
||||
id__in=plan_field_ids
|
||||
).exclude(id__in=assigned_ids).order_by('display_order', 'id')
|
||||
|
||||
unassigned_rows = []
|
||||
if unassigned_fields.exists():
|
||||
ua_totals = []
|
||||
for fert in fertilizers:
|
||||
total = sum(
|
||||
entry_map.get((f.id, fert.id), Decimal('0'))
|
||||
for f in unassigned_fields
|
||||
)
|
||||
ua_totals.append(total)
|
||||
unassigned_rows = [{
|
||||
'name': '未割り当て',
|
||||
'totals': ua_totals,
|
||||
'row_total': sum(ua_totals),
|
||||
'field_rows': [
|
||||
{
|
||||
'field': f,
|
||||
'cells': [entry_map.get((f.id, fert.id), '') for fert in fertilizers],
|
||||
'total': sum(entry_map.get((f.id, fert.id), Decimal('0')) for fert in fertilizers),
|
||||
}
|
||||
for f in unassigned_fields
|
||||
],
|
||||
}]
|
||||
|
||||
all_group_rows = group_rows + unassigned_rows
|
||||
fert_totals = [
|
||||
sum(r['totals'][i] for r in all_group_rows)
|
||||
for i in range(len(fertilizers))
|
||||
]
|
||||
|
||||
context = {
|
||||
'dist_plan': dist_plan,
|
||||
'fert_plan': fert_plan,
|
||||
'fertilizers': fertilizers,
|
||||
'group_rows': all_group_rows,
|
||||
'fert_totals': fert_totals,
|
||||
'grand_total': sum(fert_totals),
|
||||
}
|
||||
html_string = render_to_string('fertilizer/distribution_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="distribution_{fert_plan.year}_{dist_plan.id}.pdf"'
|
||||
)
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user