diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..24222d0 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(docker-compose exec:*)", + "Bash(python -c:*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index cb0f8ad..437350e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,8 +70,7 @@ keinasystem_t02/ │ │ └── urls.py │ ├── plans/ # 作付け計画アプリ │ │ ├── models.py # Plan, Crop, Variety -│ │ ├── views.py # 作付け計画API、集計API -│ │ └── management/commands/init_crops.py # 初期データ投入 +│ │ └── views.py # 作付け計画API、集計API │ └── reports/ # 申請書生成アプリ │ ├── views.py # PDF生成API │ └── templates/ # PDF用HTMLテンプレート @@ -102,7 +101,12 @@ OfficialKyosaiField (共済マスタ) └── 31区画(水稲共済細目書用) OfficialChusankanField (中山間マスタ) -└── 71区画(中山間地域等直接支払交付金用) +├── 71区画(中山間地域等直接支払交付金用) +└── 17フィールド: 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 Plan (作付け計画) ├── field (FK to Field) @@ -201,6 +205,27 @@ Variety (品種マスタ) 2. **エラーハンドリング**: フロントエンドでの統一的なエラー表示が未実装 3. **テスト**: 自動テストが未実装(Phase 2で追加予定) 4. **パフォーマンス**: N+1問題が一部存在(現状は問題ないが、データ増加時に対応必要) +5. **セキュリティ**: DEFAULT_PERMISSION_CLASSES が AllowAny → IsAuthenticated に変更必要 +6. **settings.py**: LANGUAGE_CODE と TIME_ZONE が二重定義されている(前の定義を削除) +7. **PDF生成バグ**: reports/views.py で variety が null 時にクラッシュ(null チェック未実装) +8. **init_crops.py**: 不正データを含むため削除予定 +9. **PDF帳票**: 現在のテンプレートは仕様と不一致(中国語混入、セクション形式、@page未設定)→ 再設計必要 +10. **中山間モデル**: 現在6フィールド → 17フィールドに拡張必要(E-1c) + +### 🔜 次の実装タスク(優先順) + +1. **A-8**: 圃場詳細に共済/中山間情報表示(最優先) +2. **D-1〜D-4**: バグ修正(null crash, settings二重定義, AllowAny→IsAuthenticated, init_crops削除) +3. **E-1**: PDF帳票フォーマット再設計(中山間モデル拡張含む) +4. **A-3**: 前年度コピーボタン(Frontend) +5. **A-4**: 品種のインライン追加・削除 +6. **A-5**: PDFプレビュー機能 +7. **A-6**: エクスポート機能(サーバー移行時のデータ移動用) +8. **A-2**: チェックボックス・一括操作 +9. **A-1**: ダッシュボード画面 +10. **A-7**: 検索・フィルタ + +詳細は `document/06_ドキュメントvs実装_差異レポート.md` を参照 ### 📅 次のマイルストーン(Phase 2) @@ -273,6 +298,7 @@ docker-compose exec backend python manage.py migrate - **データモデル詳細**: `document/03_データ仕様書.md` - **画面設計**: `document/04_画面設計書.md` - **実装手順**: `document/00_Gemini向け統合指示書.md` +- **差異レポート・タスク一覧**: `document/06_ドキュメントvs実装_差異レポート.md` --- @@ -288,4 +314,5 @@ docker-compose exec backend python manage.py migrate ## 📝 更新履歴 +- 2026-02-17: ドキュメント一斉更新(差異レポートA〜E反映、CSV→PDF統一、M:N関係、中山間モデル17列化、インライン編集方式、Navbar追加、既知の課題・次タスク一覧追加) - 2026-02-16: 初版作成(ハイブリッドアプローチの方針決定) diff --git a/backend/apps/plans/management/commands/init_crops.py b/backend/apps/plans/management/commands/init_crops.py deleted file mode 100644 index ade6e90..0000000 --- a/backend/apps/plans/management/commands/init_crops.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.core.management.base import BaseCommand -from apps.plans.models import Crop, Variety - - -class Command(BaseCommand): - help = 'Initialize crops and varieties master data' - - def handle(self, *args, **options): - crops_data = [ - { - 'name': '水稲', - 'varieties': ['コシヒカリ', 'ひとめぼれ', 'あきたこまち', 'つや姫', 'oniai'] - }, - { - 'name': '大豆', - 'varieties': ['タマホマレ', 'エンレイ', 'ミヤギром'] - }, - { - 'name': '小麦', - 'varieties': ['キタノカオリ', 'ホウライ'] - }, - { - 'name': 'そば', - 'varieties': ['信濃一号', 'はるか'] - }, - { - 'name': 'とうきび', - 'varieties': ['ゴールdent'] - }, - ] - - for crop_data in crops_data: - crop, _ = Crop.objects.get_or_create(name=crop_data['name']) - for variety_name in crop_data['varieties']: - Variety.objects.get_or_create(crop=crop, name=variety_name) - self.stdout.write(f'{crop.name}: {len(crop_data["varieties"])} varieties') - - self.stdout.write(self.style.SUCCESS('Successfully initialized crops and varieties')) diff --git a/backend/apps/reports/views.py b/backend/apps/reports/views.py index 5ef7cbd..0d2669c 100644 --- a/backend/apps/reports/views.py +++ b/backend/apps/reports/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render from django.template.loader import render_to_string from django.http import HttpResponse from weasyprint import HTML @@ -8,25 +7,25 @@ from apps.plans.models import Plan def generate_kyosai_pdf(request, year): kyosai_fields = OfficialKyosaiField.objects.all() - + data = [] for kyosai in kyosai_fields: related_fields = kyosai.fields.all() plans = Plan.objects.filter(field__in=related_fields, year=year) - + crops = {} total_area = 0 for plan in plans: - crop_name = plan.crop.name + crop_name = plan.crop.name if plan.crop else '未設定' if crop_name not in crops: crops[crop_name] = { 'name': crop_name, - 'variety': plan.variety.name, + 'variety': plan.variety.name if plan.variety else '', 'count': 0 } crops[crop_name]['count'] += 1 total_area += float(plan.field.area_tan) - + data.append({ 'kyosai': kyosai, 'fields': related_fields, @@ -34,14 +33,14 @@ def generate_kyosai_pdf(request, year): 'total_area': total_area, 'field_count': related_fields.count() }) - + html_string = render_to_string('reports/kyosai_template.html', { 'year': year, 'data': data }) - + pdf = HTML(string=html_string).write_pdf() - + response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="kyosai_{year}.pdf"' return response @@ -49,25 +48,25 @@ def generate_kyosai_pdf(request, year): def generate_chusankan_pdf(request, year): chusankan_fields = OfficialChusankanField.objects.all() - + data = [] for chusankan in chusankan_fields: related_fields = chusankan.fields.all() plans = Plan.objects.filter(field__in=related_fields, year=year) - + crops = {} total_area = 0 for plan in plans: - crop_name = plan.crop.name + crop_name = plan.crop.name if plan.crop else '未設定' if crop_name not in crops: crops[crop_name] = { 'name': crop_name, - 'variety': plan.variety.name, + 'variety': plan.variety.name if plan.variety else '', 'count': 0 } crops[crop_name]['count'] += 1 total_area += float(plan.field.area_tan) - + data.append({ 'chusankan': chusankan, 'fields': related_fields, @@ -75,14 +74,14 @@ def generate_chusankan_pdf(request, year): 'total_area': total_area, 'field_count': related_fields.count() }) - + html_string = render_to_string('reports/chusankan_template.html', { 'year': year, 'data': data }) - + pdf = HTML(string=html_string).write_pdf() - + response = HttpResponse(pdf, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="chusankan_{year}.pdf"' return response diff --git a/backend/keinasystem/settings.py b/backend/keinasystem/settings.py index 677fca0..7d1d492 100644 --- a/backend/keinasystem/settings.py +++ b/backend/keinasystem/settings.py @@ -110,9 +110,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'ja' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Tokyo' USE_I18N = True @@ -134,7 +134,7 @@ REST_FRAMEWORK = { 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.AllowAny', + 'rest_framework.permissions.IsAuthenticated', ), } @@ -148,7 +148,3 @@ CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "http://127.0.0.1:3000", ] - -LANGUAGE_CODE = 'ja' - -TIME_ZONE = 'Asia/Tokyo' diff --git a/document/00_Gemini向け統合指示書.md b/document/00_Gemini向け統合指示書.md index 1fbc5fb..8d7a9ec 100644 --- a/document/00_Gemini向け統合指示書.md +++ b/document/00_Gemini向け統合指示書.md @@ -19,14 +19,14 @@ - PCで登録・編集、スマホで参照 ### 主要機能(Phase 1 / MVP) -1. 作付け計画の一覧表示・編集 -2. 水稲共済細目書のCSV出力 -3. 中山間交付金申請書のCSV出力 +1. 作付け計画の一覧表示・インライン編集 +2. 水稲共済細目書のPDF出力 +3. 中山間交付金申請書のPDF出力 4. 前年度作付けのコピー機能 5. 圃場情報のスマホ参照 ### 技術スタック -- **バックエンド**: Django 5.0 + Django REST Framework + GeoDjango +- **バックエンド**: Django 5.2 + Django REST Framework + GeoDjango - **フロントエンド**: Next.js 14 (App Router) + Tailwind CSS - **データベース**: PostgreSQL 16 + PostGIS 3.4 - **インフラ**: Docker Compose @@ -49,8 +49,8 @@ ### 3. データ仕様書.md - 3種類のデータ(実圃場、共済マスタ、中山間マスタ)の関係 -- 紐付けロジック(M:1関係)の詳細 -- 申請書CSV出力のアルゴリズム +- 紐付けロジック(M:N関係)の詳細 +- 申請書PDF出力のアルゴリズム - **👉 読むべき理由**: データモデルの設計ミスは後から修正困難 ### 4. 画面設計書.md @@ -149,7 +149,7 @@ volumes: #### backend/requirements.txt ```txt -Django==5.0 +Django==5.2 djangorestframework==3.14 django-cors-headers==4.3 psycopg2-binary==2.9 @@ -227,25 +227,36 @@ from django.contrib.gis.db import models class OfficialKyosaiField(models.Model): """共済マスタ(水稲共済細目用.ods)""" - k_num = models.IntegerField("耕地番号") - s_num = models.IntegerField("分筆番号") + k_num = models.CharField("耕地番号", max_length=20) + s_num = models.CharField("分筆番号", max_length=20, blank=True) address = models.CharField("地名地番", max_length=200) kanji_name = models.CharField("漢字地名", max_length=200) - area = models.FloatField("本地面積(m2)") - + area = models.IntegerField("本地面積(m2)") + class Meta: unique_together = [['k_num', 's_num']] ordering = ['k_num', 's_num'] class OfficialChusankanField(models.Model): - """中山間マスタ(中山間.ods)""" - c_id = models.IntegerField("ID", unique=True) + """中山間マスタ(中山間.ods)- 17列全て保存""" + c_id = models.CharField("ID", max_length=20, unique=True) + chusankan_flag = models.CharField("中山間", max_length=10, blank=True) oaza = models.CharField("大字", max_length=100) aza = models.CharField("字", max_length=100) - chiban = models.IntegerField("地番") + chiban = models.CharField("地番", max_length=50) + branch_num = models.CharField("枝番", max_length=20, blank=True) + land_type = models.CharField("地目", max_length=20, blank=True) area = models.IntegerField("農地面積(m2)") + planting_area = models.IntegerField("植栽面積(m2)", null=True, blank=True) + original_crop = models.CharField("作付け品目", max_length=100, blank=True) + manager = models.CharField("協定管理者", max_length=100, blank=True) + owner = models.CharField("所有者", max_length=100, blank=True) + slope = models.CharField("傾斜度", max_length=20, blank=True) + base_amount = models.IntegerField("基本金額", null=True, blank=True) + steep_slope_addition = models.IntegerField("超急傾斜加算額", null=True, blank=True) + smart_agri_addition = models.IntegerField("スマート農業加算額", null=True, blank=True) payment_amount = models.IntegerField("交付金額", null=True, blank=True) - + class Meta: ordering = ['c_id'] @@ -257,22 +268,19 @@ class Field(models.Model): area_m2 = models.IntegerField("面積(m2)") # area_tan * 1000 owner_name = models.CharField("地主", max_length=100) - # 紐付けキー(raw値) - raw_kyosai_k_num = models.IntegerField("細目_耕地番号") - raw_kyosai_s_num = models.IntegerField("細目_分筆番号") - raw_chusankan_id = models.IntegerField("中山間_ID", null=True, blank=True) - - # 外部キー(紐付け済み) - kyosai_field = models.ForeignKey( - OfficialKyosaiField, - on_delete=models.SET_NULL, - null=True, + # グループ・表示順 + group_name = models.CharField("グループ名", max_length=100, blank=True) + display_order = models.IntegerField("表示順", default=0) + + # M:N紐付け(1つの圃場が複数の申請区画に紐づく場合がある) + kyosai_fields = models.ManyToManyField( + OfficialKyosaiField, + blank=True, related_name='fields' ) - chusankan_field = models.ForeignKey( - OfficialChusankanField, - on_delete=models.SET_NULL, - null=True, + chusankan_fields = models.ManyToManyField( + OfficialChusankanField, + blank=True, related_name='fields' ) @@ -347,97 +355,52 @@ def import_kyosai_master(request): @api_view(['POST']) def import_yoshida_fields(request): - """吉田農地台帳のインポート(紐付け処理含む)""" + """吉田農地台帳のインポート(M:N紐付け処理含む)""" file = request.FILES['file'] df = pd.read_excel(file, engine='odf') - + for _, row in df.iterrows(): - # 共済マスタとの紐付け - try: - kyosai = OfficialKyosaiField.objects.get( - k_num=row['細目_耕地番号'], - s_num=row['細目_分筆番号'] - ) - except OfficialKyosaiField.DoesNotExist: - kyosai = None - - # 中山間マスタとの紐付け - chusankan = None - if pd.notna(row['中山間_ID']): - try: - chusankan = OfficialChusankanField.objects.get( - c_id=int(row['中山間_ID']) - ) - except OfficialChusankanField.DoesNotExist: - pass - # 実圃場を作成 - Field.objects.update_or_create( + field, created = Field.objects.update_or_create( name=row['名称'], defaults={ 'address': row['住所'], 'area_tan': row['面積(反)'], 'area_m2': int(row['面積(反)'] * 1000), 'owner_name': row['地主'], - 'raw_kyosai_k_num': row['細目_耕地番号'], - 'raw_kyosai_s_num': row['細目_分筆番号'], - 'raw_chusankan_id': int(row['中山間_ID']) if pd.notna(row['中山間_ID']) else None, - 'kyosai_field': kyosai, - 'chusankan_field': chusankan, } ) - + + # 共済マスタとのM:N紐付け + try: + kyosai = OfficialKyosaiField.objects.get( + k_num=str(row['細目_耕地番号']), + s_num=str(row['細目_分筆番号']) + ) + field.kyosai_fields.add(kyosai) + except OfficialKyosaiField.DoesNotExist: + pass + + # 中山間マスタとのM:N紐付け + if pd.notna(row.get('中山間_ID')): + try: + chusankan = OfficialChusankanField.objects.get( + c_id=str(int(row['中山間_ID'])) + ) + field.chusankan_fields.add(chusankan) + except OfficialChusankanField.DoesNotExist: + pass + return Response({'status': 'success', 'imported': len(df)}) ``` ### Step 5: 作付け計画API(Day 5) -#### 作物・品種の初期データ投入 +#### 作物・品種マスタについて -**apps/plans/management/commands/init_crops.py** -```python -from django.core.management.base import BaseCommand -from apps.plans.models import Crop, Variety +初期データの自動投入は行わない。作物・品種はDjango管理画面またはUIから手動で登録する運用とする。 -class Command(BaseCommand): - help = '作物・品種マスタの初期データを投入' - - def handle(self, *args, **options): - # 作物マスタと品種 - crops_data = { - '米': ['にこまる', 'たちはるか', 'たちはるか(特栽)'], - 'トウモロコシ': [], - 'エンドウ': ['久留米豊'], - '野菜': [], - 'その他': [ - '完全休耕', - '緑肥(ヘアリーベッチ)', - '緑肥(レンゲ)', - '景観作物(コスモス)', - '景観作物(ヒマワリ)' - ] - } - - for crop_name, varieties in crops_data.items(): - crop, created = Crop.objects.get_or_create(name=crop_name) - if created: - self.stdout.write(f'作物「{crop_name}」を作成') - - for variety_name in varieties: - variety, created = Variety.objects.get_or_create( - crop=crop, - name=variety_name - ) - if created: - self.stdout.write(f' 品種「{variety_name}」を追加') - - self.stdout.write(self.style.SUCCESS('初期データ投入完了')) -``` - -**実行:** -```bash -docker-compose exec backend python manage.py init_crops -``` +主な作物: 米、トウモロコシ、エンドウ、野菜、その他 #### apps/plans/views.py ```python @@ -921,23 +884,16 @@ export default function ReportsPage() { ## ⚠️ 重要な実装上の注意点 ### 1. データベース設計 -- **面積単位**: DB内部は全て `m2` で保存、表示時に `反` に変換 -- **紐付けキー**: `raw_*` フィールドと外部キー `*_field` の両方を持つ -- **ユニーク制約**: `(field, year)` で作付け計画は1つまで +- **面積単位**: DB内部は `area_m2`(IntegerField)で保存、表示用に `area_tan`(DecimalField)も保持。1反=1000m2 +- **紐付け**: Field ↔ OfficialKyosaiField、Field ↔ OfficialChusankanField は **M:N**(ManyToManyField) +- **ユニーク制約**: `(field, year)` で作付け計画は1つまで、`(k_num, s_num)` で共済区画は一意 - **品種マスタ**: `(crop, name)` で一意制約 ### 2. 作物・品種の統一 - **すべての作物で品種選択UIは統一**: 作物による操作の違いなし - **「作付けしない」系も特別扱いしない**: 「その他」という作物に統一 - **品種の追加**: その場で追加可能、データベースに永続化 -- **初期データ**: - ``` - 米: にこまる、たちはるか、たちはるか(特栽) - トウモロコシ: (ユーザーが追加) - エンドウ: 久留米豊 - 野菜: (ユーザーが追加) - その他: 完全休耕、緑肥(ヘアリーベッチ)、緑肥(レンゲ)、景観作物(コスモス)、景観作物(ヒマワリ) - ``` +- **初期データ**: なし(管理画面またはUIから登録する運用) ### 3. 集計サイドバー - **リアルタイム更新**: 作付け計画を保存するたびに自動更新 diff --git a/document/01_プロダクトビジョン.md b/document/01_プロダクトビジョン.md index 5e942e8..99b3822 100644 --- a/document/01_プロダクトビジョン.md +++ b/document/01_プロダクトビジョン.md @@ -13,7 +13,7 @@ 2. **実圃場と申請区画のずれを管理する** - 実際に作業する圃場(39筆)と、申請書上の区画(共済31区画、中山間71区画)が異なる - - 複数の実圃場が1つの申請区画に紐づく関係(M:1)を明示的に管理 + - 実圃場と申請区画の紐づき関係(M:N)を明示的に管理 - 紐付けは半自動化するが、手動修正も可能にする 3. **将来の拡張を見据えた設計** @@ -95,7 +95,7 @@ **パフォーマンス:** - 圃場一覧の表示: 1秒以内 -- 申請書CSVの生成: 3秒以内 +- 申請書PDFの生成: 3秒以内 - スマホでの圃場詳細表示: 2秒以内 --- @@ -140,7 +140,7 @@ **Phase 1(MVP): 2025年2月まで** - 作付け計画の登録・編集 -- 申請書(水稲共済・中山間)のCSV出力 +- 申請書(水稲共済・中山間)のPDF出力 - 圃場一覧の参照(PC/スマホ) **Phase 2: 2025年3月~** diff --git a/document/03_データ仕様書.md b/document/03_データ仕様書.md index d48319c..b2b596b 100644 --- a/document/03_データ仕様書.md +++ b/document/03_データ仕様書.md @@ -1,5 +1,8 @@ # データ仕様書 +> **最終更新**: 2026-02-16 +> **変更履歴**: M:N関係に更新、中山間モデル全17列対応、面積単位統一、帳票仕様追加 + ## 📊 データ構造の全体像 このシステムで扱うデータは3種類: @@ -9,53 +12,80 @@ 3. **中山間マスタ**(中山間.ods)- 申請書用の区画 **紐付けの関係:** -- 実圃場 → 共済区画: **M対1**(複数の実圃場が1つの共済区画に対応) -- 実圃場 → 中山間区画: **M対1**(複数の実圃場が1つの中山間区画に対応) +- 実圃場 ↔ 共済区画: **M対N**(複数の実圃場が1つの共済区画に対応、また1つの実圃場が複数の共済区画に対応するケースもある) +- 実圃場 ↔ 中山間区画: **M対N**(同上) ```mermaid erDiagram - 実圃場 }o--|| 共済区画 : "紐づく(M:1)" - 実圃場 }o--|| 中山間区画 : "紐づく(M:1)" + 実圃場 }o--o{ 共済区画 : "紐づく(M:N)" + 実圃場 }o--o{ 中山間区画 : "紐づく(M:N)" 実圃場 ||--o{ 作付け計画 : "持つ(1:N)" - + 作付け計画 }o--|| 作物 : "参照" + 作付け計画 }o--o| 品種 : "参照(任意)" + 作物 ||--o{ 品種 : "持つ" + 実圃場 { int id PK string 名称 string 住所 - float 面積_反 + decimal 面積_反 + int 面積_m2 string 地主 - int 細目_耕地番号 "共済紐付けキー" - int 細目_分筆番号 "共済紐付けキー" - int 中山間_ID "中山間紐付けキー" + string グループ名 + int 表示順 + string 細目_耕地番号 "共済紐付けキー(raw)" + string 細目_分筆番号 "共済紐付けキー(raw)" + string 中山間_ID "中山間紐付けキー(raw)" } - + 共済区画 { int id PK string 地名_地番 int 耕地番号 int 分筆番号 - float 本地面積_m2 + decimal 本地面積_m2 string 漢字地名 } - + 中山間区画 { int id PK - int ID + string 中山間ID + string 中山間フラグ string 大字 string 字 - int 地番 + string 地番 + string 枝番 + string 地目 int 農地面積_m2 + int 植栽面積_m2 + string 作付け品目_元 + string 協定管理者 + string 所有者 + string 傾斜度 + int 基本金額 + decimal 超急傾斜加算額 + decimal スマート農業加算額 int 交付金額 } - + 作付け計画 { int id PK int 実圃場_id FK int 年度 - string 作物 - string 品種 - date 播種日 - date 収穫日 + int 作物_id FK + int 品種_id FK_nullable + text 備考 + } + + 作物 { + int id PK + string 作物名 + } + + 品種 { + int id PK + int 作物_id FK + string 品種名 } ``` @@ -79,6 +109,24 @@ erDiagram | 細目_分筆番号 | int | ○ | 共済マスタとの紐付けキー(2/2) | 1 | | 中山間_ID | int | △ | 中山間マスタとの紐付けキー | 50 | +### DBモデル(Field) + +| フィールド名 | データ型 | 説明 | +|-------------|---------|------| +| name | CharField(100) | 圃場名 | +| address | CharField(255) | 住所 | +| area_tan | DecimalField(6,4) | 面積(反) | +| area_m2 | IntegerField | 面積(m2)= area_tan × 1000 | +| owner_name | CharField(100) | 所有者名 | +| group_name | CharField(50), nullable | グループ名(エリアや用途による分類) | +| display_order | IntegerField, default=0 | リスト表示時の順序 | +| raw_kyosai_k_num | CharField(20), nullable | 細目_耕地番号(インポート元の値) | +| raw_kyosai_s_num | CharField(20), nullable | 細目_分筆番号(インポート元の値) | +| raw_chusankan_id | CharField(20), nullable | 中山間_ID(インポート元の値) | +| kyosai_fields | ManyToManyField → OfficialKyosaiField | 関連共済マスタ(M:N) | +| chusankan_fields | ManyToManyField → OfficialChusankanField | 関連中山間マスタ(M:N) | +| location | PointField, nullable | 位置情報(Phase 1では未使用) | + ### データサンプル ``` 名称 住所 面積(反) 細目_耕地番号 細目_分筆番号 中山間_ID @@ -90,7 +138,9 @@ erDiagram ### 特記事項 - **中山間_IDは一部NULL**: 39筆中2筆が中山間の対象外(`NaN`) - **同じ共済区画に複数の実圃場**: 例えば共済キー「2-2」には3つの実圃場が紐づく -- **面積単位**: DB内部では「反」と「m2」の両方を保持する(変換: 1反=1000m2) +- **1つの実圃場が複数の申請区画に紐づくケースもある**: M:N関係で対応 +- **面積単位**: DB内部では「反(DecimalField)」と「m2(IntegerField)」の両方を保持する(変換: 1反=1000m2) +- **グループ機能**: group_name でエリア分け、display_order で表示順を制御 --- @@ -110,6 +160,18 @@ erDiagram | 本地面積 (m2) | float | ○ | 申請上の面積(単位: m2) | 25.4 | | 漢字地名 | string | ○ | 漢字表記の地名 | "四万十町 笹ヶ谷 374-1" | +### DBモデル(OfficialKyosaiField) + +| フィールド名 | データ型 | 説明 | +|-------------|---------|------| +| k_num | IntegerField | 耕地番号 | +| s_num | IntegerField | 分筆番号 | +| address | CharField(200) | 地名地番 | +| kanji_name | CharField(200) | 漢字地名 | +| area | IntegerField | 本地面積(m2) | + +**制約:** (k_num, s_num) のペアで一意(unique_together) + ### データサンプル ``` 地名 地番 耕地番号 分筆番号 本地面積(m2) 漢字地名 @@ -129,97 +191,114 @@ erDiagram ### ファイル情報 - **行数:** 71行(71区画) -- **列数:** 17列(うち使用するのは一部) +- **列数:** 17列(全列をDBに保存) -### カラム定義(主要なもの) +### カラム定義(全17列) | カラム名 | データ型 | 必須 | 説明 | 例 | |---------|---------|-----|------|---| -| ID | int | ○ | 中山間区画の識別子 | 50 | +| ID | int | ○ | 中山間区画の識別子 | 1 | +| 中山間 | string | ○ | 中山間フラグ | "〇" | | 大字 | string | ○ | 大字名 | "口神ノ川" | | 字 | string | ○ | 字名 | "壱町切" | -| 地番 | int | ○ | 地番 | 1694 | -| 農地面積 | int | ○ | 面積(単位: m2) | 2900 | -| 作付け品目 | string | △ | (役場が記入、システムでは上書き) | "ニラ" | -| 交付金額 | int | △ | 交付金額 | 37700 | +| 地番 | string | ○ | 地番(数値でないケースあり:イ、ロ等) | "1694" | +| 枝番 | string | △ | 枝番(-, 1, イ, ロ等) | "-" | +| 地目 | string | ○ | 地目 | "田" | +| 農地面積 | int | ○ | 農地面積(m2) | 2900 | +| 植栽面積 | int | ○ | 植栽面積(m2) | 2748 | +| 作付け品目 | string | △ | 役場が記入した作付け品目 | "ニラ" | +| 協定管理者 | string | ○ | 協定管理者名 | "神山倫子" | +| 所有者 | string | △ | 所有者名(NULLあり) | "谷脇史男" | +| 傾斜度 | string | ○ | 傾斜度 | "1/29" | +| 基本金額 | int | ○ | 基本金額(円) | 23200 | +| 超急傾斜加算額 | decimal | △ | 超急傾斜加算額(円) | 0.0 | +| スマート農業加算額 | decimal | △ | スマート農業加算額(円) | 14500 | +| 交付金額 | int | ○ | 交付金額合計(円) | 37700 | + +### DBモデル(OfficialChusankanField) + +| フィールド名 | データ型 | 説明 | +|-------------|---------|------| +| c_id | CharField(20), unique | 中山間ID | +| chusankan_flag | CharField(10), nullable | 中山間フラグ(〇等) | +| oaza | CharField(100) | 大字 | +| aza | CharField(100) | 字 | +| chiban | CharField(50) | 地番(文字列:イ、ロ等があるため) | +| branch_num | CharField(20), nullable | 枝番(-, 1, イ, ロ等) | +| land_type | CharField(20), nullable | 地目(田, 畑等) | +| area | IntegerField | 農地面積(m2) | +| planting_area | IntegerField, nullable | 植栽面積(m2) | +| original_crop | CharField(100), nullable | 作付け品目(役場記入の元データ) | +| manager | CharField(100), nullable | 協定管理者 | +| owner | CharField(100), nullable | 所有者 | +| slope | CharField(20), nullable | 傾斜度 | +| base_amount | IntegerField, nullable | 基本金額(円) | +| steep_slope_addition | DecimalField, nullable | 超急傾斜加算額(円) | +| smart_agri_addition | DecimalField, nullable | スマート農業加算額(円) | +| payment_amount | IntegerField, nullable | 交付金額(円) | ### データサンプル ``` -ID 大字 字 地番 農地面積 作付け品目 交付金額 -50 口神ノ川 笹ヶ谷 374 2698 米 xxxxxx +ID 中山間 大字 字 地番 枝番 地目 農地面積 植栽面積 作付け品目 協定管理者 所有者 傾斜度 基本金額 交付金額 +1 〇 口神ノ川 壱町切 1694 - 田 2900 2748 ニラ 神山倫子 1/29 23200 37700 +2 〇 口神ノ川 大窪 490 1 田 652 490 野菜 谷脇誠一 谷脇史男 1/20 15204 18824 ``` ### 特記事項 -- **使用する列は限定的**: システムでは主に「ID」「大字」「字」「地番」「農地面積」を使用 -- **作付け品目は上書き**: 役場が記入した「作付け品目」は参考情報で、システムで上書きする +- **全17列をDBに保存**: 将来どの列が必要になるかわからないため全保存 +- **地番・枝番は文字列型**: 「イ」「ロ」などの非数値データが入る +- **作付け品目は参考情報**: 役場が記入した値。システムの作付け計画(Plan)とは別 - **面積の不整合は許容**: 共済マスタと同様、実圃場との差異は受け入れる --- ## 4. 作付け計画データ(システム内部) -### テーブル定義 +### DBモデル(Plan) -| カラム名 | データ型 | 必須 | 説明 | -|---------|---------|-----|------| -| id | int | ○ | 主キー(自動採番) | -| field_id | int | ○ | 実圃場ID(外部キー) | -| year | int | ○ | 年度(2025など) | -| crop | string | ○ | 作物(「米」「トウモロコシ」など) | -| variety | string | △ | 品種(「にこまる」など) | -| planting_date | date | △ | 播種日/定植日(Phase 2) | -| harvest_date | date | △ | 収穫日(Phase 2) | -| notes | text | △ | 備考 | +| フィールド名 | データ型 | 必須 | 説明 | +|-------------|---------|-----|------| +| id | int (自動) | ○ | 主キー | +| field | FK → Field | ○ | 実圃場(外部キー) | +| year | IntegerField | ○ | 年度(2025など) | +| crop | FK → Crop, nullable | △ | 作物(外部キー) | +| variety | FK → Variety, nullable | △ | 品種(外部キー、NULLあり) | +| notes | TextField, nullable | △ | 備考 | ### 制約 -- **ユニーク制約**: (field_id, year) - 1つの圃場に対して1年度につき1つの作付け計画のみ +- **ユニーク制約**: (field, year) - 1つの圃場に対して1年度につき1つの作付け計画のみ - Phase 2で二毛作対応する場合は、この制約を見直す +### 備考 +- planting_date(播種日)、harvest_date(収穫日)は Phase 2 で追加予定 +- crop, variety は外部キー(文字列ではなくリレーション) + --- ## 5. 作物マスタ -### 作物リスト -- 米 -- トウモロコシ -- エンドウ -- 野菜 -- その他 +### DBモデル(Crop) -### 品種の登録方法 +| フィールド名 | データ型 | 説明 | +|-------------|---------|------| +| id | int (自動) | 主キー | +| name | CharField(50), unique | 作物名 | -**すべての作物で統一されたUI:** -- プリセット品種から選択 -- その場で新しい品種を追加可能 -- 作物による操作の違いなし +### DBモデル(Variety) -### プリセット品種の例 +| フィールド名 | データ型 | 説明 | +|-------------|---------|------| +| id | int (自動) | 主キー | +| crop | FK → Crop | 所属する作物 | +| name | CharField(100) | 品種名 | -#### 米 -- にこまる -- たちはるか -- たちはるか(特栽) +**制約:** (crop, name) のペアで一意 -#### トウモロコシ -- (ユーザーが追加) - -#### エンドウ -- 久留米豊 - -#### 野菜 -- (ユーザーが追加) - -#### その他 -- 完全休耕 -- 緑肥(ヘアリーベッチ) -- 緑肥(レンゲ) -- 景観作物(コスモス) -- 景観作物(ヒマワリ) - -### 実装上の注意 -- すべての作物で `Variety` テーブルに品種を登録 -- 「作付けしない」系を特別扱いしない -- UIは完全に統一 +### 作物・品種の管理方針 +- 初期データは投入しない(管理画面またはUIから登録) +- すべての作物で品種選択UIは統一 +- 「作付けしない」系も特別扱いしない(「その他」作物の品種として扱う) +- 品種の追加・削除は作付け計画画面から可能 --- @@ -233,31 +312,31 @@ ID 大字 字 地番 農地面積 作付け品目 交付金額 2. **中山間マスタのインポート** - `中山間.ods` を読み込み - - `OfficialChusankanField` テーブルに保存 + - `OfficialChusankanField` テーブルに全17列を保存 3. **実圃場データのインポート** - `吉田農地台帳.ods` を読み込み - `Field` テーブルに保存 - - 同時に共済・中山間マスタとの紐付けを確立: - - `細目_耕地番号` + `細目_分筆番号` → `OfficialKyosaiField.id` を外部キーとして保存 - - `中山間_ID` → `OfficialChusankanField.id` を外部キーとして保存 + - 同時に共済・中山間マスタとの紐付けを確立(ManyToMany): + - `細目_耕地番号` + `細目_分筆番号` → `OfficialKyosaiField` を検索して M:N 関連に追加 + - `中山間_ID` → `OfficialChusankanField` を検索して M:N 関連に追加 ### 紐付けロジック ```python -# 共済マスタとの紐付け +# 共済マスタとの紐付け(M:N) kyosai_record = OfficialKyosaiField.objects.get( k_num=row['細目_耕地番号'], s_num=row['細目_分筆番号'] ) -field.kyosai_field_ref = kyosai_record.id +field.kyosai_fields.add(kyosai_record) -# 中山間マスタとの紐付け +# 中山間マスタとの紐付け(M:N) if pd.notna(row['中山間_ID']): chusankan_record = OfficialChusankanField.objects.get( - c_id=int(row['中山間_ID']) + c_id=str(int(row['中山間_ID'])) ) - field.chusankan_field_ref = chusankan_record.id + field.chusankan_fields.add(chusankan_record) ``` --- @@ -268,115 +347,50 @@ if pd.notna(row['中山間_ID']): **出力形式:** - A4サイズ、縦向き -- ヘッダー: 「水稲共済細目書(◯◯年度)」 -- 表形式(罫線あり) +- ヘッダー: 「水稲共済細目書(YYYY年度)」 +- 表形式(罫線あり)、1行1区画(31行) - フォントサイズ: 10pt -- ページ番号(複数ページの場合) +- ページ番号あり **表の列:** -``` -耕地番号 | 分筆番号 | 地名地番 | 漢字地名 | 本地面積(m2) | 作付品目 | 品種 | 備考 - 1 | 1 | 四万十町... | 四万十町... | 2.2 | 米 |にこまる| - 2 | 1 | 四万十町... | 四万十町... | 25.4 | 米 |にこまる| - 2 | 2 | 四万十町... | 四万十町... | 12.0 |米,野菜|にこまる,トマト|複数圃場 -``` + +| 列名 | データ元 | 備考 | +|------|---------|------| +| 漢字地名 | OfficialKyosaiField.kanji_name | 例: 四万十町 笹ヶ谷 374-1 | +| 耕地-分筆 | k_num + "-" + s_num | 例: 2-1 | +| 本地面積 (m2) | OfficialKyosaiField.area | | +| 作付品目 | Plan.crop.name | システムの作付け計画から | +| 品種 | Plan.variety.name | 〃 | +| 圃場名称 | Field.name | 吉田農地台帳の名称 | **集計ロジック:** -```python -def generate_kyosai_pdf(year): - # 1. データ集約(CSVと同じロジック) - output_rows = [] - for kyosai in OfficialKyosaiField.objects.all().order_by('k_num', 's_num'): - fields = Field.objects.filter(kyosai_field_ref=kyosai.id) - plans = Plan.objects.filter(field__in=fields, year=year) - - crops = list(set([p.crop.name for p in plans if p.crop])) - varieties = list(set([p.variety.name for p in plans if p.variety])) - - row = { - '耕地番号': kyosai.k_num, - '分筆番号': kyosai.s_num, - '地名地番': kyosai.address, - '漢字地名': kyosai.kanji_name, - '本地面積(m2)': kyosai.area, - '作付品目': ','.join(crops) if crops else '未設定', - '品種': ','.join(varieties) if varieties else '', - '備考': f'{len(fields)}筆合算' if len(fields) > 1 else '' - } - output_rows.append(row) - - # 2. HTMLテンプレートで表を生成 - html = render_to_string('reports/kyosai_template.html', { - 'year': year, - 'rows': output_rows - }) - - # 3. HTML → PDF変換 - pdf = HTML(string=html).write_pdf() - return pdf -``` - -**HTMLテンプレート例(reports/kyosai_template.html):** -```html - - -
- - - - -| 耕地番号 | -分筆番号 | -地名地番 | -漢字地名 | -本地面積(m2) | -作付品目 | -品種 | -備考 | -
|---|---|---|---|---|---|---|---|
| {{ row.耕地番号 }} | -{{ row.分筆番号 }} | -{{ row.地名地番 }} | -{{ row.漢字地名 }} | -{{ row.本地面積(m2) }} | -{{ row.作付品目 }} | -{{ row.品種 }} | -{{ row.備考 }} | -