施肥計画機能を追加(年度×品種単位のマトリクス管理)
- Backend: apps/fertilizer を新規追加 - Fertilizer(肥料マスタ)、FertilizationPlan、FertilizationEntry モデル - 肥料マスタ・施肥計画 CRUD API - 3方式の自動計算API(反当袋数・均等配分・反当チッソ成分量) - 作付け計画から圃場候補を取得する API - WeasyPrint による PDF 出力(圃場×肥料=袋数 マトリクス表) - Frontend: app/fertilizer を新規追加 - 施肥計画一覧(年度セレクタ・PDF出力・編集・削除) - 肥料マスタ管理(インライン編集) - 施肥計画編集(品種選択→圃場自動取得→肥料追加→自動計算→マトリクス手動調整) - Navbar に「施肥計画」メニューを追加(Sprout アイコン) - Cursor ルールファイル・連携ガイドを削除(Claude Code 単独運用へ) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
backend/apps/fertilizer/__init__.py
Normal file
0
backend/apps/fertilizer/__init__.py
Normal file
19
backend/apps/fertilizer/admin.py
Normal file
19
backend/apps/fertilizer/admin.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from .models import Fertilizer, FertilizationPlan, FertilizationEntry
|
||||
|
||||
|
||||
@admin.register(Fertilizer)
|
||||
class FertilizerAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'maker', 'capacity_kg', 'nitrogen_pct']
|
||||
|
||||
|
||||
class FertilizationEntryInline(admin.TabularInline):
|
||||
model = FertilizationEntry
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(FertilizationPlan)
|
||||
class FertilizationPlanAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'year', 'variety']
|
||||
list_filter = ['year']
|
||||
inlines = [FertilizationEntryInline]
|
||||
7
backend/apps/fertilizer/apps.py
Normal file
7
backend/apps/fertilizer/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FertilizerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.fertilizer'
|
||||
verbose_name = '施肥計画'
|
||||
67
backend/apps/fertilizer/migrations/0001_initial.py
Normal file
67
backend/apps/fertilizer/migrations/0001_initial.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated by Django 5.0 on 2026-03-01 02:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('fields', '0006_e1c_chusankan_17_fields'),
|
||||
('plans', '0004_crop_base_temp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fertilizer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True, verbose_name='肥料名')),
|
||||
('maker', models.CharField(blank=True, max_length=100, null=True, verbose_name='メーカー')),
|
||||
('capacity_kg', models.DecimalField(blank=True, decimal_places=3, max_digits=8, null=True, verbose_name='1袋重量(kg)')),
|
||||
('nitrogen_pct', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='窒素含有率(%)')),
|
||||
('phosphorus_pct', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='リン酸含有率(%)')),
|
||||
('potassium_pct', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='カリ含有率(%)')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='備考')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '肥料マスタ',
|
||||
'verbose_name_plural': '肥料マスタ',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FertilizationPlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='計画名')),
|
||||
('year', models.IntegerField(verbose_name='年度')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('variety', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fertilization_plans', to='plans.variety', verbose_name='品種')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '施肥計画',
|
||||
'verbose_name_plural': '施肥計画',
|
||||
'ordering': ['-year', 'variety'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FertilizationEntry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('bags', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='袋数')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fields.field', verbose_name='圃場')),
|
||||
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='fertilizer.fertilizationplan')),
|
||||
('fertilizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fertilizer.fertilizer', verbose_name='肥料')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '施肥エントリ',
|
||||
'verbose_name_plural': '施肥エントリ',
|
||||
'ordering': ['field', 'fertilizer'],
|
||||
'unique_together': {('plan', 'field', 'fertilizer')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/apps/fertilizer/migrations/__init__.py
Normal file
0
backend/apps/fertilizer/migrations/__init__.py
Normal file
69
backend/apps/fertilizer/models.py
Normal file
69
backend/apps/fertilizer/models.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Fertilizer(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True, verbose_name='肥料名')
|
||||
maker = models.CharField(max_length=100, blank=True, null=True, verbose_name='メーカー')
|
||||
capacity_kg = models.DecimalField(
|
||||
max_digits=8, decimal_places=3, blank=True, null=True, verbose_name='1袋重量(kg)'
|
||||
)
|
||||
nitrogen_pct = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, blank=True, null=True, verbose_name='窒素含有率(%)'
|
||||
)
|
||||
phosphorus_pct = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, blank=True, null=True, verbose_name='リン酸含有率(%)'
|
||||
)
|
||||
potassium_pct = models.DecimalField(
|
||||
max_digits=5, decimal_places=2, blank=True, null=True, verbose_name='カリ含有率(%)'
|
||||
)
|
||||
notes = models.TextField(blank=True, null=True, verbose_name='備考')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '肥料マスタ'
|
||||
verbose_name_plural = '肥料マスタ'
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class FertilizationPlan(models.Model):
|
||||
name = models.CharField(max_length=200, verbose_name='計画名')
|
||||
year = models.IntegerField(verbose_name='年度')
|
||||
variety = models.ForeignKey(
|
||||
'plans.Variety', on_delete=models.PROTECT,
|
||||
related_name='fertilization_plans', verbose_name='品種'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '施肥計画'
|
||||
verbose_name_plural = '施肥計画'
|
||||
ordering = ['-year', 'variety']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.year} {self.name}"
|
||||
|
||||
|
||||
class FertilizationEntry(models.Model):
|
||||
"""圃場 × 肥料 × 袋数 の中間テーブル"""
|
||||
plan = models.ForeignKey(
|
||||
FertilizationPlan, on_delete=models.CASCADE, related_name='entries'
|
||||
)
|
||||
field = models.ForeignKey(
|
||||
'fields.Field', on_delete=models.CASCADE, verbose_name='圃場'
|
||||
)
|
||||
fertilizer = models.ForeignKey(
|
||||
Fertilizer, on_delete=models.CASCADE, verbose_name='肥料'
|
||||
)
|
||||
bags = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='袋数')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '施肥エントリ'
|
||||
verbose_name_plural = '施肥エントリ'
|
||||
unique_together = [['plan', 'field', 'fertilizer']]
|
||||
ordering = ['field', 'fertilizer']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.plan} / {self.field} / {self.fertilizer}: {self.bags}袋"
|
||||
81
backend/apps/fertilizer/serializers.py
Normal file
81
backend/apps/fertilizer/serializers.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Fertilizer, FertilizationPlan, FertilizationEntry
|
||||
|
||||
|
||||
class FertilizerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Fertilizer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class FertilizationEntrySerializer(serializers.ModelSerializer):
|
||||
field_name = serializers.CharField(source='field.name', read_only=True)
|
||||
field_area_tan = serializers.DecimalField(
|
||||
source='field.area_tan', max_digits=6, decimal_places=4, read_only=True
|
||||
)
|
||||
fertilizer_name = serializers.CharField(source='fertilizer.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FertilizationEntry
|
||||
fields = ['id', 'field', 'field_name', 'field_area_tan', 'fertilizer', 'fertilizer_name', 'bags']
|
||||
|
||||
|
||||
class FertilizationPlanSerializer(serializers.ModelSerializer):
|
||||
variety_name = serializers.SerializerMethodField()
|
||||
crop_name = serializers.SerializerMethodField()
|
||||
entries = FertilizationEntrySerializer(many=True, read_only=True)
|
||||
field_count = serializers.SerializerMethodField()
|
||||
fertilizer_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FertilizationPlan
|
||||
fields = [
|
||||
'id', 'name', 'year', 'variety', 'variety_name', 'crop_name',
|
||||
'entries', 'field_count', 'fertilizer_count', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_variety_name(self, obj):
|
||||
return obj.variety.name
|
||||
|
||||
def get_crop_name(self, obj):
|
||||
return obj.variety.crop.name
|
||||
|
||||
def get_field_count(self, obj):
|
||||
return obj.entries.values('field').distinct().count()
|
||||
|
||||
def get_fertilizer_count(self, obj):
|
||||
return obj.entries.values('fertilizer').distinct().count()
|
||||
|
||||
|
||||
class FertilizationPlanWriteSerializer(serializers.ModelSerializer):
|
||||
"""保存用(entries を一括で受け取る)"""
|
||||
entries = serializers.ListField(child=serializers.DictField(), write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = FertilizationPlan
|
||||
fields = ['id', 'name', 'year', 'variety', 'entries']
|
||||
|
||||
def create(self, validated_data):
|
||||
entries_data = validated_data.pop('entries', [])
|
||||
plan = FertilizationPlan.objects.create(**validated_data)
|
||||
self._save_entries(plan, entries_data)
|
||||
return plan
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
entries_data = validated_data.pop('entries', None)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
if entries_data is not None:
|
||||
instance.entries.all().delete()
|
||||
self._save_entries(instance, entries_data)
|
||||
return instance
|
||||
|
||||
def _save_entries(self, plan, entries_data):
|
||||
for entry in entries_data:
|
||||
FertilizationEntry.objects.create(
|
||||
plan=plan,
|
||||
field_id=entry['field_id'],
|
||||
fertilizer_id=entry['fertilizer_id'],
|
||||
bags=entry['bags'],
|
||||
)
|
||||
58
backend/apps/fertilizer/templates/fertilizer/pdf.html
Normal file
58
backend/apps/fertilizer/templates/fertilizer/pdf.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page { size: A4 landscape; margin: 15mm; }
|
||||
body { font-family: "Noto Sans CJK JP", "Hiragino Kaku Gothic Pro", sans-serif; font-size: 10pt; }
|
||||
h1 { font-size: 14pt; text-align: center; margin-bottom: 4px; }
|
||||
.subtitle { text-align: center; font-size: 10pt; color: #555; margin-bottom: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
||||
th, td { border: 1px solid #888; padding: 4px 6px; text-align: right; }
|
||||
th { background: #e8f5e9; text-align: center; }
|
||||
.col-name { text-align: left; }
|
||||
.col-area { text-align: right; }
|
||||
tr.total-row { font-weight: bold; background: #f5f5f5; }
|
||||
.zero { color: #bbb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>施肥計画書</h1>
|
||||
<p class="subtitle">{{ plan.year }}年度 {{ plan.variety.crop.name }} / {{ plan.variety.name }} 「{{ plan.name }}」</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-name">圃場名</th>
|
||||
<th class="col-area">面積(反)</th>
|
||||
{% for fert in fertilizers %}
|
||||
<th>{{ fert.name }}<br><small>(袋)</small></th>
|
||||
{% endfor %}
|
||||
<th>合計袋数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td class="col-name">{{ row.field.name }}</td>
|
||||
<td class="col-area">{{ row.field.area_tan }}</td>
|
||||
{% for cell in row.cells %}
|
||||
<td>{% if cell %}{{ cell }}{% else %}<span class="zero">-</span>{% endif %}</td>
|
||||
{% endfor %}
|
||||
<td>{{ row.total }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="total-row">
|
||||
<td class="col-name">合計</td>
|
||||
<td></td>
|
||||
{% for total in fert_totals %}
|
||||
<td>{{ total }}</td>
|
||||
{% endfor %}
|
||||
<td>{{ grand_total }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
13
backend/apps/fertilizer/urls.py
Normal file
13
backend/apps/fertilizer/urls.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'fertilizers', views.FertilizerViewSet, basename='fertilizer')
|
||||
router.register(r'plans', views.FertilizationPlanViewSet, basename='fertilization-plan')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
path('candidate_fields/', views.CandidateFieldsView.as_view(), name='candidate-fields'),
|
||||
path('calculate/', views.CalculateView.as_view(), name='fertilizer-calculate'),
|
||||
]
|
||||
196
backend/apps/fertilizer/views.py
Normal file
196
backend/apps/fertilizer/views.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
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.plans.models import Plan, Variety
|
||||
from .models import Fertilizer, FertilizationPlan
|
||||
from .serializers import (
|
||||
FertilizerSerializer,
|
||||
FertilizationPlanSerializer,
|
||||
FertilizationPlanWriteSerializer,
|
||||
)
|
||||
|
||||
|
||||
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'
|
||||
)
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user