Pythonスクリプト（帳票出力）をDjangoから実行

## はじめに
弊社が開発・運用・保守している+Auctionでは、pdf帳票生成・メール送信をpythonスクリプトから実行しています
基本的は定期実行なので、cronで運用していますが、スポット的にイレギュラーな場合があり、定期実行から外れることがあります
そういう場合を考慮して、Django Admin（Web）から、実行できるようにしました
帳票生成（メール送信）スクリプトをurlリンクから実行させる、というシンプルな思想です

## 概要

1. ~/project/scriots/bin/create_join_invoice.pyを、~/project/app_name/views.pyにインポートし、CreateJoinInvoiceView()を設定する
1. adminにurlリンクを設定し、CreateJoinInvoiceView()を実行する


## 帳票作成スクリプト
pdfrw、reportlabを使って、pdfを生成しています
他、Taxモデル、os、datetime、settings等importしています

```scripts/bin/create_join_invoice.py
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas

def main():
    # フォントの設定
    ttf_file = settings.FONT_PATH
    pdfmetrics.registerFont(TTFont('IPAexGothic', ttf_file))
    
    # ファイル生成
    cc = canvas.Canvas(file_path + '/' + file_name)
    
    # テンプレート読み込み
    page = PdfReader(settings.JOIN_FEE_TEMPLATE_INVOICE_PDF, decompress=False).pages
    
    # 1ページ目をオブジェクトに
    pp = pagexobj(page[0])
    cc.doForm(makerl(cc, pp))
    
    # 宛先
    cc.setFont('IPAexGothic', 16)
    target_x, target_y = 20 * mm, 255 * mm
    cc.drawString(target_x, target_y, custom_user.company_name)　# defaultの描画（左寄せ）
    
    
    # 合計
    cc.setFont('IPAexGothic', 8)
    target_x, target_y = 145 * mm,  129 * mm
    cc.drawRightString(target_x, target_y, '¥ ' + str('{:,}'.format(join_fee.annual_fee + math.floor(sale_tax))))　
    # 右よせの描画

    #
    # 実際はこの部分に、明細、請求元、消費税等があるが、長くなる
    # または、importしているので、複雑になることから割愛
    #
    
    cc.showPage()
    cc.save()

if __name__ == '__main__':
    main()
```
1. IPAexGothicのインストール方法は、
[IPA Font ダウンロード | 一般社団法人 文字情報技術促進協議会](https://moji.or.jp/ipafont/ipafontdownload/)等を参照ください

## 帳票生成スクリプトをviews.pyにimport
create_join_invoice.pyをapp_name/views.pyにimportし、webから実行できるようにします
```app_name/views.py
from scripts.bin import create_join_invoice

class CreateJoinInvoiceView(SuperuserRequiredMixin, TemplateView) :
	def get(self, request, *args, **kwargs):
		context = {}
		context['back_link'] = '/admin/'+settings.ADMIN_PATH+'/app_name/script'
		context['title'] = '年会費（システム利用料）請求書生成スクリプト実行'
		context['detail'] = Script.objects.get(pk=self.kwargs['pk'])
		run_script = create_join_invoice.main()　# create_join_invoice.py内のmain()を呼び出す
		context['script'] = run_script

		return render(request, 'app_name/invoice/invoice.html', context)
```


## 定期実行スクリプト用のmodel
スクリプトファイル名をDBに登録しておきます
DBに登録することで、DjangoのAdminから実行しやすくなります
```models.py
class Script(BaseModel):
  title = models.CharField(verbose_name='タイトル', max_length=50, blank=False, null=False)
  detail = models.TextField(verbose_name='詳細・内容', blank=False, null=False)
  script_file_name = models.CharField(verbose_name='ファイル名', max_length=50, blank=False, null=False)
  run_time = models.CharField(verbose_name='実行日時', max_length=100, blank=False, null=False)
  url_link = models.CharField(verbose_name='実行スクリプトへのリンク', max_length=500, blank=True, null=True)
```
+ 共通Modelとして、BaseModelを必ず設定するようにしています
```BaseModel
class BaseModel(models.Model):
  class Meta:
    abstract = True

  created = models.DateTimeField(verbose_name='生成日時', auto_now_add=True)
  modified = models.DateTimeField(verbose_name='更新日時', auto_now=True)
  deleted = models.DateTimeField(verbose_name='削除日時', blank=True, null=True)
```
 
url_linkに実行するファイル名を登録することで、後述するリンクを生成しやすくなります

## Script ModelのAdmin画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14219/e6b7e687-c5f1-606b-4446-18e4129b52f8.png)


「スクリプト実行」をクリックすると、「CreateJoinInvoiceView()」から、「create_join_invoice.py」が実行され、帳票が生成される仕組みです
## 「スクリプト実行」リンクの生成
```admin.py
list_display = ('run_time', 'title', 'script_file_name', 'detail', 'run_script_link')

.....

def run_script_link(self, obj):
    if obj.url_link:
       return mark_safe(
			'<a href="/xxxxxxxxxxxx/'+settings.ADMIN_PATH+'/' + str(obj.url_link) + '/'+str(obj.pk)+'">'+obj.title+'実行</a>'
			)
    else:
        return ''

run_script_link.short_description = 'スクリプト実行'
```
セキュリテイ対策として、urlに「settings.ADMIN_PATH（32桁の「ランダムな文字列）」を挟んでいます

## app_nameアプリケーションのurl.pyより抜粋
```url.py
path('{obj.url_link}/<int:pk>', CreateJoinInvoice.as_view(), name='create_join_invoice'),
```

## 終わりに
昔、CakePHPで同様な処理を行ったことがあり、Djangoでもいけんじゃね？と思い、このような処理にしてみました
帳票出力のサンプルはよく見かけますが、帳票出力+Webから実行の合わせ技はなかなか無いのではと思い、記事にしました
どなたかの参考になれば幸甚です