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