エンジニア夫婦の技術日記

【Django】家計請求額算出Webアプリを作った話


2022年7月15日
Posted by 

Page4.実装


はじめに

実装については以下をご理解いただけますと幸いです。宜しくお願い致します。

  • 申し訳ないですが、数字等のお見せしたくない部分はマスキングさせていただいています。
    そのため実際に動作しているコードとは違う部分もございます。
    ご承知おきください。
  • ソースコードの詳細なロジックの解説は今回は割愛させていただきます。
    アプリ自体まだまだ改修を重ねていくつもりですので、
    今後折を見て少しずつ解説を挙げていこうかと思います。


実際の画面イメージ

以下4つのタブ構成になっています。

  1. ダッシュボード
  2. カード明細一覧
  3. カード明細取込
  4. カード明細削除

これらはBootstrap5のtabsを利用しています。


妻への請求明細CSV

↓実際に出力されるCSVです。(※数字や支払先は申し訳ないですがマスキングしています。)


ディレクトリ構成

ルート
│  db.sqlite3
│  manage.py
│  
├─.vscode
│  │  launch.json
│  │  
│  └─.ropeproject
│          config.py
│          objectdb
│          
├─CardDetailCalculator
│  │  asgi.py
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │  
│  └─__pycache__
│          settings.cpython-38.pyc
│          urls.cpython-38.pyc
│          wsgi.cpython-38.pyc
│          __init__.cpython-38.pyc
│          
└─Report
    │  admin.py
    │  apps.py
    │  DataType.py
    │  forms.py
    │  graph.py
    │  models.py
    │  tests.py
    │  urls.py
    │  utils.py
    │  views.py
    │  __init__.py
    │  
    ├─migrations
    │  │  0001_initial.py
    │  │  0002_auto_20211031_1215.py
    │  │  0003_cardpayment_billingdateym.py
    │  │  __init__.py
    │  │  
    │  └─__pycache__
    │          0001_initial.cpython-38.pyc
    │          0002_auto_20211031_1215.cpython-38.pyc
    │          0003_cardpayment_billingdateym.cpython-38.pyc
    │          __init__.cpython-38.pyc
    │          
    ├─static
    │  └─Report
    │      ├─css
    │      │  ├─lib
    │      │  │  └─bootstrap
    │      │  │          bootstrap-grid.css
    │      │  │          bootstrap-grid.css.map
    │      │  │          bootstrap-grid.min.css
    │      │  │          bootstrap-grid.min.css.map
    │      │  │          bootstrap-grid.rtl.css
    │      │  │          bootstrap-grid.rtl.css.map
    │      │  │          bootstrap-grid.rtl.min.css
    │      │  │          bootstrap-grid.rtl.min.css.map
    │      │  │          bootstrap-reboot.css
    │      │  │          bootstrap-reboot.css.map
    │      │  │          bootstrap-reboot.min.css
    │      │  │          bootstrap-reboot.min.css.map
    │      │  │          bootstrap-reboot.rtl.css
    │      │  │          bootstrap-reboot.rtl.css.map
    │      │  │          bootstrap-reboot.rtl.min.css
    │      │  │          bootstrap-reboot.rtl.min.css.map
    │      │  │          bootstrap-utilities.css
    │      │  │          bootstrap-utilities.css.map
    │      │  │          bootstrap-utilities.min.css
    │      │  │          bootstrap-utilities.min.css.map
    │      │  │          bootstrap-utilities.rtl.css
    │      │  │          bootstrap-utilities.rtl.css.map
    │      │  │          bootstrap-utilities.rtl.min.css
    │      │  │          bootstrap-utilities.rtl.min.css.map
    │      │  │          bootstrap.css
    │      │  │          bootstrap.css.map
    │      │  │          bootstrap.min.css
    │      │  │          bootstrap.min.css.map
    │      │  │          bootstrap.rtl.css
    │      │  │          bootstrap.rtl.css.map
    │      │  │          bootstrap.rtl.min.css
    │      │  │          bootstrap.rtl.min.css.map
    │      │  │          
    │      │  └─report
    │      │          index.css
    │      │          
    │      └─js
    │          ├─lib
    │          │  ├─bootstrap
    │          │  │      bootstrap.bundle.js
    │          │  │      bootstrap.bundle.js.map
    │          │  │      bootstrap.bundle.min.js
    │          │  │      bootstrap.bundle.min.js.map
    │          │  │      bootstrap.esm.js
    │          │  │      bootstrap.esm.js.map
    │          │  │      bootstrap.esm.min.js
    │          │  │      bootstrap.esm.min.js.map
    │          │  │      bootstrap.js
    │          │  │      bootstrap.js.map
    │          │  │      bootstrap.min.js
    │          │  │      bootstrap.min.js.map
    │          │  │      
    │          │  └─jquery
    │          │          jquery-3.5.1.min.js
    │          │          
    │          └─report
    │                  index.js
    │                  
    ├─templates
    │  └─Report
    │          base.html
    │          report.html
    │          
    └─__pycache__
            admin.cpython-38.pyc
            apps.cpython-38.pyc
            DataType.cpython-38.pyc
            eDataType.cpython-38.pyc
            forms.cpython-38.pyc
            graph.cpython-38.pyc
            models.cpython-38.pyc
            models2.cpython-38.pyc
            urls.cpython-38.pyc
            utils.cpython-38.pyc
            views.cpython-38.pyc
            __init__.cpython-38.pyc

ソースコード

HTML

まぁHTMLは割と皆さんご存じだと思います。

base.html

このファイルはあまり重要ではないです。名前の通りベースのhtmlで、共通ファイルを読み込んだりしています。

{% load i18n static %}
<!DOCTYPE html>{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="{% static 'Report/css/lib/bootstrap/bootstrap.min.css' %}">
{% block extra_css %}{% endblock %}
<title>{% block title %}My books{% endblock %}</title>
</head>
<body>
  <div class="container">
    {% block content %}
      {{ content }}
    {% endblock %}
  </div>
<script src="{% static 'Report/js/lib/jquery/jquery-3.5.1.min.js' %}"></script>
<script src="{% static 'Report/js/lib/bootstrap/bootstrap.bundle.min.js' %}"></script>
<script src="https://unpkg.com/vue"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

report.html(表示部分)

メインで表示されるHTMLです。

妻がウォーターサーバーを契約していてその請求は妻の口座に行くため、
その分も計算に含められるようにしています(妻への請求から控除することで実質割り勘にする)。

Scriptタグ内でVue.jsの設定をしているので、その部分は切り出します。

{% extends "Report/base.html" %}

{% block title %}カード明細清算{% endblock title %}

{% load i18n static %}
{% block content %}
<link rel="stylesheet" href="{% static 'Report/css/report/index.css' %}">
<div>
    <ul class="nav nav-tabs">
        <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="#tab-dashbord" data-bs-toggle="tab">ダッシュボード</a>
          </li>
        <li class="nav-item">
          <a class="nav-link" aria-current="page" href="#tab-list" data-bs-toggle="tab">カード明細一覧</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#tab-import" data-bs-toggle="tab">カード明細取込</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#tab-delete" data-bs-toggle="tab">カード明細削除</a>
        </li>
      </ul>
</div>
<div id="reportApp" class="tab-content">
    <!-- ダッシュボードタブ -->
    <div class="tab-pane active"  id="tab-dashbord">
        <img src="data:image/png;base64,{{ chart | safe }}">
    </div>
    <!-- カード明細一覧タブ -->
    <div class="tab-pane"  id="tab-list">
        <h4 class="mt-4 border-bottom">表示対象</h4>
        <form action="" method="get">
            <div class="form-group">
                <div class="row">
                    <div class="col-sm-1">
                        対象日
                    </div>
                    <div class="col-sm-2">
                        {{SearchForm.targetDate}}
                    </div>
                    <div class="col">
                        <input type="submit" class="btn btn-outline-primary" name="btn_search" value="検索" />
                    </div>
                </div>
            </div>
        </form>
        <div class="row">
            <div class="col-sm-1">
                請求ファイル
            </div>
            <div class="col-sm-2">
                <input type="button" class="btn btn-outline-primary" v-on:click="download()" name="btn_download" value="CSVダウンロード" />
            </div>
        </div>
        <h4 class="mt-4 border-bottom">カード明細一覧</h4>
        <div class="container-fluid">
            <div id="tableWrapper">
                <table id="detailListTable" border="1" class="table table-striped table-bordered">
                    <thead class="thead-dark">
                        <tr class="table-primary">
                            <th>個人的支出</th>
                            <th>分類</th>
                            <th>支払日</th>
                            <th>支払先</th>
                            <th>支払金額</th>
                        </tr>
                    </thead>
                    <tfoot>
                        <tr class="table-success">
                            <td colspan="4" align="right">合計</td>
                            <td align="right">¥[[ total.toLocaleString() ]]</td>
                        </tr>
                        <tr class="table-success">
                            <td colspan="4" align="right">請求額(合計額/2 - 貯金(**,**0) - ウォーターサーバー代の半額)</td>
                            <td align="right">¥[[ billedAmount.toLocaleString() ]]</td>
                        </tr>
                    </tfoot>
                    <tbody>
                        <tr class="table-secondary">
                            <td colspan="4" align="right">家賃</td>
                            <td align="right">[[ rent.toLocaleString() ]]</td>
                        </tr>
                        <tr class="table-secondary">
                            <td colspan="4" align="right">貯金</td>
                            <td align="right">△¥[[ savings.toLocaleString() ]]</td>
                        </tr>
                        <tr class="table-warning">
                            <td colspan="4" align="right">電気代</td>
                            <td align="right">¥<input type="number" v-model="electricBill"/></td>
                        </tr>
                        <tr class="table-warning">
                            <td colspan="4" align="right">水道代</td>
                            <td align="right">¥<input type="number" v-model="waterBill"/></td>
                        </tr>
                        <tr class="table-warning">
                            <td colspan="4" align="right">ガス代</td>
                            <td align="right">¥<input type="number" v-model="gasBill"/></td>
                        </tr>
                        <tr class="table-warning">
                            <td colspan="4" align="right">ウォーターサーバーの水代</td>
                            <td align="right">△¥<input type="number" v-model="waterServerBill"/></td>
                        </tr>
                        <tr v-for="cardDetail in list">
                            <td>
                                <input type="checkbox"
                                    v-bind:checked="cardDetail.checked"
                                    v-on:change="changeSumTarget(cardDetail.rowIndex)">
                            </td>
                            <td>[[ convertDataType(cardDetail.dataType) ]]</td>
                            <td>[[ cardDetail.payDate ]]</td>
                            <td>[[ cardDetail.paymentDestination ]]</td>
                            <td align="right">¥[[ cardDetail.payment.toLocaleString() ]]</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- カード明細取得タブ -->
    <div class="tab-pane"  id="tab-import">
        <h4 class="mt-4 border-bottom">カード明細取得</h4>
        <div class="container">
            <form action="" method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                <div class="form-group">
                    <div class="row">
                        <div class="col-sm-2">
                            請求月
                        </div>
                        <div class="col-sm-3">
                            {{CSVUploadForm.billingDate}}
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-sm-2">
                            イオンカード明細CSV 
                        </div>
                        <div class="col">
                            {{CSVUploadForm.aeonCardCSV}}
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-sm-2">
                            Dカード明細CSV 
                        </div>
                        <div class="col">
                            {{CSVUploadForm.dCardCSV}}
                        </div>
                    </div>
                </div>
                <input type="submit" class="btn btn-outline-primary" name="btn_output" value="帳票出力" />
            </form>
        </div>
    </div>

    <!-- カード明細削除タブ -->
    <div class="tab-pane" id="tab-delete">
        <h4 class="mt-4 border-bottom">削除対象</h4>
        <form action="" method="post">
            {% csrf_token %}
            <div class="form-group">
                <div class="row">
                    <div class="col-sm-2">
                        対象日
                    </div>
                    <div class="col-sm-2">
                        {{DeleteForm.deleteDate}}
                    </div>
                    <div class="col">
                        <input type="submit" class="btn btn-outline-primary" name="btn_delete" value="削除" />
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>

<script src="https://unpkg.com/vue@2.6.14"></script>
<!-- Bootstrap用JavaScriptの読み込み -->
<script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"
></script>
<script>
↓Vueの設定を以下に切り出します。
</script>
{% endblock content %}

report.html(ロジック部分)

Vue.jsで画面上での処理を制御しています。

妻への請求明細のCSVファイル作成は、力技ですがJavaScriptで作成してダウンロードまでしています。

    const reportApp = new Vue({
    delimiters: ['[[', ']]'],
    el: '#reportApp',
    data: {
        list: [],
        rent: *****,
        savings: *****,
        electricBill: 0,
        waterBill: 0,
        gasBill: 0,
        waterServerBill: 0
    },
    mounted: function() {
        // 初期値を設定する
        {% for cardDetail in detailList %}
        this.list.push(
            /*
             * payDate<string>:支払日
             * paymentDestination<string>:支払先
             * paymentDestination<string>:支払金額
             * checked<boolean>: true:計算対象外 false:計算対象
             * rowIndex<Number>: 計算対象の制御処理に使う
             */
            {
                "payDate": "{{ cardDetail.payDate|date:"Y/m/d" }}",
                "paymentDestination": "{{ cardDetail.paymentDestination }}",
                "payment": {{ cardDetail.payment }},
                "dataType": {{ cardDetail.dataType }},
                "checked": false,
                "rowIndex": {{ forloop.counter0 }}
            }
        )
        {% endfor %}
        },
        computed: {
            total() {
                const cardTotal = this.list.reduce((sum, cardDetail) => {
                    // 計算対象外の場合はそのまま返す
                    if (cardDetail.checked) {
                        return sum;
                    }
                    return sum + (cardDetail.payment)
                },0);
                // 請求額合計 = 家賃 + 水道光熱費 + カード請求額
                const total = 
                    Number(this.rent)
                    + Number(this.electricBill)
                    + Number(this.waterBill)
                    + Number(this.gasBill)
                    + cardTotal
                return total;
            },
            billedAmount() {
                return Math.floor(this.total / 2) - this.savings - this.waterServerBillAmount;
            },
            waterServerBillAmount() {
                return Math.floor(this.waterServerBill / 2);
            }
        },
        methods: {
            /*
            * 計算対象、対象外を切り替える
            */
            changeSumTarget(rowIndex) {
                const cardDetail = this.list[rowIndex];
                cardDetail.checked = !cardDetail.checked;
                this.list[rowIndex] = cardDetail;
            },
            convertDataType(dataType) {
                switch (dataType) {
                    case 1:
                        return "家賃光熱費";
                        break;
                    case 2:
                        return "Dカード";
                        break;
                    case 3:
                        return "イオンカード";
                        break;
                    case 4:
                        return "ドコモ";
                        break;
                    default:
                        return "その他"
                        break;
                }
            },
            download() {
                // csv出力レコード作成
                let outputStr = this.createOutputRecord();
                const fileName = "請求内訳_" + (new Date().getMonth() + 1) + "月分"  + ".csv";
                // BOMを付与(Excelの文字化け対策)
                const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
                // Blobでデータを作成する
                const blob = new Blob([bom, outputStr], { type: "text/csv" });

                //BlobからオブジェクトURLを作成する
                const url = (window.URL || window.webkitURL).createObjectURL(blob);
                //ダウンロード用にリンクを作成する
                const download = document.createElement("a");
                //リンク先に上記で生成したURLを指定する
                download.href = url;
                //download属性にファイル名を指定する
                download.download = fileName;
                //作成したリンクをクリックしてダウンロードを実行する
                download.click();
                //createObjectURLで作成したオブジェクトURLを開放する
                (window.URL || window.webkitURL).revokeObjectURL(url);
            },
            createOutputRecord() {
                // ヘッダー行
                let outputStr = "\"分類\",\"支払日\",\"支払先\",\"支払金額\"\n";
                // 固定費用
                outputStr +=
                    "\"家賃\",\"-\",\"-\"," + this.rent + "\n"
                    + "\"貯金\",\"-\",\"-\"," + "-" + this.savings + "\n"
                    + "\"電気代\",\"-\",\"-\"," + this.electricBill + "\n"
                    + "\"水道代\",\"-\",\"-\"," + this.waterBill + "\n"
                    + "\"ガス代\",\"-\",\"-\"," + this.gasBill + "\n"
                    + "\"ウォーターサーバー代\",\"-\",\"-\"," + "-" + this.waterServerBillAmount + "\n";
                this.list.reduce((sum, cardDetail) => {
                    // 計算対象外の場合はそのまま返す
                    if (cardDetail.checked) {
                        return;
                    }
                    // 流動費用
                    outputStr +=
                        "\"" + this.convertDataType(cardDetail.dataType) + "\"" + ","
                        + "\"" + cardDetail.payDate + "\"" + ","
                        + "\"" + cardDetail.paymentDestination + "\"" + ","
                        + "\"" + cardDetail.payment + "\"" + "\n";
                    return;
                },0);
                // 合計額、請求額
                outputStr +=
                    "\"-\",\"-\",\"合計\"," + this.total + "\n"
                    + "\"-\",\"-\",\"請求額(合計額/2 - 貯金(**,**0) - ウォーターサーバー代の半額)\"," + this.billedAmount;
                return outputStr;
            }
        }
    });

CSS

見た目を調整するひな形です。

見た目はBootstrap5でほぼ調整出来ますが、細かい部分はstyle指定をする必要があるため、
1ファイルだけ用意しています。

index.css

テーブルヘッダーを固定するためのstyle指定をしています。

thead {
    /* 縦スクロール時に固定する */
    position: -webkit-sticky;
    position: sticky;
    top: 0;
    /* tbody内のセルより手前に表示する */
    z-index: 1;
}

#tableWrapper {
    height: 650px;
    overflow-y: scroll;
}

pyファイル

pythonのテキストファイルです。
Djangoはpythonのフレームワークですので、MVCほぼ全てpyファイルで実装出来るようです。

forms.py

MVC概念のV(View)の部分です。
Djangoはこのforms.pyに入力フォームを定義し、HTML側で呼び出すことで簡単にフォームを作れるようです。

正直まだうまく使いこなせていない感じがします。

from django import forms

class CSVUploadForm(forms.Form):
    aeonCardCSV = forms.FileField(label='CSVファイル')
    dCardCSV = forms.FileField(label='CSVファイル')
    billingDate = forms.DateField(
        label='請求日',
        required=True,
        widget=forms.DateInput(attrs={"type": "date"}),
        input_formats=['%Y-%m-%d']
    )

class SearchForm(forms.Form):
    targetDate = forms.DateField(
        label='対象日',
        required=True,
        widget=forms.DateInput(attrs={"type": "date"}),
        input_formats=['%Y-%m-%d']
    )

class DeleteForm(forms.Form):
    deleteDate = forms.DateField(
        label='削除対象日',
        required=True,
        widget=forms.DateInput(attrs={"type": "date"}),
        input_formats=['%Y-%m-%d']
    )

views.py

ファイル名の割に、MVC概念のC(Controller)にあたる部分の認識です。

GETやPOSTのルーティングをしている感じでしょうか?

ちょっとまだ深く理解できていないです。

import csv, io, datetime, os
import datetime
from django.db.models.fields import BigIntegerField
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse, HttpResponseRedirect
from django.views import generic
from django.urls import reverse_lazy
from datetime import datetime as dt
from django.db import connection
from django.db.models import Sum
from .forms import CSVUploadForm, SearchForm, DeleteForm
from .utils import Validator
from .models import CardPayment
from .DataType import DataType
from . import graph

class list(generic.ListView):
    context_object_name = 'detailList'
    model = CardPayment
    template_name = 'Report/report.html'
    success_url = reverse_lazy('index')

class index(generic.FormView, generic.ListView):
    context_object_name = 'detailList'
    model = CardPayment
    template_name = 'Report/report.html'
    success_url = reverse_lazy('index')
    form_class = CSVUploadForm
    resultList = []
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs) # はじめに継承元のメソッドを呼び出す
        context.update({
            'CSVUploadForm': CSVUploadForm(**self.get_form_kwargs()),
            'SearchForm': SearchForm(**self.get_form_kwargs()),
            'DeleteForm': DeleteForm(**self.get_form_kwargs()),
        })
        #グラフオブジェクト
        qs    = CardPayment.objects.all().order_by('billingDateYM')  #モデルクラス(ProductAテーブル)読込
        x     = [x.billingDateYM for x in qs]           #X軸データ
        y     = [y.payment for y in qs]        #Y軸データ
        chart = graph.Plot_Graph(x,y)          #グラフ作成
        context['chart'] = chart
        return context
    def post(self, request, *args, **kwargs):
        print('post')
        if 'btn_search' in request.POST:
            print('btn_searchが押されている')
        if 'btn_delete' in request.POST:
            print('btn_deleteが押されている')
            targetDateStr = request.POST.get('deleteDate')
            if targetDateStr is not None:
                tdatetime = dt.strptime(targetDateStr, '%Y-%m-%d')
                targetDateYM = tdatetime.strftime('%Y-%m')
                CardPayment.objects.filter(billingDateYM = targetDateYM).delete()
                print('消した')
            return self.form_valid(request)
        return super().post(self, request, *args, **kwargs)
    def get_queryset(self):
        # デフォルトは全件取得
        results = self.model.objects.all()
        print('クエリセット')     

        # GETのURLクエリパラメータを取得する
        # 該当のクエリパラメータが存在しない場合は、[]が返ってくる
        targetDateStr = self.request.GET.get('targetDate')
        if targetDateStr is not None:
            tdatetime = dt.strptime(targetDateStr, '%Y-%m-%d')
            targetDateYM = tdatetime.strftime('%Y-%m')
            results = results.filter(billingDateYM = targetDateYM)
        else:
            targetDateYM = dt.now().strftime('%Y-%m')
            results = results.filter(billingDateYM = targetDateYM)
        return results
    def form_valid(self, form):
        if hasattr(form, 'POST') is False:
            billingDateYM = form.cleaned_data['billingDate'].strftime('%Y-%m')
            targetData = CardPayment.objects.filter(billingDateYM = billingDateYM)
            if targetData.count() > 0 :
                # 同じ請求月がある場合は先に削除する
                print('同じ請求月がある')
                targetData.delete()

            print(billingDateYM )

            # イオンカード利用明細取得
            aeonCardCsv = io.TextIOWrapper(form.cleaned_data['aeonCardCSV'])
            aeonCardReader = csv.reader(aeonCardCsv)
            resultList = self.read_aeoncard(aeonCardReader, billingDateYM )
            # Dカード利用明細取得
            dCardCsv = io.TextIOWrapper(form.cleaned_data['dCardCSV'])
            dCardReader = csv.reader(dCardCsv)
            resultList = resultList + self.read_dcard(dCardReader, billingDateYM )

            self.output_result(resultList)
            return super().form_valid(form)
        if 'btn_output' in form.POST:
            print(form.POST)
            billingDateYM = form.POST.get('billingDate').strftime('%Y-%m')
            targetData = CardPayment.objects.filter(billingDateYM = billingDateYM)
            if targetData.count() > 0 :
                # 同じ請求月がある場合は先に削除する
                print('同じ請求月がある')
                targetData.delete()

            print(billingDateYM )

            # イオンカード利用明細取得
            aeonCardCsv = io.TextIOWrapper(form.cleaned_data['aeonCardCSV'])
            aeonCardReader = csv.reader(aeonCardCsv)
            resultList = self.read_aeoncard(aeonCardReader, billingDateYM )
            # Dカード利用明細取得
            dCardCsv = io.TextIOWrapper(form.cleaned_data['dCardCSV'])
            dCardReader = csv.reader(dCardCsv)
            resultList = resultList + self.read_dcard(dCardReader, billingDateYM )

            self.output_result(resultList)
            return super().form_valid(form)
        else:
            return super().form_valid(form)
    
    def read_dcard(self, reader, billingDateYM):
        """
        Dカード利用明細CSVから必要なデータを取得する
        DカードのCSV構成
        ・1行目:カード名義人氏名, カード番号, カード種類 ←不要
        ・2行目以降~ドコモ利用者名がくるまで:利用日付, 利用店名, 利用額, ...
        ・ドコモ利用者名以降:月末の日付, ドコモ利用料金/iD, 利用額, ... ←不要
                          :月末の日付, ドコモ決済サービス等/iD, 利用額, ...
        """
        resultList = []
        validator = Validator()
        isDocomoPayment = False
        for index, row in enumerate(reader):
            if index == 0:
                continue
            if validator.isDate(row[0]):
                dataType = DataType.Dカード
                if isDocomoPayment:
                    dataType = DataType.ドコモ
                dateTime = datetime.datetime.strptime(row[0], '%Y/%m/%d')
                payDate = datetime.date(dateTime.year, dateTime.month, dateTime.day)
                print('payDate')
                print(payDate)
                # 日付、利用店名、利用額を取得する
                resultList.append(
                    CardPayment(
                        # payDate = row[0],
                        billingDateYM = billingDateYM,
                        payDate = payDate,
                        paymentDestination = row[1],
                        payment = row[5],
                        dataType = dataType)
                )
            else:
                isDocomoPayment = True
        return resultList

    def read_aeoncard(self, reader, billingDateYM):
        """
        イオンカード利用明細CSVから必要なデータを取得する
        イオンカードのCSV構成
        ・1行目:カードの種類
        ・2行目:請求額合計
        ・3行目:支払日
        ・4行目:指定口座
        ・5行目:金融機関ラベル, 支店ラベル, 口座番号ラベル, 名義人ラベル
        ・6行目:金融機関, 支店, 口座番号, 名義人
        ・7行目:ご利用明細
        ・8行目以降:ご利用日, 利用者区分, ご利用先, 支払方法, , , ご利用金額, 備考
        """ 
        resultList = []
        for index, row in enumerate(reader):
            if index < 7:
                continue
            if str.isdigit(row[0]):
                strPrefix2Date = str(datetime.date.today().year)[0:2]
                dateTime = datetime.datetime.strptime(strPrefix2Date + row[0], '%Y%m%d')
                payDate = datetime.date(dateTime.year, dateTime.month, dateTime.day)
                print('payDate')
                print(payDate)
                # 日付、利用店名、利用額を取得する
                resultList.append(
                    CardPayment(
                        # payDate = row[0],
                        billingDateYM = billingDateYM,
                        payDate = payDate,
                        paymentDestination = row[2],
                        payment = int(row[6]),
                        dataType = DataType.イオンカード)
                )
        return resultList
    
    def output_result(self, resultList):
        """
        各明細取得結果を出力する
        """
        print ("BulkCreate開始")
        CardPayment.objects.bulk_create(resultList)
        print ("BulkCreate終了")
        # filePath = "Report/output/card_detail_" + str(datetime.date.today()) + ".csv"
        # if os.path.isfile(filePath):
        #     os.remove(filePath)
        # with open(filePath, 'w', newline="") as f:
        #     writer = csv.writer(f)
        #     writer.writerows(resultList)

models.py

MVC概念のM(Model)の部分です。

ここで定義したモデルがそのままSQLiteのテーブルとして、Djangoのマイグレーション機能で作成できます。

今回は「CardPayment」というテーブルのみ使用しています。

from django.db import models

class CardPayment(models.Model):
    """
    カード利用明細
    """
    id = models.AutoField(primary_key=True)
    billingDateYM = models.CharField('請求年月', max_length=7)
    payDate = models.DateField('支払日')
    paymentDestination = models.CharField('支払先', max_length=50)
    payment = models.BigIntegerField('金額')
    dataType = models.PositiveSmallIntegerField('データ種類')

DataType.py

定数ファイルです。

class DataType():
    家賃光熱費 = 1
    Dカード = 2
    イオンカード = 3
    ドコモ = 4

CardPaymentテーブルのdataTypeカラムには上記のいずれかのみ登録するようにしています。

graph.py

グラフ描画用の関数を集めたファイルです。

まだコピペしてきたままの状態のため、本当に欲しいグラフを描画出来ていません。

import matplotlib.pyplot as plt
import base64
from io import BytesIO

#プロットしたグラフを画像データとして出力するための関数
def Output_Graph():
	buffer = BytesIO()                   #バイナリI/O(画像や音声データを取り扱う際に利用)
	plt.savefig(buffer, format="png")    #png形式の画像データを取り扱う
	buffer.seek(0)                       #ストリーム先頭のoffset byteに変更
	img   = buffer.getvalue()            #バッファの全内容を含むbytes
	graph = base64.b64encode(img)        #画像ファイルをbase64でエンコード
	graph = graph.decode("utf-8")        #デコードして文字列から画像に変換
	buffer.close()
	return graph

#グラフをプロットするための関数
def Plot_Graph(x,y):
	plt.switch_backend("AGG")        #スクリプトを出力させない
	plt.figure(figsize=(10,5))       #グラフサイズ
	for i in range(1, len(x)+1):
		plt.text(i, y[i-1], y[i-1])
	plt.plot(x,y)                    #グラフ作成
	# plt.xticks(rotation=45)        #X軸値を45度傾けて表示
	plt.title("Payment History")     #グラフタイトル
	plt.xlabel("Date")               #xラベル
	plt.ylabel("Payment")            #yラベル
	plt.tight_layout()               #レイアウト
	graph = Output_Graph()           #グラフプロット
	return graph

コメントを書く