djangoをdockerコンテナで利用(16) – djangoで科目単位の集計

集計処理やっと作るところまで来た。

最初の頃にグラフ作れるかなってやってのたと、集計を組み合わせる。

結論

テストデータはこんな感じ。

月単位で経費を計上したレコードがある。

見た目ほぼそのままdockerコンテナであるmariadbのテーブル上でレコードとして存在する。

django-record-summary

これを、縦軸に科目、横軸に月として、その1年の金額と比率を集計させる。
今月売上なんぼ、機材なんぼ、新聞図書費なんぼとかわかる。

帳簿の一部としてwebページをpdfにして保管するから、グラフはそのとき省く様ためグラフ表示のスイッチをつけとく。

django-record-summary

さらに集計結果を円グラフ表示させる。
今月は旅費かかったなーとか、交際費もう少し使っても大丈夫かなーとか目安がわかる。

django-record-summary

現行php版での処理をdjangoで全部作り直したとき、過去7年分の経費レコードに対する金額合計が1円の差異もなく集計・表示されたときは嬉しかった。

あと少しでphpのdockerコンテナとおさらばできる。

科目単位の集計

今までのdjango作りこみは実在するテーブルに対応したモデルだけを扱ってきた。
今回は複数テーブルから読み込んだ結果を集計するモデルを作っていく。

「見せ方」を先に考えるから、そのためのモデルをまずは準備して次にテンプレートを準備してく。

modelsの準備

結局は科目の一覧なので、ベースは科目のマスタ(GVIS_mst_kamoku)。
そこに、月単位の合計を肉付けしてモデルを作る。

1月から12月までの列を用意して、経費テーブル(GvisKeihi)の中にある科目で集計するメソッドを定義する。

以下、帳簿の一部であるGvisChobo.pyのモデル定義。

from django.db import models
from django.utils import timezone
from pymysql import NULL

from datetime import datetime               ## 日付取得のため
from datetime import date, timedelta


## マスターテーブル使うため
from gvisWebApp.models.GvisMaster import GvisMstKeihishubetsu,GvisMstKamoku
from gvisWebApp.models.GvisKeihi import GvisKeihi

## マスターテーブル絞り込みのため
from django.db.models import Q,Sum

class GvisChobo(models.Model):
    kamokuno = models.IntegerField(verbose_name='科目No',db_column='kamokuNo')  # Field name made lowercase.
    def __str__(self):
        return str(self.kamokuno)

    kamokuname = models.CharField(verbose_name='科目名',db_column='kamokuName',max_length=20)  # Field name made lowercase.
    def __str__(self):
        return self.kamokuname

    kamokusetsumei = models.CharField(verbose_name='科目説明',db_column='kamokuSetsumei', max_length=200)  # Field name made lowercase.
    def __str__(self):
        return self.kamokusetsumei

    hyojiorder = models.IntegerField(db_column='hyojiOrder')  # Field name made lowercase.
    def __str__(self):
        return str(self.hyojiorder)

    keihitaisho = models.IntegerField(db_column='keihiTaisho')  # Field name made lowercase.
    def __str__(self):
        return str(self.keihitaisho)

    def get_kng01(self,year):
        object_list = GvisKeihi.objects.filter(workperiod__range = [date(year,1,1),date(year,2,1) - timedelta(days=1)])
        findkey = '%03d' % self.kamokuno + '-' + self.kamokuname
        p = object_list.filter(Q(kamoku=findkey)).aggregate(sum=Sum('kng'))['sum']
        kng = 0 if p is None else p

        return kng

    def get_kng02(self,year):
        object_list = GvisKeihi.objects.filter(workperiod__range = [date(year,2,1),date(year,3,1) - timedelta(days=1)])
        findkey = '%03d' % self.kamokuno + '-' + self.kamokuname
        p = object_list.filter(Q(kamoku=findkey)).aggregate(sum=Sum('kng'))['sum']
        kng = 0 if p is None else p

        return kng

    :
    (中略)
    :

    def get_kng12(self,year):
        object_list = GvisKeihi.objects.filter(workperiod__range = [date(year,12,1),date(year,12,31)])
        findkey = '%03d' % self.kamokuno + '-' + self.kamokuname
        p = object_list.filter(Q(kamoku=findkey)).aggregate(sum=Sum('kng'))['sum']
        kng = 0 if p is None else p

        return kng

    def get_KamokuTotal(self,year):
        object_list = GvisKeihi.objects.filter(workperiod__range = [date(year,1,1),date(year,12,31)])
        findkey = '%03d' % self.kamokuno + '-' + self.kamokuname
        p = object_list.filter(Q(kamoku=findkey)).aggregate(sum=Sum('kng'))['sum']
        kng = 0 if p is None else p

        return kng
    class Meta:
        managed = False
        db_table = 'GVIS_mst_kamoku'

例えばget_kng12はフォームで受け取った年で、12月の科目を集計してる。経費テーブルには「003-研究開発費」みたいな感じで、科目の頭に3桁の数字とハイフンが入っているから、それをキーにしてfilterしてる。

集計はQとaggregateを使えばすぐに取れる。

使ってない経費あるので、金額はレコードがなくてNoneが戻ることもあるから、そのときはゼロを設定する。

年間合計(get_KamokuTotal)と12月(get_kng12)は31日までって固定で書いているけど、他の月は末日が30だったり31だったりするから、「翌月1日の前日」って表現で- timedelta(days=1)って日付を引き算してる。

けっこうこの日付計算便利。

最後にget_KamokuTotalって定義で12か月分を集計させてる。

templatesとformsの準備

templates

科目を縦軸に取って、横軸に月単位の合計を表示させてる。
科目明細は、ファイルサーバに置いてるpdfへのリンクを入れてる。
pdfは昔に買った帳簿のつけ方の解説本の付録。

最後の列には、科目明細にある経費対象かどうかの表示として、経費の対象ならその年の経費に対する比率を計算させてる。

humanizeってのをロードしとくと、金額表記うまくやってくれる。
3桁ごとのカンマ入れたり、小数点以下がゼロのときは整数表示してくれる。

gvis_tagsってのは自分で定義したテンプレートから呼び出せる関数で、カスタムテンプレートの名前。これがあるおかげで、テンプレートの中の表示形式を編集したり、モデルに書いたメソッドを呼び出したりできる。

以下、テンプレートの定義。

{% extends 'gvisWebApp/base.html' %}
{% load static %}
{% load humanize %}   {# 数字の値を3桁のカンマ区切りで表示したいため追記 #}  
{% load gvis_tags %}  {# templatetagフォルダにカスタムタグ作って使うために追記 #}

{% block content %}
<center>

  {# 参考URL #}
  {# https://noumenon-th.net/programming/2019/12/18/django-search/ #}
  {# https://qiita.com/uenosy/items/3c8e220a01ae21546e1c #}
  {# https://djangobrothers.com/blogs/custom_template_tags_filters/ #}

  <h1>検索条件</h1>
  <form method="POST">
    {% csrf_token %}

    <fieldset style="width:60%">
      <legend align="center">検索フォーム</legend>

      {% for field in find_form %}
        {{ field.label }}:{{ field }}
        <br>
      {% endfor %}
      <input class="gvis_button" type="submit" id="submit1" name="submit1" value="検索">
    </fieldset>
  </form>
  <br>

  {% if messages %}
    {% for message in messages %}
      <h2> {{ message }}</h2>
    {% endfor %}
  {% endif %}

  <h1>検索結果</h1>
    <fieldset style="width:90%">
    {% if object_list|length == 0 %}
        <p>検索結果が存在しません。</p>
    {% else %}

      <h2>科目帳簿一覧 {{workperiodYYYY}} 経費合計 {{KeihiNenGokei}} グラフ {{graphDraw}} list {{object_list|length}} </h2>
      <table border=4 align=center width="100%">
        <tr>
          <th>科目</th>
          <th>
            <input class="darkmode-ignore" type="button" name="kamokuOrginURL_btn" onclick="window.open('http://nafslinux.intra.gavann-it.com/nariHTTP/data/GVIS_warehouse/warehouse3/800_書籍/実用/フリーランス&個人事業者のための確定申告/4_appendix.pdf')" value="科目明細">
          </th>
          <th>1月</th>
          <th>2月</th>
          <th>3月</th>
          <th>4月</th>
          <th>5月</th>
          <th>6月</th>
          <th>7月</th>
          <th>8月</th>
          <th>9月</th>
          <th>10月</th>
          <th>11月</th>
          <th>12月</th>
          <th>科目単位年度合計</th>
          <th>科目単位%</th>
        </tr>
        {% for GvisChobo in object_list %}
          <tr>
            {% if GvisChobo.keihitaisho == 1 %}
              <td bgcolor="#166985"><font color="#ffffff">{% gvis_zeropad3 GvisChobo.kamokuno %}-{{GvisChobo.kamokuname}}</font></td>
            {% elif GvisChobo.kamokuno == 101 %}
              <td bgcolor="#FF312E"><font color="#ffffff">{% gvis_zeropad3 GvisChobo.kamokuno %}-{{GvisChobo.kamokuname}}</font></td>
            {% elif GvisChobo.kamokuno == 102 %}
              <td bgcolor="#FF312E"><font color="#ffffff">{% gvis_zeropad3 GvisChobo.kamokuno %}-{{GvisChobo.kamokuname}}</font></td>
            {% else %}
              <td>{% gvis_zeropad3 GvisChobo.kamokuno %}-{{GvisChobo.kamokuname}}</td>
            {% endif %}

            <td>{{GvisChobo.kamokusetsumei }}</td>
            <td>{{GvisChobo.id | gvisKamokuKng01:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng02:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng03:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng04:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng05:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng06:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng07:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng08:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng09:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng10:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng11:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKng12:workperiodYYYY | floatformat | intcomma }}円 </td>
            <td>{{GvisChobo.id | gvisKamokuKngTotal:workperiodYYYY | floatformat | intcomma }}円 </td>

            {% if GvisChobo.keihitaisho == 1 %}
              <td>{{GvisChobo.id | gvisKamokuKngTotal:workperiodYYYY | gvis_hiritsu:KeihiNenGokei}}</td>
            {% else %}
              <td>経費外</td>
            {% endif %}

          </tr>
        {% endfor %} 

      </table>
      <p>■</p>
      <p>■</p>

      {% if graphDraw == 'on' and object_list|length > 0 %}
        <img src="{% url 'gvisWebApp:ENplot' workperiodYYYY %}" width=1400 height=800 align=middle>
      {% endif %}

    {% endif %}
    </fieldset>

</center>

{% endblock content %}

カスタムテンプレートフィルタをめっちゃ使ってる。
gvisKamokuKng01からgvisKamokuKng12まで定義し、モデルのidと、フォームの年を受け取らせて、モデル内のメソッドを呼び出す。

gvis_hiritsuは科目合計を年間経費で割り算して、比率をパーセンテージで返してくるのを表示させてる。

最後に、フォームのグラフ表示スイッチ(boolean)が入ってて検索結果があれば、円グラフ表示させている。

booleanだからテンプレートの中でTrueとかFalseって判断するのかなって思ってたら、実際の値を調べるとgraphDraw == 'on'って書かないとifにひっかかってくれんかった。

forms

フォームはこんな感じ。

from django import forms

class GvisChoboForm(forms.Form):

    workperiodYYYY = forms.ChoiceField(
        label='経費登録年', initial='',
        choices = (
            ('2014','2014'),
            ('2015','2015'),
            ('2016','2016'),
            ('2017','2017'),
            ('2018','2018'),
            ('2019','2019'),
            ('2020','2020'),
            ('2021','2021'),
            ('2022','2022'),
            ('2023','2023'),
            ('2024','2024'),
            ('2025','2025'),
            ('2026','2026'),
            ('2027','2027'),
            ('2028','2028'),
            ('2029','2029'),
            ('2030','2030'),
        ),
        required=True,
        widget=forms.widgets.Select,
    )

    graphDraw = forms.BooleanField(label='グラフ表示on',initial=True,required=False)

templatetags

djangoのテンプレートは書きにくい。
あんまり使いやすいとは思えない。

そこでカスタムテンプレートを作って定義しておき、本来の表示用テンプレートから呼び出すとうまく処理してくれる。

ビルトインっていって既存のものもあるらしいけど、djangoがバージョンアップして仕様がホイホイ変わっていっても困るし、自分で作ってみたかったし。

定義の仕方とか、使い方に癖があるけど、まぁ使える。
タグとフィルタってのがある。

引数の数が1つならタグ、2つならフィルタを使うってなんかヘンテコ。
それ以上のときは自分でセパレータ考えて渡せってことか?

例えば、n=a(p,q,r)やったら、n=a(p,q + '|' + r)とかやって、'|'を使って受取先でセパレートするとかやな。

テンプレートから呼び出すとき、a(p,q)p | a:qって書かなあかんらしい。1つ目の引数から先に書かなアカンってわかりにくいなぁ。

扱い方を書いておられる方がおられた。
作者さんありがとう。

【Django】カスタムテンプレートフィルタ・テンプレートタグの作り方 - DjangoBrothers
PythonをベースとしたWebフレームワーク『Django』のチュートリアルサイトです。入門から応用まで、レベル別のチュートリアルで学習することができます。徐々にレベルを上げて、実務でDjangoを使えるところを目指しましょう。ブログでは...
タグ

以下、まずは定義の前半。

from django import template
from datetime import datetime

register = template.Library() # Djangoのテンプレートタグライブラリ

    :
    (中略)
    :

# カスタムタグとして登録する
@register.simple_tag
def gvis_zeropad4(value1):
    if value1 is None : return '0000'
    return '%04d' % int(value1) if str(value1).isdigit() else '0000'

@register.simple_tag
def gvis_zeropad3(value1):
    if value1 is None : return '000'
    return '%03d' % int(value1) if str(value1).isdigit() else '000'

    :
    (中略)
    :

@register.simple_tag
def gvis_today():
    dt_year = datetime.now().year
    dt_mon = '%02d' % datetime.now().month
    dt_day = '%02d' % datetime.now().day
    dt_today = str(dt_year) + '-' + dt_mon + '-' + dt_day

    return dt_today

gvis_zeropad4ってのは書式指定して数字の文字列を返す。
1230123になって戻る。
isdigitで判断しちゃいけないのかもしれないけど、今はこのまま。

gvis_todayは今日の日付をハイフン区切りの文字列で戻す。
2022-3-4なら2022-03-04って返す。

こんなの書かなくてもタグあるのかもしれないけど、書いてみたかったのでこのまま。

フィルタ

次、後半のフィルタ。

モデルの中に書いたメソッドに引数渡したかったのを、同じようなこと考えて実際にやっておられる方がおられた。

たいへん参考になった。
作者さんありがとう。

djangoテンプレート上でmodelのメソッドに引数を渡す方法(djangoで出勤簿アプリ試作中♪) | ななうぇぶのブログ
三十路のママさんWEBプログラマーです。主な活動領域はphp,symfony,Symfony2,OpenPNE3,python,django,jqueryあたり。
@register.filter
def gvis_hiritsu(value1,value2):

    # 数字変換がうまくできるかやってみて、うまく行かなければそのエラーを返し、そうでなければざっくりしたパーセンテージを戻す
    try:
        v1 = int(value1)
        v2 = int(value2)
    except Exception as e:
        return 'v1=' + str(value1) + ' v2=' + str(value2) + ' exeption={' + str(e) + '}'
    else:
        ## return ".2%" % int(value1) / int(value2)
        if v1 > 0 and v2 > 0 :
            return "{:.2%}".format(v1 / v2)
        else:
            return '0.00%'

@register.filter
def gvisKamokuKng01(value,args):
    from gvisWebApp.models.GvisChobo import GvisChobo
    return GvisChobo.objects.get(pk=value).get_kng01(int(args))

@register.filter
def gvisKamokuKng02(value,args):
    from gvisWebApp.models.GvisChobo import GvisChobo
    return GvisChobo.objects.get(pk=value).get_kng02(int(args))

    :
    (中略)
    :
@register.filter
def gvisKamokuKng12(value,args):
    from gvisWebApp.models.GvisChobo import GvisChobo
    return GvisChobo.objects.get(pk=value).get_kng12(int(args))

@register.filter
def gvisKamokuKngTotal(value,args):
    from gvisWebApp.models.GvisChobo import GvisChobo
    return GvisChobo.objects.get(pk=value).get_KamokuTotal(int(args))

gvis_hiritsuは科目合計を年間経費で割り算して、比率をパーセンテージで返す。割り算に失敗したら画面上ですぐにわかるようにexceptionの内容を表示させてる。

まぁ、自分用やからexception結果を表示させてもいいってことで。

gvisKamokuKng01は末尾に数字が1から12までのものがある。モデルに書いた12か月分のメソッドに年を引数にしてそれぞれ呼び出すことで、縦軸の科目に対する横軸の月単位合計を表示するための金額を返してくれる。

gvisKamokuKngTotalは1年分の科目金額を返してくれる。

モデルのメソッドに引数を渡す表記に悩んだなぁ。
get(pk=lalue)ってのにたどりつけた時は嬉しかった。

urls.pyの準備

次に書くviewsの前半部分を用意したら書き足した。

    path('gvis_600_ChoboKensaku/', gvis_chobo_views.GvisChoboList.as_view(), name="gvis_600_ChoboKensaku"),
    path('ENplot/<int:year>', gvis_chobo_views.get_ENpng, name='ENplot')   # 円グラフを描くための設定

viewsの準備

普通にListViewだけを使う。
更新とかはせず、単に表示させるのみなのでCreateViewとかは要らない。

他の画面と違ってページネーションも要らないし、from ~ importで削り忘れてるものもあったかも。

帳簿データ取得と表示のための定義

前半の定義。
共通関数はテーブルにログ出力する箇所(gv_logwrite)を使ってる。

from django.contrib.auth.decorators import login_required
from django.http import request
from django.utils import timezone

from ..models import GvisChobo               ## DB読み込みのため
from django.views.generic import (
                            ListView,       ## リスト表示のため
)
from django.db.models import (  Q,          ## 検索のため
                                Max,        ## 最大値取得のため      
                                Sum,        ## 合計表示させるため
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse,reverse_lazy
                                            ## urlを返すreverse/reverse_lazyを使うため
from ..forms import GvisChoboForm           ## 検索フォームを使うため
from django.contrib import messages         ## 通知メッセージを表示するため
from datetime import datetime               ## テーブルの日付フィールドを扱うため
from ..common.common import *               ## 共通関数
from django.shortcuts import redirect       ## ページ表示のため

from django.http import HttpResponse
import numpy as np                          ## 多次元配列や数値計算のため(要pipインストール 'pip3 install numpy')
import matplotlib                           ## (要pipインストール 'pip3 install matplotlib')
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import io

from django.contrib.auth.decorators import login_required   ## 関数でログイン必須にするため

class GvisChoboList(LoginRequiredMixin, ListView):

    model = GvisChobo
    template_name = 'gvisWebApp/gvis_600_ChoboKensaku.html'

    def post(self, request, *args, **kwargs):
        chobo_form_value = [
            self.request.POST.get('workperiodYYYY', None),
            self.request.POST.get('graphDraw', None),
        ]
        request.session['chobo_form_value'] = chobo_form_value

        return self.get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # 年指定のデフォルトを設定しておく(今日の年)
        workperiodYYYY = gvis_today()[0:4]
        graphDraw = True

        # sessionに値がある場合、その値をセットする。(ページングしてもform値が変わらないように)
        if 'chobo_form_value' in self.request.session:
            chobo_form_value = self.request.session['chobo_form_value']
            workperiodYYYY = chobo_form_value[0]
            graphDraw = chobo_form_value[1]

        default_data = {'workperiodYYYY': workperiodYYYY,   # 経費年
                        'graphDraw': graphDraw,             # グラフ表示スイッチ
                        }
        find_form = GvisChoboForm(initial=default_data) # 検索フォーム
        context['find_form'] = find_form

        # 年単位科目合計を取得する
        KeihiNenGokei = gv_Func_GetKeihiTotal(workperiodYYYY,'00',self.request)

        context.update({
            'workperiodYYYY': workperiodYYYY,   # 登録年
            'graphDraw': graphDraw,
            'KeihiNenGokei': KeihiNenGokei,     # 登録年の経費合計
        })

        return context

    def get_queryset(self):

        # sessionに値がある場合、その値でクエリ発行する。
        # シリアル種別はicontains(部分一致の大文字小文字区別なし)で検索する

        if 'chobo_form_value' in self.request.session:
            chobo_form_value = self.request.session['chobo_form_value']

            workperiodYYYY = chobo_form_value[0]

            ## まずは全件取得
            object_list = GvisChobo.objects.all().order_by('hyojiorder')

            ## ログ出力
            key = workperiodYYYY
            gv_logwrite('info',self.request,'dj-科目帳簿検索','(' + key + ')')

            return object_list
        else:
            # 何も返さない
            return GvisChobo.objects.none()

円グラフの表示

集計の量はまぁまぁあるけど、高性能なPCじゃなくて非力なfire8タブレットでも1秒ぐらいで集計結果取れる。

ただグラフ表示入れたら、5秒ぐらいかかる。
毎年保管する帳簿のpdfにはグラフ不要なので、このスイッチはやっぱり必要。

以下、後半のviews。

get_ENpngがテンプレートから直接呼び出され、numpyにグラフ表示のための要素を入れておき、setPltを呼び出してグラフを描かせ、plt2pngでpng画像を返す。

## 円グラフ表示の習作からそのまま持ってきた
def setPlt(chart_item,values):
    # 日本語フォントを使う
    plt.rcParams['font.family'] = 'IPAexGothic' #全体のフォントを設定

    # ドーナツグラフ - ここから
    fig, ax = plt.subplots(figsize=(24,12), subplot_kw=dict(aspect="equal"))

    # ラベルのフォントサイズ
    plt.rcParams['font.size'] = 19.0

    # 背景色を設定(値は1まででrgbを指定、淡いグレーにしたかったので0.8を指定)
    fig.set_facecolor((0.8,0.8,0.8))

    # グラフの描画
    wedges, texts = ax.pie(values, wedgeprops=dict(width=0.9), counterclock=False, startangle=90)

    bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72)
    kw = dict(arrowprops=dict(arrowstyle="-"),
            bbox=bbox_props, zorder=0, va="center")

    # 注釈入れて表示、参考URLそのまんま
    for i, p in enumerate(wedges):
        ang = (p.theta2 - p.theta1)/2. + p.theta1
        y = np.sin(np.deg2rad(ang))
        x = np.cos(np.deg2rad(ang))
        horizontalalignment = {-1: "right", 1: "left"}[int(np.sign(x))]
        connectionstyle = "angle,angleA=0,angleB={}".format(ang)
        kw["arrowprops"].update({"connectionstyle": connectionstyle})
        ax.annotate(chart_item[i], xy=(x, y), xytext=(1.35*np.sign(x), 1.4*y),
                    horizontalalignment=horizontalalignment, **kw)

    # タイトルを上端にセット
    ax.set_title("経費内科目比率TOP9+その他",fontsize = 32,x=0.5,y=1.05)

    # 凡例の表示
    plt.legend(wedges, chart_item,
                fontsize=19,
                ## bbox_to_anchor=(0.0,0.7),
                bbox_to_anchor=(1.34,1.1),
                loc='upper left'
                )

## png化
def plt2png():
    buf = io.BytesIO()                          # バッファを作成
    plt.savefig(buf, format='png', dpi=200)     # バッファにpngを一時保存
    s = buf.getvalue()                          # バッファの内容を渡す
    buf.close()                                 # バッファはクローズ
    return s

## グラフ表示を実行するビュー関数
@login_required
def get_ENpng(request,year):

    try:

        ## 科目単位高額TOP10を取ってくる
        p = gv_Func_KamokuTop10(request,year)

        ## 参考URL  https://qiita.com/sho11hei12-1998/items/2458aa0822cc6e7268fa
        chart_item = p[0,0:10]
        values = p[1,0:10]

        key = year        
        gv_logwrite('info',request,'dj-グラフ描画','key=(' + str(key) + ')')
        gv_logwrite('info',request,'dj-グラフ要素1','chart_item=(' + str(chart_item) + ')')
        gv_logwrite('info',request,'dj-グラフ要素2','values=(' + str(values) + ')')

        ## グラフ要素をセットしてpng化
        setPlt(chart_item,values)
        png = plt2png()

        ## グラフをリセットしてから画像を返す
        plt.cla()
        response = HttpResponse(png, content_type='image/png')
        return response

    except Exception as e:

        ## グラフ表示するまでにエラーが発生したらログに書き出す
        key = year
        gv_logwrite('error',request,'dj-グラフ描画','key=(' + str(key) + ')' + 'exeption={' + str(e) + '}')

setPltは引数に「2022」とか年指定が入ってきて、共通関数のgv_Func_KamokuTop10を呼び出してデータベースからグラフ表示のための要素をnumpyの配列で受け取る。

setPltは位置の調整のための書き換えしたのみで、以前に作ったものそのまま利用。

plt2pngは未だになぜこれだけで画像ができるのかがよくわかってない。苦手な箇所の1つやなぁ。

年間経費取得処理(共通関数)

gv_Func_KamokuTop10は直接SQL発行してnumpyに格納する。

円グラフのためのデータを作る(ダミー)

最初に処理書く前にこんな具合で直接値を返して表示テストしてた。

def gv_Func_KamokuTop10(request,year):
  ## サンプルこんな感じで「その他」は100から他の要素の数字を引いた数字
  p = np.array([
      ["003-研究開発費-40%",
        "015-車両関係費-21%",
        "016-修繕費-10%",
        "019-新聞図書費-6%",
        "021-接待交際費-5%",
        "022-租税公課-4%",
        "026-荷造運賃-3%",
        "017-消耗品費-2%",
        "010-研修費-1%",
        "その他-8%"]
  ,
      [40,21,10,6,5,4,3,2,1,8]
  ])

  return p
円グラフのためのデータを作る(SQL発行)

だいたいの円グラフ表示がうまく行ったら、numpyにSQLの実行結果を入れるように書き換えた。

djangoのモデル使うことも考えたけど、php版の中で使ってるSQLを流用してすぐに動かしたかったのでSQL直接発行。

matplotのサイトにある円グラフ作成処理がnumpy使ってるので、丸パクリした自分としては、同じようにデータが入るように書いた。

def gv_Func_KamokuTop10(request,year):

    ## モデルを使わずにsql発行
    try:
        con = connections['default']
        cursor = con.cursor()

        SQLstr = ""
        SQLstr += "select * from ( "
        SQLstr += "    select Kamoku,sum(Kng) as Kng "
        SQLstr += "    from (  "
        SQLstr += "        select  "
        SQLstr += "            GVIS_keihi.Kamoku,  "
        SQLstr += "            GVIS_keihi.Tehai,  "
        SQLstr += "            GVIS_keihi.Kng,  "
        SQLstr += "            (  "
        SQLstr += "                select keihitaisho  "
        SQLstr += "                from GVIS_mst_kamoku  "
        SQLstr += "                where concat(concat(LPAD(GVIS_mst_kamoku.kamokuNo,3,'0'),'-'),GVIS_mst_kamoku.KamokuName)  "
        SQLstr += "                    = GVIS_keihi.Kamoku  "
        SQLstr += "            ) as keihitaisho  "
        SQLstr += "        from GVIS_keihi  "
        SQLstr += "        WHERE year(GVIS_keihi.workPeriod) = " + str(year)
        SQLstr += "        ) as q  "
        SQLstr += "    where keihitaisho = 1 "
        SQLstr += "    group by Kamoku "
        SQLstr += ") as p "
        SQLstr += "order by Kng desc "
        SQLstr += "limit 0,9 "

        cursor.execute(SQLstr)
        ret = cursor.fetchall()

        ## SQL実行結果は例えばこう格納される。格納したいリストに対して行列がひっくり返ってる。
        ## gv_logwrite('info',request,'dj-sql結果1','ret[0][0]=(' + str(ret[0][0]) + ')') ## ret[0][0]=(030-旅費交通費)
        ## gv_logwrite('info',request,'dj-sql結果2','ret[0][1]=(' + str(ret[0][1]) + ')') ## ret[0][1]=(292934.0000)
        ## gv_logwrite('info',request,'dj-sql結果3','ret[1][0]=(' + str(ret[1][0]) + ')') ## ret[1][0]=(011-広告宣伝費)
        ## gv_logwrite('info',request,'dj-sql結果4','ret[1][1]=(' + str(ret[1][1]) + ')') ## ret[1][1]=(260000.0000)
        ## gv_logwrite('info',request,'dj-sql結果5','ret[0]=(' + str(ret[0]) + ')') ## ret[0]=(('030-旅費交通費', Decimal('292934.0000')))
        ## gv_logwrite('info',request,'dj-sql結果6','ret[1]=(' + str(ret[1]) + ')') ## ret[1]=(('011-広告宣伝費', Decimal('260000.0000')))

        q = np.array(ret)
        ## gv_logwrite('info',request,'dj-numpy格納結果1','q=(' + str(q) + ')')
        ## q=([['030-旅費交通費' Decimal('292934.0000')] ['011-広告宣伝費' Decimal('260000.0000')] ['008-給料賃金' Decimal('240000.0000')] ['017-消耗品費' Decimal('207060.0000')] ['001-打合会議費' Decimal('131690.0000')] ['031-IT機材費' Decimal('127196.0000')] ['013-支払手数料' Decimal('111348.0000')] ['021-接待交際費' Decimal('105093.0000')] ['003-研究開発費' Decimal('59356.0000')]])

        ## 行列をひっくり返す
        p = q.T
        ## gv_logwrite('info',request,'dj-numpy格納結果1','p=(' + str(p) + ')')
        ## p=([['030-旅費交通費' '011-広告宣伝費' '008-給料賃金' '017-消耗品費' '001-打合会議費' '031-IT機材費' '013-支払手数料' '021-接待交際費' '003-研究開発費'] [Decimal('292934.0000') Decimal('260000.0000') Decimal('240000.0000') Decimal('207060.0000') Decimal('131690.0000') Decimal('127196.0000') Decimal('111348.0000') Decimal('105093.0000') Decimal('59356.0000')]])

        ## 年間経費取得
        NenKng = gv_Func_GetKeihiTotal(str(year),'00',request)

        ## グラフ横の凡例に表示させるため、数値箇所を項目にくっつける
        p[0,0] = p[0,0] + '-' + "{:.1%}".format(p[1,0] / NenKng)
        p[0,1] = p[0,1] + '-' + "{:.1%}".format(p[1,1] / NenKng)
        p[0,2] = p[0,2] + '-' + "{:.1%}".format(p[1,2] / NenKng)
        p[0,3] = p[0,3] + '-' + "{:.1%}".format(p[1,3] / NenKng)
        p[0,4] = p[0,4] + '-' + "{:.1%}".format(p[1,4] / NenKng)
        p[0,5] = p[0,5] + '-' + "{:.1%}".format(p[1,5] / NenKng)
        p[0,6] = p[0,6] + '-' + "{:.1%}".format(p[1,6] / NenKng)
        p[0,7] = p[0,7] + '-' + "{:.1%}".format(p[1,7] / NenKng)
        p[0,8] = p[0,8] + '-' + "{:.1%}".format(p[1,8] / NenKng)

        ## 最後の要素に経費の合計からTop1〜9を引き算した「その他」を追加する
        Sonota = NenKng - p[1,0] - p[1,1] - p[1,2] - p[1,3] - p[1,4] - p[1,5] - p[1,6] - p[1,7] - p[1,8]
        r = np.array([["その他" + '-' +  "{:.1%}".format(Sonota / NenKng) ],[str(Sonota)]] )
        ## r = np.array([["その他"],[str(Sonota)]] )
        gv_logwrite('info',request,'dj-numpy格納結果2','r=(' + str(r) + ')')

        s = np.append(p,np.array(r),axis=1)

    except Exception as e:
        key = year
        gv_logwrite('error',request,'dj-年間経費取得','key=(' + key + ')' + 'exeption={' + str(e) + '}')

    return s

SQLの実行結果はret = cursor.fetchall()で取っておく。
そのときの結果は、複数レコードのときどんなふうに入るのか。

gv_logwriteで出力された結果を格納具合を目で追った結果をコメントに埋め込んでおいた。

最初に思ってたのと違って、行列がひっくり返ってた。
行列って2次元までならイメージ湧くけど、それ以上は苦手やなぁ。

q = np.array(ret)ってレコードを格納したらp = q.Tで行列をひっくり返す。

あとは月指定に'00'を入れて年間経費を取ってきたものを使い、それぞれの科目の比率パーセンテージを格納する。

最後に「その他」をグラフ表示のための要素に追加する。

要素追加するのも悩んだ。
np.appendにたどりつけたのはよかったけど、今回の配列のときはaxis=1って書かなきゃいけなかった。

残務

グラフの表示はできてるけど、文字が小さい。
もう少し左に寄せて、凡例とかの位置も調整したい。

これがけっこう地味な作業で、面倒なのよ。

タイトルとURLをコピーしました