タスク 内容 状態 A-3 前年度コピーボタン ✅ 完了 A-4 品種のインライン追加・削除 ✅ 完了 A-5 PDFプレビュー機能 ✅ 完了 A-6 エクスポート機能 ✅ 完了 残りタスク: A-2: チェックボックス・一括操作 A-1: ダッシュボード画面 A-7: 検索・フィルタ 確認ポイント: 作付け計画 (/allocation): 年度セレクタの横に「前年度コピー」「品種管理」ボタン、品種セレクトに「+ 新しい品種を追加...」 帳票出力 (/reports): 各帳票にプレビュー/ダウンロードの2ボタン データ取込 (/import): ページ下部に「データエクスポート」(ZIPダウンロード)
381 lines
15 KiB
Python
381 lines
15 KiB
Python
import csv
|
|
import io
|
|
import json
|
|
import zipfile
|
|
import pandas as pd
|
|
from django.db import models as django_models
|
|
from django.http import JsonResponse, HttpResponse
|
|
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)
|
|
|
|
|
|
@api_view(['GET'])
|
|
@perm_classes([permissions.IsAuthenticated])
|
|
def export_all_zip(request):
|
|
"""全データをCSV形式でZIPアーカイブとしてエクスポート"""
|
|
from apps.plans.models import Plan, Crop, Variety
|
|
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
# 1. Fields CSV
|
|
fields_csv = io.StringIO()
|
|
w = csv.writer(fields_csv)
|
|
w.writerow(['id', 'name', 'address', 'area_tan', 'area_m2', 'owner_name', 'group_name', 'display_order'])
|
|
for f in Field.objects.all().order_by('id'):
|
|
w.writerow([f.id, f.name, f.address, f.area_tan, f.area_m2, f.owner_name, f.group_name or '', f.display_order or 0])
|
|
zf.writestr('fields.csv', fields_csv.getvalue())
|
|
|
|
# 2. Kyosai CSV
|
|
kyosai_csv = io.StringIO()
|
|
w = csv.writer(kyosai_csv)
|
|
w.writerow(['id', 'k_num', 's_num', 'address', 'kanji_name', 'area'])
|
|
for k in OfficialKyosaiField.objects.all().order_by('id'):
|
|
w.writerow([k.id, k.k_num, k.s_num, k.address, k.kanji_name, k.area])
|
|
zf.writestr('kyosai_fields.csv', kyosai_csv.getvalue())
|
|
|
|
# 3. Chusankan CSV
|
|
chusankan_csv = io.StringIO()
|
|
w = csv.writer(chusankan_csv)
|
|
w.writerow(['id', 'c_id', 'chusankan_flag', 'oaza', 'aza', 'chiban', 'branch_num',
|
|
'land_type', 'area', 'planting_area', 'original_crop', 'manager', 'owner',
|
|
'slope', 'base_amount', 'steep_slope_addition', 'smart_agri_addition', 'payment_amount'])
|
|
for c in OfficialChusankanField.objects.all().order_by('id'):
|
|
w.writerow([c.id, c.c_id, c.chusankan_flag or '', c.oaza, c.aza, c.chiban,
|
|
c.branch_num or '', c.land_type or '', c.area, c.planting_area or '',
|
|
c.original_crop or '', c.manager or '', c.owner or '', c.slope or '',
|
|
c.base_amount or '', c.steep_slope_addition or '', c.smart_agri_addition or '',
|
|
c.payment_amount or ''])
|
|
zf.writestr('chusankan_fields.csv', chusankan_csv.getvalue())
|
|
|
|
# 4. Plans CSV
|
|
plans_csv = io.StringIO()
|
|
w = csv.writer(plans_csv)
|
|
w.writerow(['id', 'field_id', 'field_name', 'year', 'crop_id', 'crop_name', 'variety_id', 'variety_name', 'notes'])
|
|
for p in Plan.objects.select_related('field', 'crop', 'variety').all().order_by('year', 'field_id'):
|
|
w.writerow([p.id, p.field_id, p.field.name, p.year, p.crop_id, p.crop.name,
|
|
p.variety_id or '', p.variety.name if p.variety else '', p.notes or ''])
|
|
zf.writestr('plans.csv', plans_csv.getvalue())
|
|
|
|
# 5. Crops & Varieties CSV
|
|
crops_csv = io.StringIO()
|
|
w = csv.writer(crops_csv)
|
|
w.writerow(['crop_id', 'crop_name', 'variety_id', 'variety_name'])
|
|
for crop in Crop.objects.prefetch_related('varieties').all().order_by('id'):
|
|
if crop.varieties.count() == 0:
|
|
w.writerow([crop.id, crop.name, '', ''])
|
|
else:
|
|
for v in crop.varieties.all().order_by('id'):
|
|
w.writerow([crop.id, crop.name, v.id, v.name])
|
|
zf.writestr('crops_varieties.csv', crops_csv.getvalue())
|
|
|
|
# 6. M:N links (field_kyosai, field_chusankan)
|
|
links_csv = io.StringIO()
|
|
w = csv.writer(links_csv)
|
|
w.writerow(['field_id', 'field_name', 'link_type', 'linked_id'])
|
|
for f in Field.objects.prefetch_related('kyosai_fields', 'chusankan_fields').all().order_by('id'):
|
|
for k in f.kyosai_fields.all():
|
|
w.writerow([f.id, f.name, 'kyosai', k.id])
|
|
for c in f.chusankan_fields.all():
|
|
w.writerow([f.id, f.name, 'chusankan', c.id])
|
|
zf.writestr('field_links.csv', links_csv.getvalue())
|
|
|
|
buf.seek(0)
|
|
response = HttpResponse(buf.read(), content_type='application/zip')
|
|
response['Content-Disposition'] = 'attachment; filename="keinasystem_backup.zip"'
|
|
return response
|