昔つくった病院事務VBAのWeb版をつくろう！

## はじめに
https://qiita.com/YoshitakaOkada/items/8100db539b0359aa8641

のつづきで、Web化したもの。
やりとりしてたのが2014年なので10年越しのWeb化である。あの当時のスキルレベルを振り返るとなつかしいな。

適用するのはおなじみ、ポートフォリオ（hospital）だ

https://github.com/duri0214/portfolio/tree/master/hospital

## 要望
不在者投票の選挙事務業務効率化について

### 関わる人
※ここで「わたし」と書いてあるのは僕じゃなくて依頼主のこと
※太字が主な事務処理関係者

- **選管**
- 病院
- **管理課（わたし）**
- **病棟担当者（ケースワーカー）**
- 患者
- 病棟看護師

### 選挙期間のタイムライン
```mermaid
sequenceDiagram
    participant 患者
    participant 病院
    participant 選管
    病院 ->> 患者: 不在者投票の日程を決めて周知と貼り出し
    患者 ->> 病院: 投票日前に病棟から投票用紙等の請求依頼
    病院 ->> 病院: 住所チェック
    病院 ->> 病院: 病棟名、投票区名、取込日、選挙人住所、選挙人氏名、選挙人生年月日を請求者名簿に入力
    病院 ->> 選管: 請求者名簿の提出
    選管 ->> 病院: 各市区選管から投票用紙を受け取る
    病院 ->> 患者: 投票用紙を病棟ごとに仕分けし、交付
    病院 ->> 病院: 請求の方法、代理請求依頼日、請求月日、受領日を事務処理簿に入力
    患者 ->> 病院: （※アプリ業務の対象外）投票
    病院 ->> 病院: 投票日、投票場所、立会人、代理投票申請有無を事務処理簿に入力
    病院 ->> 選管: 送致用封筒で送致
    病院 ->> 病院: 投票人数×727円を経費として請求書作成
    病院 ->> 選管: 不在者投票事務処理簿、経費請求書の提出
```

1. 選挙告示日
    - 病院で行う不在者投票の日程を決めて周知と貼り出し
1. 投票日前
    - 病棟からわんさかやってくる請求者名簿の処理、各市区選管へ郵送、持ち込み
    - 各市区選管から投票用紙を受取り、市区別のものを病棟ごとに仕分け（※受付期間は定めているが、受付期間終了後でも申し出があれば断れないため随時追加がでてきて、投票用紙の処理と同時並行で追加の人の請求処理もする。しんどい）
    - 事務処理簿に”請求の方法”、”代理請求の依頼を受けた月日”、”請求月日”、”受領日”（投票用紙の）の項目を随時入力する
1. 投票日～投票用紙提出
    - 2日間にわけて投票場所を設置し、立会い、追加があれば請求事務
    - 投票封筒に記載された立会人を事務処理簿に入力
    - 投票日、投票場所も入力
1. 選挙後
    - 実際に投票した人数×727円を経費として請求できるので、退院や棄権を除いた人数を集計し、請求書作成。人数間違うと大変

1～2: 選挙告示日から投票日前まで
- 行政
    - 選挙します！ 不在者投票認可施設だから病院で選挙事務してね！
- 病院
    - 選挙権のある入院患者さん、代理で手続きするから名乗り出てね！
- 病棟担当者
    - 病棟別で意思表示のある患者さんの住所調べて、名前と生年月日を名簿にかくよ！管理課にﾌｧｯｸｽする！
- 管理課（病棟からのﾌｧｯｸｽを選管に提出できるように処理）
    - 請求者名簿を病棟別→市区別にまとめなおし
    - 投票済み用紙と一緒に送る事務処理簿にも随時項目を入力

3: 管理課にて投票日～投票用紙提出まで
- 手書き（下図左端ピンク部分）がFAXで返却されてくる
- 請求者名簿.xls（下図左端水色部分） に入力
    - 選挙管理委員会に要提出
    - 住所、氏名、生年月日、備考に病棟名（備考は提出時空欄にする）
    - 住所チェックする
        - 病院のオーダリング端末の患者情報マスタ
        - ネット検索（読みが難しい地域名など。あとは特にマンション名とか。そのとき賃貸サイトとかに飛んで、よくネットワーク管理者にブロックされる）
    - 後から追加の場合は、すでに提出している人とは別で印刷するので、同じ区でも日付ごとで分けないといけない　→提出済みかどうかをステータス管理すればよさそう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/fed512eb-1ffb-7390-e979-8f20ca59c013.png)
- 不在者投票事務処理簿について
    - 選挙管理委員会に要提出
    - 提出先の各市区選管ごとに印刷する（なぜ市区ごとにシートがわかれている？: 1ページぶんの帳票デザインをタテにコピーしてハンドコピーしているからシートがわかれている。つまりdb化されればこれら複数のシートは不要になる）
    - 提出用のファイルなので不要な文字は書けないが、A列に白文字で病棟名を書いている<br><img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/a433f908-1b29-893f-6268-bf335034ce16.png">
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/fe882dd2-5946-1dd9-48bc-e500ad9c67b6.png)
- 管理経費請求書について![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/527a3eb5-7ae7-ff26-fb6f-4eb55e8c2f53.png)

:::note info
ほら、ちゃんと「選挙管理委員会に提出せぇ」って様式があるんだよ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ac88132a-a089-de96-f278-3e73b3a5f05d.png)
:::

## アプリケーションのベースをつくる
このあたり見て

https://qiita.com/YoshitakaOkada/items/d1e14776040e64cd1434#django%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90

<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/bb35a05a-07a7-1920-a998-20f996d1543b.png">

## modelとseederをつくる
心臓部分だ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c3d64e48-a721-1e47-302a-ee2e12756d5d.png)

### models.py
```py:hospital/models.py
from django.contrib.auth.models import User
from django.db import models


class WardType(models.Model):
    name = models.CharField(verbose_name="病棟種", max_length=100, unique=True)

    def __str__(self):
        return self.name


class Ward(models.Model):
    abbreviation = models.CharField(verbose_name="略称", unique=True, max_length=10)
    ward_type = models.ForeignKey(
        WardType, verbose_name="病棟種", on_delete=models.CASCADE
    )
    name = models.CharField(
        verbose_name="病棟名", max_length=100, null=False, blank=False
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

    def __str__(self):
        return self.name


class City(models.Model):
    name = models.CharField(verbose_name="エリア", max_length=100, unique=True)

    def __str__(self):
        return self.name


class CitySector(models.Model):
    name = models.CharField(verbose_name="市区", max_length=100)
    city = models.ForeignKey(City, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

    def __str__(self):
        return self.name


class Election(models.Model):
    name = models.CharField(verbose_name="選挙名", max_length=255, unique=True)
    execution_date = models.DateField(verbose_name="執行日")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

    def __str__(self):
        return self.name


class VotePlace(models.Model):
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class UserAttribute(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    address = models.TextField(verbose_name="住所")
    date_of_birth = models.DateField(verbose_name="生年月日")


BILLING_METHOD_CHOICES = [
    (1, "代理・直接"),
    (2, "代理・郵便"),
]


class ElectionLedger(models.Model):
    """
    選挙事務用の請求者名簿と事務処理簿の入力項目をまとめた、事務処理台帳

    Note: モデルフィールドで choices 属性が設定されている場合、_get_FOO_display() メソッドが自動生成される。
            これにより、レコード内部値に代替する表示名（"代理・直接" など）を取得することができます。
    >>> ledger = ElectionLedger.objects.get(id=1)
    >>> print(ledger.get_billing_method_display())

    Attributes:
        - election (ForeignKey): 選挙名
        - voter (ForeignKey): 投票者氏名
        - vote_ward (ForeignKey): 病棟名
        - vote_city_sector (ForeignKey): 投票区名
        - remark (CharField): 備考
        - billing_method (CharField): 投票用紙の請求方法
        - proxy_billing_request_date (DateField): 代理請求依頼日
        - proxy_billing_date (DateField): 代理請求日
        - ballot_received_date (DateField): 投票用紙受領日
        - vote_date (DateField): 投票日（投票済みか否かを判断できる）
        - vote_place (ForeignKey): 投票場所
        - voter_witness (ForeignKey): 投票立会人
        - applied_for_proxy_voting (BooleanField): 代理投票申請の有無
        - delivery_date (DateField): 投票用紙送付日
        - created_at (DateTimeField): 取込日時
        - updated_at (DateTimeField): 更新日時
    """

    election = models.ForeignKey(
        Election, verbose_name="選挙名", on_delete=models.CASCADE
    )
    voter = models.ForeignKey(
        User,
        verbose_name="選挙人氏名",
        on_delete=models.CASCADE,
        related_name="voter",
    )
    vote_ward = models.ForeignKey(Ward, verbose_name="病棟", on_delete=models.CASCADE)
    vote_city_sector = models.ForeignKey(
        CitySector, verbose_name="投票区", on_delete=models.CASCADE
    )
    remark = models.CharField(
        verbose_name="備考", max_length=255, null=True, blank=True
    )
    billing_method = models.IntegerField(
        verbose_name="投票用紙請求の方法",
        choices=BILLING_METHOD_CHOICES,
        null=True,
        blank=True,
    )
    proxy_billing_request_date = models.DateField(
        verbose_name="代理請求の依頼を受けた日", null=True, blank=True
    )
    proxy_billing_date = models.DateField(
        verbose_name="代理請求日", null=True, blank=True
    )
    ballot_received_date = models.DateField(
        verbose_name="投票用紙受領日", null=True, blank=True
    )
    vote_date = models.DateField(verbose_name="投票日", null=True, blank=True)
    vote_place = models.ForeignKey(
        VotePlace,
        verbose_name="投票場所",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    vote_observer = models.ForeignKey(
        User,
        verbose_name="投票立会人",
        on_delete=models.CASCADE,
        related_name="voter_witness",
        null=True,
        blank=True,
    )
    applied_for_proxy_voting = models.BooleanField(
        verbose_name="代理投票申請の有無",
        default=False,
    )
    delivery_date = models.DateField(
        verbose_name="投票用紙送付日", null=True, blank=True
    )
    created_at = models.DateTimeField(verbose_name="取込日", auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

    def __str__(self):
        return f"{self.election.name}, {self.voter}（病棟: {self.vote_ward.name}）"
    
```

### 病棟名マスタ
```json:hospital/fixtures/ward.json
[
  {
    "model": "hospital.WardType",
    "pk": 1,
    "fields": {
      "name": "精神"
    }
  },
  {
    "model": "hospital.WardType",
    "pk": 2,
    "fields": {
      "name": "一般"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 1,
    "fields": {
      "abbreviation": "A3",
      "ward_type": 1,
      "name": "A館3階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 2,
    "fields": {
      "abbreviation": "A4",
      "ward_type": 1,
      "name": "A館4階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 3,
    "fields": {
      "abbreviation": "A5",
      "ward_type": 1,
      "name": "A館5階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 4,
    "fields": {
      "abbreviation": "A6",
      "ward_type": 1,
      "name": "A館6階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 5,
    "fields": {
      "abbreviation": "A7",
      "ward_type": 1,
      "name": "A館7階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 6,
    "fields": {
      "abbreviation": "A8",
      "ward_type": 1,
      "name": "A館8階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 7,
    "fields": {
      "abbreviation": "B2",
      "ward_type": 1,
      "name": "B館2階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 8,
    "fields": {
      "abbreviation": "B3",
      "ward_type": 1,
      "name": "B館3階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 9,
    "fields": {
      "abbreviation": "B4",
      "ward_type": 1,
      "name": "B館4階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 10,
    "fields": {
      "abbreviation": "B5",
      "ward_type": 1,
      "name": "B館5階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 11,
    "fields": {
      "abbreviation": "B6",
      "ward_type": 1,
      "name": "B館6階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 12,
    "fields": {
      "abbreviation": "C1",
      "ward_type": 1,
      "name": "C館1階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 13,
    "fields": {
      "abbreviation": "西",
      "ward_type": 1,
      "name": "西館",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 14,
    "fields": {
      "abbreviation": "東3",
      "ward_type": 2,
      "name": "東館3階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 15,
    "fields": {
      "abbreviation": "東4",
      "ward_type": 2,
      "name": "東館4階",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 16,
    "fields": {
      "abbreviation": "リハ",
      "ward_type": 2,
      "name": "リハビリテーション病棟",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 17,
    "fields": {
      "abbreviation": "包括",
      "ward_type": 2,
      "name": "地域包括ケア病棟",
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.ward",
    "pk": 18,
    "fields": {
      "abbreviation": "緩和",
      "ward_type": 2,
      "name": "緩和ケア病棟",
      "created_at": "2024-09-23T10:00:00Z"
    }
  }
]

```
### 市区名マスタ
```json:hospital/fixtures/city.json
[
  {
    "model": "hospital.city",
    "pk": 1,
    "fields": {
      "name": "堺市"
    }
  },
  {
    "model": "hospital.city",
    "pk": 2,
    "fields": {
      "name": "大阪市"
    }
  },
  {
    "model": "hospital.city",
    "pk": 3,
    "fields": {
      "name": "大阪府"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 1,
    "fields": {
      "name": "堺区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 2,
    "fields": {
      "name": "中区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 3,
    "fields": {
      "name": "東区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 4,
    "fields": {
      "name": "西区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 5,
    "fields": {
      "name": "南区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 6,
    "fields": {
      "name": "北区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 7,
    "fields": {
      "name": "美原区",
      "city": 1,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 8,
    "fields": {
      "name": "阿倍野区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 9,
    "fields": {
      "name": "平野区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 10,
    "fields": {
      "name": "住吉区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 11,
    "fields": {
      "name": "住之江区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 12,
    "fields": {
      "name": "東住吉区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 13,
    "fields": {
      "name": "生野区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 14,
    "fields": {
      "name": "城東区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 15,
    "fields": {
      "name": "東成区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 16,
    "fields": {
      "name": "旭区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 17,
    "fields": {
      "name": "鶴見区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 18,
    "fields": {
      "name": "中央区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 19,
    "fields": {
      "name": "西成区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 20,
    "fields": {
      "name": "西区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 21,
    "fields": {
      "name": "天王寺区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 22,
    "fields": {
      "name": "浪速区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 23,
    "fields": {
      "name": "都島区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 24,
    "fields": {
      "name": "福島区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 25,
    "fields": {
      "name": "北区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 26,
    "fields": {
      "name": "東淀川区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 27,
    "fields": {
      "name": "西淀川区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 28,
    "fields": {
      "name": "港区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 29,
    "fields": {
      "name": "大正区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 30,
    "fields": {
      "name": "此花区",
      "city": 2,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 31,
    "fields": {
      "name": "高石市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 32,
    "fields": {
      "name": "松原市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 33,
    "fields": {
      "name": "富田林市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 34,
    "fields": {
      "name": "大阪狭山市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 35,
    "fields": {
      "name": "岸和田市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 36,
    "fields": {
      "name": "和泉市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 37,
    "fields": {
      "name": "河内長野市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  },
  {
    "model": "hospital.CitySector",
    "pk": 37,
    "fields": {
      "name": "羽曳野市",
      "city": 3,
      "created_at": "2024-09-23T10:00:00Z"
    }
  }
]

```
### 看護師・患者マスタ
（※portfolioの既存のシーダーに追記している）
```json:vietnam_research/fixtures/group.json
[
  {
    "model": "auth.group",
    "pk": 1,
    "fields": {
      "name": "看護師"
    }
  },
  {
    "model": "auth.group",
    "pk": 2,
    "fields": {
      "name": "患者"
    }
  }
]

```
```json:vietnam_research/fixtures/user.json
[
    :
  {
    "model": "auth.user",
    "pk": 5,
    "fields": {
      "username": "A看護師",
      "first_name": "A",
      "last_name": "看護師",
      "email": "kangoshiA@example.com",
      "password": "XXX",
      "groups": [
        1
      ],
      "is_staff": 1
    }
  },
  {
    "model": "auth.user",
    "pk": 6,
    "fields": {
      "username": "B看護師",
      "first_name": "B",
      "last_name": "看護師",
      "email": "kangoshiB@example.com",
      "password": "XXX",
      "groups": [
        1
      ],
      "is_staff": 1
    }
  },
  {
    "model": "auth.user",
    "pk": 7,
    "fields": {
      "username": "C看護師",
      "first_name": "C",
      "last_name": "看護師",
      "email": "kangoshiC@example.com",
      "password": "XXX",
      "groups": [
        1
      ],
      "is_staff": 1
    }
  },
  {
    "model": "auth.user",
    "pk": 8,
    "fields": {
      "username": "D看護師",
      "first_name": "D",
      "last_name": "看護師",
      "email": "kangoshiD@example.com",
      "password": "XXX",
      "groups": [
        1
      ],
      "is_staff": 1
    }
  },
  {
    "model": "auth.user",
    "pk": 9,
    "fields": {
      "username": "E看護師",
      "first_name": "E",
      "last_name": "看護師",
      "email": "kangoshiE@example.com",
      "password": "XXX",
      "groups": [
        1
      ],
      "is_staff": 1
    }
  },
  {
    "model": "auth.user",
    "pk": 10,
    "fields": {
      "username": "A患者",
      "first_name": "A",
      "last_name": "患者",
      "email": "kanjaA@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 11,
    "fields": {
      "username": "B患者",
      "first_name": "B",
      "last_name": "患者",
      "email": "kanjaB@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 12,
    "fields": {
      "username": "C患者",
      "first_name": "C",
      "last_name": "患者",
      "email": "kanjaC@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 13,
    "fields": {
      "username": "D患者",
      "first_name": "D",
      "last_name": "患者",
      "email": "kanjaD@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 14,
    "fields": {
      "username": "E患者",
      "first_name": "E",
      "last_name": "患者",
      "email": "kanjaE@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 15,
    "fields": {
      "username": "F患者",
      "first_name": "F",
      "last_name": "患者",
      "email": "kanjaF@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 16,
    "fields": {
      "username": "G患者",
      "first_name": "G",
      "last_name": "患者",
      "email": "kanjaG@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 17,
    "fields": {
      "username": "H患者",
      "first_name": "H",
      "last_name": "患者",
      "email": "kanjaH@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 18,
    "fields": {
      "username": "I患者",
      "first_name": "I",
      "last_name": "患者",
      "email": "kanjaI@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  },
  {
    "model": "auth.user",
    "pk": 19,
    "fields": {
      "username": "J患者",
      "first_name": "J",
      "last_name": "患者",
      "email": "kanjaJ@example.com",
      "password": "XXX",
      "groups": [
        2
      ]
    }
  }
]

```
### 患者属性マスタ
```json:hospital/fixtures/userattribute.json
[
  {
    "model": "hospital.UserAttribute",
    "pk": 1,
    "fields": {
      "user": 10,
      "address": "大阪府大阪市北区梅田1丁目1-1",
      "date_of_birth": "1980-01-01"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 2,
    "fields": {
      "user": 11,
      "address": "大阪府堺市堺区築港八町1丁",
      "date_of_birth": "1982-02-02"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 3,
    "fields": {
      "user": 12,
      "address": "大阪府大阪市中央区城見1丁目",
      "date_of_birth": "1975-03-03"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 4,
    "fields": {
      "user": 13,
      "address": "大阪府大阪市東淀川区東淡路5丁目",
      "date_of_birth": "1985-04-04"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 5,
    "fields": {
      "user": 14,
      "address": "大阪府堺市泉北区和泉野町",
      "date_of_birth": "1978-05-05"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 6,
    "fields": {
      "user": 15,
      "address": "大阪府大阪市此花区逸津川町",
      "date_of_birth": "1972-06-06"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 7,
    "fields": {
      "user": 16,
      "address": "大阪府大阪市西区江戸堀1丁目",
      "date_of_birth": "1962-07-07"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 8,
    "fields": {
      "user": 17,
      "address": "大阪府大阪市西淀川区姫里3丁目",
      "date_of_birth": "1971-08-08"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 9,
    "fields": {
      "user": 18,
      "address": "大阪府大阪市浪速区敷津東3丁目",
      "date_of_birth": "1988-09-09"
    }
  },
  {
    "model": "hospital.UserAttribute",
    "pk": 10,
    "fields": {
      "user": 19,
      "address": "大阪府大阪市東成区星ヶ町",
      "date_of_birth": "1987-10-10"
    }
  }
]

```

### 選挙名マスタ
```json:hospital/fixtures/election.json
[
  {
    "model": "hospital.election",
    "pk": 1,
    "fields": {
      "name": "大阪府議会・堺市議会議員選挙",
      "execution_date": "2015-04-12",
      "created_at": "2024-09-23T10:00:00Z"
    }
  }
]

```

### 選挙場所マスタ
```json:hospital/fixtures/voteplace.json
[
  {
    "model": "hospital.VotePlace",
    "pk": 1,
    "fields": {
      "name": "総務"
    }
  },
  {
    "model": "hospital.VotePlace",
    "pk": 2,
    "fields": {
      "name": "食堂"
    }
  },
  {
    "model": "hospital.VotePlace",
    "pk": 3,
    "fields": {
      "name": "院内ローソン"
    }
  }
]

```

## CRUD
### forms.py
```py:hospital/forms.py
from django import forms
from django.contrib.auth.models import User

from hospital.models import (
    ElectionLedger,
    Election,
    Ward,
    CitySector,
    BILLING_METHOD_CHOICES,
    VotePlace,
)


class ElectionLedgerCreateForm(forms.ModelForm):
    election = forms.ModelChoiceField(
        queryset=Election.objects.all(),
        label="選挙名*",
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "1"}),
    )

    patient_ids = User.objects.filter(groups__name__in=["患者"]).values_list(
        "id", flat=True
    )
    voter = forms.ModelChoiceField(
        queryset=User.objects.filter(id__in=patient_ids),
        label="選挙人氏名*",
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "2"}),
    )

    vote_ward = forms.ModelChoiceField(
        queryset=Ward.objects.all(),
        label="病棟*",
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "3"}),
    )

    vote_city_sector = forms.ModelChoiceField(
        queryset=CitySector.objects.all(),
        label="投票区*",
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "4"}),
    )

    remark = forms.CharField(
        label="備考",
        required=False,
        widget=forms.Textarea(
            attrs={"class": "form-control", "rows": "3", "tabindex": "5"}
        ),
    )

    billing_method = forms.ChoiceField(
        choices=BILLING_METHOD_CHOICES,
        label="投票用紙請求の方法*",
        initial=2,
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "6"}),
    )

    proxy_billing_request_date = forms.DateField(
        label="代理請求の依頼を受けた日",
        error_messages={
            "invalid": "代理請求の依頼を受けた日を正しく入力してください。"
        },
        required=False,
        widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "7"}),
    )

    proxy_billing_date = forms.DateField(
        label="代理請求日",
        error_messages={"invalid": "代理請求日を正しく入力してください。"},
        required=False,
        widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "8"}),
    )

    ballot_received_date = forms.DateField(
        label="投票用紙受領日",
        error_messages={"invalid": "投票用紙受領日を正しく入力してください。"},
        required=False,
        widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "9"}),
    )

    vote_date = forms.DateField(
        label="投票日",
        error_messages={"invalid": "投票日を正しく入力してください。"},
        required=False,
        widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "10"}),
    )

    vote_place = forms.ModelChoiceField(
        queryset=VotePlace.objects.all(),
        label="投票場所",
        required=False,
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "11"}),
    )

    vote_observer_ids = User.objects.filter(groups__name__in=["看護師"]).values_list(
        "id", flat=True
    )

    vote_observer = forms.ModelChoiceField(
        queryset=User.objects.filter(id__in=vote_observer_ids),
        label="投票立会人",
        required=False,
        widget=forms.Select(attrs={"class": "form-control", "tabindex": "12"}),
    )

    applied_for_proxy_voting = forms.BooleanField(
        label="代理投票申請の有無",
        required=False,
        initial=False,
        widget=forms.CheckboxInput(
            attrs={
                "class": "form-check-input",
                "style": "margin-left: 20px;",
                "tabindex": "13",
            }
        ),
    )

    delivery_date = forms.DateField(
        label="投票用紙送付日",
        error_messages={"invalid": "投票用紙送付日を正しく入力してください。"},
        required=False,
        widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "14"}),
    )

    class Meta:
        model = ElectionLedger
        fields = "__all__"


class ElectionLedgerUpdateForm(ElectionLedgerCreateForm):
    pass
        
```

### create.html
```html:hospital/templates/hospital/election_ledger/create.html
{% extends 'hospital/base.html' %}

{% block content %}
    <div class="container">
        <h2>新規作成</h2>
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-outline-primary">送信</button>
        </form>
    </div>
{% endblock %}

```

### index.html
```html:hospital/templates/hospital/index.html
{% extends "hospital/base.html" %}
{% load static %}
{% load humanize %}
{% block content %}
    <div class="jumbotron">
        <h1 class="display-4">Let's work at the hospital!</h1>
        <p class="lead">improving proxi voting operations.</p>
        <hr class="my-4">
        <p>You can do hospital administration too.</p>
        <ul>
            <li><a class="btn btn-secondary btn-sm" href="#" role="button">XXXX</a></li>
            <li><a class="btn btn-secondary btn-sm" href="#" role="button">YYYY</a></li>
        </ul>
    </div>

    <div class="container">
        <div class="form-group">
            <form method="get">
                <select name="election" class="form-control" onchange="this.form.submit()">
                    <option value="">-- 選挙を選択 --</option>
                    {% for election in elections %}
                        <option value="{{ election.id }}"
                                {% if request.GET.election == election.id|stringformat:"s" %}selected{% endif %}>
                            {{ election.name }}
                        </option>
                    {% endfor %}
                </select>
            </form>
        </div>
        <a href="{% url 'hsp:election_ledger_create' %}" class="btn btn-outline-primary"
           role="button">新規投票記録を追加</a>
        <table class="table table-striped table-bordered">
            <thead class="bg-primary text-white">
            <tr>
                <th scope="col">病棟名</th>
                <th scope="col">投票区名</th>
                <th scope="col">取込日</th>
                <th scope="col">選挙人氏名</th>
                <th scope="col">選挙人住所</th>
                <th scope="col">選挙人生年月日</th>
                <th scope="col">操作</th>
            </tr>
            </thead>
            <tbody>
            {% for ledger in electionledger_list %}
                <tr>
                    <td>{{ ledger.vote_ward }}</td>
                    <td>{{ ledger.vote_city_sector }}</td>
                    <td>{{ ledger.created_at|date:"Y-m-d" }}</td>
                    <td>{{ ledger.voter }}</td>
                    <td>{{ ledger.voter.userattribute.address }}</td>
                    <td>{{ ledger.voter.userattribute.date_of_birth|date:"Y-m-d" }}</td>
                    <td>
                        <a href="{% url 'hsp:election_ledger_detail' ledger.id %}" class="btn btn-outline-info">詳細</a>
                        <a href="{% url 'hsp:election_ledger_update' ledger.id %}"
                           class="btn btn-outline-primary">更新</a>
                        <a href="{% url 'hsp:election_ledger_delete' ledger.id %}"
                           class="btn btn-outline-danger">削除</a>
                    </td>
                </tr>
            {% empty %}
                <tr>
                    <td colspan="8">No ledger available.</td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
        <nav aria-label="Page navigation example">
            <ul class="pagination">
                {% if page_obj.has_previous %}
                    <li class="page-item"><a class="page-link" href="?page=1">First</a></li>
                    <li class="page-item"><a class="page-link"
                                             href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
                {% else %}
                    <li class="page-item disabled"><a class="page-link" href="#">First</a></li>
                    <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
                {% endif %}
                <li class="page-item active"><a class="page-link" href="#">{{ page_obj.number }}</a></li>
                {% if page_obj.has_next %}
                    <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
                    </li>
                    <li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
                    </li>
                {% else %}
                    <li class="page-item disabled"><a class="page-link" href="#">Next</a></li>
                    <li class="page-item disabled"><a class="page-link" href="#">Last</a></li>
                {% endif %}
            </ul>
        </nav>
    </div>

{% endblock %}

```

### detail.html
```html:hospital/templates/hospital/election_ledger/detail.html
{% extends "hospital/base.html" %}
{% load humanize %}

{% block content %}
    <div class="container">

        <h2 class="my-2">投票記録詳細</h2>

        <dl class="row">
            <dt class="col-sm-3">選挙名</dt>
            <dd class="col-sm-9">{{ object.election }}</dd>

            <dt class="col-sm-3">選挙人氏名</dt>
            <dd class="col-sm-9">{{ object.voter }}</dd>

            <dt class="col-sm-3">病棟</dt>
            <dd class="col-sm-9">{{ object.vote_ward }}</dd>

            <dt class="col-sm-3">投票区</dt>
            <dd class="col-sm-9">{{ object.vote_city_sector }}</dd>

            <dt class="col-sm-3">備考</dt>
            <dd class="col-sm-9">{{ object.remark|default:"-" }}</dd>

            <dt class="col-sm-3">取込日</dt>
            <dd class="col-sm-9">{{ object.created_at|date:"Y-m-d" }}</dd>

            <dt class="col-sm-3">投票用紙請求の方法</dt>
            <dd class="col-sm-9">{{ object.get_billing_method_display }}</dd>

            <dt class="col-sm-3">代理請求の依頼を受けた日</dt>
            <dd class="col-sm-9">{{ object.proxy_billing_request_date|date:"Y-m-d"|default:"-" }}</dd>

            <dt class="col-sm-3">代理請求日</dt>
            <dd class="col-sm-9">{{ object.proxy_billing_date|date:"Y-m-d"|default:"-" }}</dd>

            <dt class="col-sm-3">投票用紙受領日</dt>
            <dd class="col-sm-9">{{ object.ballot_received_date|date:"Y-m-d"|default:"-" }}</dd>

            <dt class="col-sm-3">投票日</dt>
            <dd class="col-sm-9">{{ object.vote_date|date:"Y-m-d"|default:"-" }}</dd>

            <dt class="col-sm-3">投票場所</dt>
            <dd class="col-sm-9">{{ object.vote_place|default:"-" }}</dd>

            <dt class="col-sm-3">投票立会人</dt>
            <dd class="col-sm-9">{{ object.vote_observer|default:"-" }}</dd>

            <dt class="col-sm-3">代理投票申請の有無</dt>
            <dd class="col-sm-9">{{ object.applied_for_proxy_voting|yesno:"有,無" }}</dd>

            <dt class="col-sm-3">投票用紙送付日</dt>
            <dd class="col-sm-9">{{ object.delivery_date|date:"Y-m-d"|default:"-" }}</dd>

            <dt class="col-sm-3">更新日</dt>
            <dd class="col-sm-9">{{ object.updated_at|date:"Y-m-d H:i"|default:"-" }}</dd>
        </dl>

        <a href="{% url 'hsp:election_ledger_update' object.id %}" class="btn btn-outline-primary mx-1">更新</a>
        <a href="{% url 'hsp:election_ledger_delete' object.id %}" class="btn btn-outline-danger mx-1">削除</a>
        <a href="{% url 'hsp:index' %}" role="button" class="btn btn-outline-secondary mx-1">戻る</a>

    </div>
{% endblock content %}

```

### update.html
```html:hospital/templates/hospital/election_ledger/update.html
{% extends 'hospital/base.html' %}

{% block content %}
    <div class="container">
        <h2>更新</h2>
        <form method="post">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit" class="btn btn-outline-primary">更新</button>
            <a href="{% url 'hsp:index' %}" role="button" class="btn btn-outline-secondary mx-1">戻る</a>
        </form>
    </div>
{% endblock %}

```

### delete.html
```py:hospital/templates/hospital/election_ledger/delete.html
{% extends 'hospital/base.html' %}

{% block content %}
    <h2>投票記録の削除</h2>
    <p>本当に "{{ object }}" の投票記録を削除しますか？</p>

    <form method="post">
        {% csrf_token %}
        <input type="submit" value="削除" class="btn btn-danger"/>
        <a href="{% url 'hsp:index' %}" class="btn btn-outline-secondary" role="button">キャンセル</a>
    </form>
{% endblock %}

```

### urls.py
```py:hospital/urls.py
from django.urls import path

from hospital.views import (
    IndexView,
    ElectionLedgerCreateView,
    ElectionLedgerUpdateView,
    ElectionLedgerDeleteView,
    ElectionLedgerDetailView,
)

app_name = "hsp"
urlpatterns = [
    path("", IndexView.as_view(), name="index"),
    path(
        "election_ledger/create/",
        ElectionLedgerCreateView.as_view(),
        name="election_ledger_create",
    ),
    path(
        "election_ledger/update/<int:pk>/",
        ElectionLedgerUpdateView.as_view(),
        name="election_ledger_update",
    ),
    path(
        "election_ledger/delete/<int:pk>/",
        ElectionLedgerDeleteView.as_view(),
        name="election_ledger_delete",
    ),
    path(
        "election_ledger/<int:pk>/detail/",
        ElectionLedgerDetailView.as_view(),
        name="election_ledger_detail",
    ),
]

```

### views.py
```py:hospital/views.py
from django.urls import reverse_lazy
from django.views.generic import (
    CreateView,
    ListView,
    UpdateView,
    DeleteView,
    DetailView,
)

from hospital.forms import ElectionLedgerCreateForm, ElectionLedgerUpdateForm
from hospital.models import ElectionLedger, Election


class IndexView(ListView):
    model = ElectionLedger
    template_name = "hospital/index.html"
    paginate_by = 5
    ordering = ["-created_at"]

    def get_queryset(self):
        election = self.request.GET.get("election")
        if election:
            return ElectionLedger.objects.filter(election=election).order_by(
                "-created_at"
            )
        return ElectionLedger.objects.all().order_by("-created_at")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["elections"] = Election.objects.all()
        return context


class ElectionLedgerCreateView(CreateView):
    form_class = ElectionLedgerCreateForm
    template_name = "hospital/election_ledger/create.html"
    success_url = reverse_lazy("hsp:index")


class ElectionLedgerUpdateView(UpdateView):
    model = ElectionLedger
    form_class = ElectionLedgerUpdateForm
    template_name = "hospital/election_ledger/update.html"
    success_url = reverse_lazy("hsp:index")


class ElectionLedgerDeleteView(DeleteView):
    model = ElectionLedger
    template_name = "hospital/election_ledger/delete.html"
    success_url = reverse_lazy("hsp:index")


class ElectionLedgerDetailView(DetailView):
    model = ElectionLedger
    template_name = "hospital/election_ledger/detail.html"

```

## 帳票のExport
### 請求者名簿
#### export_report.py
```py:hospital/domain/service/export_report.py
import os
import uuid
from abc import ABC, abstractmethod
from itertools import islice, groupby
from pathlib import Path
from urllib.parse import quote

from django.http import HttpResponse
from openpyxl.reader.excel import load_workbook

from hospital.domain.valueobject.export_report import BillingListRow
from hospital.models import ElectionLedger


class AbstractExport(ABC):
    def __init__(self, temp_folder: Path):
        self.temp_folder = temp_folder
        os.makedirs(self.temp_folder, exist_ok=True)

    def create_unique_filename(self):
        return str(self.temp_folder / f"{str(uuid.uuid4())}.xlsx")

    def get_excel_data(self, filename: str) -> bytes:
        with open(self.temp_folder / filename, "rb") as f:
            return f.read()

    @abstractmethod
    def export(self, *args, **kwargs):
        pass


class ExportBillingService(AbstractExport):
    def export(self, election_id) -> HttpResponse:
        ledgers = (
            ElectionLedger.objects.filter(election_id=election_id)
            .select_related("vote_ward")
            .order_by("vote_ward__name")
        )

        filepath = os.path.abspath("hospital/domain/service/xlsx/billing_list.xlsx")
        wb = load_workbook(filepath)
        filename = self.create_unique_filename()

        chunk_size = 15
        start_row = 5

        sheet_counter = 1
        for ward_name, group in groupby(ledgers, key=lambda x: x.vote_ward.name):
            ledgers_iter = iter(group)
            while True:
                chunk = list(islice(ledgers_iter, chunk_size))
                if not chunk:
                    break

                new_worksheet = wb.copy_worksheet(wb["ひな形"])
                new_worksheet.title = ward_name if sheet_counter == 1 else f"{ward_name} ({sheet_counter})"
                sheet_counter += 1

                for i, ledger in enumerate(chunk, start=0):
                    row = BillingListRow(ledger)
                    new_worksheet.cell(row=start_row + i, column=1, value=row.address)  # A列
                    new_worksheet.cell(row=start_row + i, column=2, value=row.voter_name)  # B列
                    new_worksheet.cell(row=start_row + i, column=3, value=row.date_of_birth)  # C列
        del wb["ひな形"]
        wb.save(filename)

        try:
            excel_data = self.get_excel_data(filename)
            response = HttpResponse(
                excel_data,
                content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
            header_content = f"attachment; filename*=UTF-8''{quote("請求者名簿.xlsx")}"
            response["Content-Disposition"] = header_content
        finally:
            os.remove(self.temp_folder / filename)

        return response
        
```

#### billing_list.xlsx
```hospital/domain/service/xlsx/billing_list.xlsx
(excelのテンプレートをアップロード)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/95210fc1-55e4-001c-64f8-91463daa28f7.png)

#### export_report.py
```py:hospital/domain/valueobject/export_report.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import date

from hospital.models import ElectionLedger


def convert_to_japanese_era(birth_date: date) -> str:
    year, month, day = birth_date.year, birth_date.month, birth_date.day
    if year > 2019 or (year == 2019 and month >= 5 and day >= 1):
        era_year = year - 2018
        era_name = "令和"
    elif year > 1989 or (year == 1989 and month >= 1 and day >= 8):
        era_year = year - 1988
        era_name = "平成"
    else:
        era_year = year - 1925
        era_name = "昭和"

    return f"{era_name} {era_year}.{month:02d}.{day:02d}"


class AbstractRow(ABC):
    @staticmethod
    @abstractmethod
    def get_field_names() -> list[str]:
        pass

    @abstractmethod
    def to_list(self) -> list[str]:
        pass


@dataclass
class BillingListRow(AbstractRow):
    ledger: ElectionLedger

    @property
    def address(self) -> str:
        return self.ledger.voter.userattribute.address

    @property
    def voter_name(self) -> str:
        return self.ledger.voter.username

    @property
    def date_of_birth(self) -> str:
        return convert_to_japanese_era(self.ledger.voter.userattribute.date_of_birth)

    @property
    def ward_name(self) -> str:
        return self.ledger.vote_ward.name

    @staticmethod
    def get_field_names() -> list[str]:
        return ["選挙人住所", "選挙人氏名", "生年月日", "病棟"]

    def to_list(self) -> list:
        return [self.address, self.voter_name, self.date_of_birth, self.ward_name]
        
```

#### base.html
※portfolioのものだが必要な箇所だけ加筆した
```diff_html:hospital/templates/hospital/base.html
        :
+   {% block extra_css %}{% endblock %}
        :
+{% block extra_js %}{% endblock %}
    :

```

#### index.html
```diff_html:hospital/templates/hospital/index.html
                :
        <ul>
-           <li><a class="btn btn-secondary btn-sm" href="#" role="button">XXXX</a></li>
-           <li><a class="btn btn-secondary btn-sm" href="#" role="button">YYYY</a></li>
+           <li>
+               <a class="btn btn-secondary btn-sm"
+                  href="{% url 'hsp:export_billing_list' %}?election={{ request.GET.election }}"
+                  role="button"
+                  style="{% if not canExport %}opacity:0.65; cursor:not-allowed;{% endif %}">
+                   請求者名簿
+               </a>
+           </li>
+           <li>
+               <a class="btn btn-secondary btn-sm" href="#" role="button"
+                  style="{% if not canExport %}opacity:0.65; cursor:not-allowed;{% endif %}">
+                   不在者投票事務処理簿
+               </a>
+           </li>
        </ul>
            :
-               <select name="election" class="form-control" onchange="this.form.submit()">
+               <select name="election" class="form-control" onchange="onElectionChangeHandler(this)">
                    <option value="">-- 選挙を選択 --</option>
    :
+{% block extra_js %}
+   <script>
+       function onElectionChangeHandler(selectElement) {
+           if (selectElement.value === '') {
+               location.href = location.pathname;
+           } else {
+               selectElement.form.submit();
+           }
+       }
+   </script>
+{% endblock %}

```

#### urls.py
```diff_python:hospital/urls.py
from django.urls import path

from hospital.views import (
        :
+   ExportBillingListView,
)

app_name = "hsp"
        :
+   path(
+       "export/billing-list/",
+       ExportBillingListView.as_view(),
+       name="export_billing_list",
+   ),
]
```

#### views.py
```diff_python:hospital/views.py
+from django.http import HttpResponse
from django.urls import reverse_lazy
+from django.views import View
from django.views.generic import (
    CreateView,
    ListView,
    UpdateView,
    DeleteView,
    DetailView,
)

+from config.settings import MEDIA_ROOT
+from hospital.domain.service.export_report import ExportBillingService
from hospital.forms import ElectionLedgerCreateForm, ElectionLedgerUpdateForm
    :
    def get_context_data(self, **kwargs):
+       election = self.request.GET.get("election")
        context = super().get_context_data(**kwargs)
        context["elections"] = Election.objects.all()
+       context["canExport"] = True if election else False
        return context
    :
+class ExportBillingListView(View):
+    @staticmethod
+    def get(request, *args, **kwargs):
+        election_id = request.GET.get("election", None)
+        if not election_id:
+            return HttpResponse("Election ID not provided", status=400)
+        service = ExportBillingService(temp_folder=MEDIA_ROOT / "hospital/temp")
+        return service.export(election_id)
        
```

#### requirements.txt
```diff:requirements.txt
    :
+openpyxl~=3.1.5
```

### 不在者投票事務処理簿
まぁテンプレートが違うだけなんで同じように作ってください

:::note warn
TODOではあるが、改善案ぐらいのテンションにしておこう
- 住所チェック（病院のオーダリング端末の患者情報マスタ、ネット検索）
- 後から追加の場合は、同じ区でも日付ごとで分けないといけない
    - 提出済みかどうかをステータス管理すればよさそう
:::