A-6 完了。 本セッションの進捗まとめ:

タスク	内容	状態
A-3	前年度コピーボタン	 完了
A-4	品種のインライン追加・削除	 完了
A-5	PDFプレビュー機能	 完了
A-6	エクスポート機能	 完了
残りタスク:

A-2: チェックボックス・一括操作
A-1: ダッシュボード画面
A-7: 検索・フィルタ
確認ポイント:

作付け計画 (/allocation): 年度セレクタの横に「前年度コピー」「品種管理」ボタン、品種セレクトに「+ 新しい品種を追加...」
帳票出力 (/reports): 各帳票にプレビュー/ダウンロードの2ボタン
データ取込 (/import): ページ下部に「データエクスポート」(ZIPダウンロード)
This commit is contained in:
Akira
2026-02-19 12:21:17 +09:00
parent 23cb4d3118
commit 8b5e0fc66e
9 changed files with 497 additions and 128 deletions

View File

@@ -9,6 +9,7 @@ urlpatterns = [
path('import/kyosai/', views.import_kyosai_master, name='import_kyosai'),
path('import/yoshida/', views.import_yoshida_fields, name='import_yoshida'),
path('import/chusankan/', views.import_chusankan_master, name='import_chusankan'),
path('export/zip/', views.export_all_zip, name='export_all_zip'),
path('<int:field_id>/kyosai-links/', views.add_kyosai_links, name='add_kyosai_links'),
path('<int:field_id>/kyosai-links/<int:kyosai_id>/', views.remove_kyosai_link, name='remove_kyosai_link'),
path('<int:field_id>/chusankan-links/', views.add_chusankan_links, name='add_chusankan_links'),

View File

@@ -1,6 +1,10 @@
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
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
@@ -298,3 +302,79 @@ def import_chusankan_master(request):
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

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0 on 2026-02-19 03:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('plans', '0002_alter_plan_variety'),
]
operations = [
migrations.AlterField(
model_name='plan',
name='variety',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plans', to='plans.variety', verbose_name='品種'),
),
]

View File

@@ -30,7 +30,7 @@ class Plan(models.Model):
field = models.ForeignKey(Field, on_delete=models.CASCADE, related_name='plans', verbose_name="圃場")
year = models.IntegerField(verbose_name="作付年度")
crop = models.ForeignKey(Crop, on_delete=models.CASCADE, related_name='plans', verbose_name="作物")
variety = models.ForeignKey(Variety, on_delete=models.CASCADE, related_name='plans', verbose_name="品種", blank=True, null=True)
variety = models.ForeignKey(Variety, on_delete=models.SET_NULL, related_name='plans', verbose_name="品種", blank=True, null=True)
notes = models.TextField(blank=True, null=True, verbose_name="備考")
class Meta: