Files
keinasystem/backend/apps/fields/views.py
Akira 64e7701456 実装サマリー
バックエンド(3ファイル変更)
ファイル	変更内容
views.py	OfficialKyosaiFieldViewSet、OfficialChusankanFieldViewSet(ReadOnly)、紐づけ追加/解除の4つのAPIビューを追加
urls.py	紐づけ管理用の4パス追加
serializers.py	linked_field_namesフィールドを追加(紐づけ先の圃場名を返す)
keinasystem/urls.py	/api/kyosai-fields/、/api/chusankan-fields/ をルーターに登録
新規API一覧
メソッド	エンドポイント	動作確認
GET	/api/kyosai-fields/	31件返却
GET	/api/chusankan-fields/	71件返却
POST	/api/fields/{id}/kyosai-links/	{"added":1}
DELETE	/api/fields/{id}/kyosai-links/{kyosai_id}/	204
POST	/api/fields/{id}/chusankan-links/	同上
DELETE	/api/fields/{id}/chusankan-links/{chusankan_id}/	同上
フロントエンド(3ファイル変更)
ファイル	変更内容
types/index.ts	linked_field_namesプロパティ追加
fields/[id]/page.tsx	紐づけ管理UI全面実装(+追加ボタン、x解除ボタン、検索付きモーダル、面積参考表示)
fields/page.tsx	「共済」「中山間」紐づけ件数列を追加
http://localhost:3000/fields/4 などで圃場詳細画面を開いて動作確認できます。
2026-02-18 14:02:40 +09:00

301 lines
11 KiB
Python

import pandas as pd
from django.db import models as django_models
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from rest_framework import viewsets, permissions, filters, status
from rest_framework.decorators import action, api_view, permission_classes as perm_classes
from rest_framework.response import Response
from .models import OfficialKyosaiField, OfficialChusankanField, Field
from .serializers import FieldSerializer, OfficialKyosaiFieldSerializer, OfficialChusankanFieldSerializer
class FieldViewSet(viewsets.ModelViewSet):
queryset = Field.objects.all().order_by(
django_models.functions.Coalesce('group_name', django_models.Value('')),
'display_order',
'id'
)
serializer_class = FieldSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [filters.OrderingFilter]
ordering_fields = ['group_name', 'display_order', 'id', 'area_tan']
class OfficialKyosaiFieldViewSet(viewsets.ReadOnlyModelViewSet):
queryset = OfficialKyosaiField.objects.all().order_by('k_num', 's_num')
serializer_class = OfficialKyosaiFieldSerializer
permission_classes = [permissions.IsAuthenticated]
class OfficialChusankanFieldViewSet(viewsets.ReadOnlyModelViewSet):
queryset = OfficialChusankanField.objects.all().order_by('c_id')
serializer_class = OfficialChusankanFieldSerializer
permission_classes = [permissions.IsAuthenticated]
@api_view(['POST'])
@perm_classes([permissions.IsAuthenticated])
def add_kyosai_links(request, field_id):
field = get_object_or_404(Field, pk=field_id)
ids = request.data.get('kyosai_field_ids', [])
records = OfficialKyosaiField.objects.filter(id__in=ids)
field.kyosai_fields.add(*records)
return Response({'added': len(records)})
@api_view(['DELETE'])
@perm_classes([permissions.IsAuthenticated])
def remove_kyosai_link(request, field_id, kyosai_id):
field = get_object_or_404(Field, pk=field_id)
kyosai = get_object_or_404(OfficialKyosaiField, pk=kyosai_id)
field.kyosai_fields.remove(kyosai)
return Response(status=status.HTTP_204_NO_CONTENT)
@api_view(['POST'])
@perm_classes([permissions.IsAuthenticated])
def add_chusankan_links(request, field_id):
field = get_object_or_404(Field, pk=field_id)
ids = request.data.get('chusankan_field_ids', [])
records = OfficialChusankanField.objects.filter(id__in=ids)
field.chusankan_fields.add(*records)
return Response({'added': len(records)})
@api_view(['DELETE'])
@perm_classes([permissions.IsAuthenticated])
def remove_chusankan_link(request, field_id, chusankan_id):
field = get_object_or_404(Field, pk=field_id)
chusankan = get_object_or_404(OfficialChusankanField, pk=chusankan_id)
field.chusankan_fields.remove(chusankan)
return Response(status=status.HTTP_204_NO_CONTENT)
@csrf_exempt
@require_http_methods(["POST"])
def import_kyosai_master(request):
if 'file' not in request.FILES:
return JsonResponse({'error': 'No file uploaded'}, status=400)
ods_file = request.FILES['file']
try:
df = pd.read_excel(ods_file, engine='odf')
df.columns = df.columns.str.strip()
# ODS カラム名 "本地面積 (m2)" のスペース有無に対応
area_col = None
for col in df.columns:
if '本地面積' in col:
area_col = col
break
created_count = 0
updated_count = 0
for _, row in df.iterrows():
k_num = str(row.get('耕地番号', '')).strip() if pd.notna(row.get('耕地番号')) else ''
s_num = str(row.get('分筆番号', '')).strip() if pd.notna(row.get('分筆番号')) else ''
if not k_num:
continue
area_val = 0
if area_col and pd.notna(row.get(area_col)):
# ODS の値はアール(a)単位。m2 に変換 (1a = 100m2)
area_val = int(float(row.get(area_col)) * 100)
defaults = {
'address': str(row.get('地名 地番', '')).strip() if pd.notna(row.get('地名 地番')) else '',
'kanji_name': str(row.get('漢字地名', '')).strip() if pd.notna(row.get('漢字地名')) else '',
'area': area_val,
}
obj, created = OfficialKyosaiField.objects.update_or_create(
k_num=k_num,
s_num=s_num,
defaults=defaults
)
if created:
created_count += 1
else:
updated_count += 1
return JsonResponse({
'success': True,
'created': created_count,
'updated': updated_count,
'message': f'共済マスタ: {created_count}件作成, {updated_count}件更新'
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@csrf_exempt
@require_http_methods(["POST"])
def import_yoshida_fields(request):
if 'file' not in request.FILES:
return JsonResponse({'error': 'No file uploaded'}, status=400)
ods_file = request.FILES['file']
try:
df = pd.read_excel(ods_file, engine='odf')
df.columns = df.columns.str.strip()
created_count = 0
updated_count = 0
def clean_int_str(val):
if pd.isna(val):
return None
s = str(val).strip()
if not s:
return None
if s.endswith('.0'):
s = s[:-2]
return s
for _, row in df.iterrows():
name = str(row.get('名称', '')).strip() if pd.notna(row.get('名称')) else ''
if not name:
continue
raw_kyosai_k = clean_int_str(row.get('細目_耕地番号'))
raw_kyosai_s = clean_int_str(row.get('細目_分筆番号'))
raw_chusankan_str = clean_int_str(row.get('中山間_ID'))
raw_chusankan_ids = []
if raw_chusankan_str:
raw_chusankan_ids = [cid.strip() for cid in raw_chusankan_str.split(',') if cid.strip()]
area_tan = float(row.get('面積(反)', 0)) if pd.notna(row.get('面積(反)')) else 0
area_m2 = int(area_tan * 1000) if area_tan else 0
defaults = {
'address': str(row.get('住所', '')).strip() if pd.notna(row.get('住所')) else '',
'area_tan': area_tan,
'area_m2': area_m2,
'owner_name': str(row.get('地主', '')).strip() if pd.notna(row.get('地主')) else '',
'raw_kyosai_k_num': raw_kyosai_k,
'raw_kyosai_s_num': raw_kyosai_s,
'raw_chusankan_id': raw_chusankan_str,
}
field, created = Field.objects.update_or_create(
name=name,
defaults=defaults
)
if created:
created_count += 1
else:
updated_count += 1
if raw_kyosai_k and raw_kyosai_s:
try:
kyosai_record = OfficialKyosaiField.objects.get(k_num=raw_kyosai_k, s_num=raw_kyosai_s)
field.kyosai_fields.add(kyosai_record)
except OfficialKyosaiField.DoesNotExist:
pass
except OfficialKyosaiField.MultipleObjectsReturned:
pass
for cid in raw_chusankan_ids:
if not cid:
continue
try:
chusankan_record = OfficialChusankanField.objects.get(c_id=cid)
field.chusankan_fields.add(chusankan_record)
except OfficialChusankanField.DoesNotExist:
pass
return JsonResponse({
'success': True,
'created': created_count,
'updated': updated_count,
'message': f'実圃場: {created_count}件作成, {updated_count}件更新'
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@csrf_exempt
@require_http_methods(["POST"])
def import_chusankan_master(request):
if 'file' not in request.FILES:
return JsonResponse({'error': 'No file uploaded'}, status=400)
ods_file = request.FILES['file']
def safe_str(val, default=''):
return str(val).strip() if pd.notna(val) else default
def safe_int(val, default=None):
if pd.isna(val):
return default
try:
return int(float(val))
except (ValueError, TypeError):
return default
try:
df = pd.read_excel(ods_file, engine='odf')
df.columns = df.columns.str.strip()
created_count = 0
updated_count = 0
for _, row in df.iterrows():
raw_id = row.get('ID')
c_id = str(raw_id).strip() if pd.notna(raw_id) else ''
if not c_id:
continue
if not any(char.isdigit() for char in c_id):
continue
defaults = {
'chusankan_flag': safe_str(row.get('中山間')) or None,
'oaza': safe_str(row.get('大字')),
'aza': safe_str(row.get('')),
'chiban': safe_str(row.get('地番')),
'branch_num': safe_str(row.get('枝番')) or None,
'land_type': safe_str(row.get('地目')) or None,
'area': safe_int(row.get('農地面積'), 0),
'planting_area': safe_int(row.get('植栽面積')),
'original_crop': safe_str(row.get('作付け品目')) or None,
'manager': safe_str(row.get('協定管理者')) or None,
'owner': safe_str(row.get('所有者')) or None,
'slope': safe_str(row.get('傾斜度')) or None,
'base_amount': safe_int(row.get('基本金額')),
'steep_slope_addition': safe_int(row.get('超急傾斜加算額')),
'smart_agri_addition': safe_int(row.get('スマート農業加算額')),
'payment_amount': safe_int(row.get('交付金額')),
}
obj, created = OfficialChusankanField.objects.update_or_create(
c_id=c_id,
defaults=defaults
)
if created:
created_count += 1
else:
updated_count += 1
return JsonResponse({
'success': True,
'created': created_count,
'updated': updated_count,
'message': f'中山間マスタ: {created_count}件作成, {updated_count}件更新'
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)