Django➕VueでPDF自動作成機能実装
利用するためにクライアントのPCに事前にwkhtmltopdfを導入
htttp://wkhtmltopdf.org
❶.pdfkitをインストール
pip install django-pdfkit
❷.PDFを生成するためのdjangoビューを作成する
import pdfkit
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from rest_framework import viewsets
from rest_framework import status, authentication, permissions
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.response import Response
from .serializers import InvoiceSerializer, ItemSerializer
from .models import Invoice, Item
from team.models import Team
class InvoiceViewSet(viewsets.ModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.all()
def get_queryset(self):
return self.queryset.filter(created_by=self.request.user)
def perform_create(self, serializer):
team = self.request.user.teams.first()
invoice_number = team.first_invoice_number
team.first_invoice_number = invoice_number + 1
team.save()
serializer.save(created_by=self.request.user, team=team, modified_by=self.request.user, invoice_number=invoice_number, bankaccount=team.bankaccount)
def perform_update(self, serializer):
obj = self.get_object()
if self.request.user != obj.created_by:
raise PermissionDenied('Wrong object owner')
serializer.save()
@api_view(['GET'])
@authentication_classes([authentication.TokenAuthentication])
@permission_classes([permissions.IsAuthenticated])
def generate_pdf(request, invoice_id):
invoice = get_object_or_404(Invoice, pk=invoice_id, created_by=request.user)
team = Team.objects.filter(created_by=request.user).first()
template = get_template('pdf.html')
html = template.render({'invoice': invoice, 'team': team})
pdf = pdfkit.from_string(html, False, options={})
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="invoice.pdf"'
return response
❸.ビューをurlsファイルに追加する
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import InvoiceViewSet, ItemViewSet,generate_pdf
router = DefaultRouter()
router.register("invoices", InvoiceViewSet, basename="invoices")
router.register("items", ItemViewSet, basename="items")
urlpatterns = [
path('', include(router.urls)),
path('invoices/<int:invoice_id>/generate_pdf/', generate_pdf, name='generate_pdf'),
]
❹.htmlテンプレートを作成する
<!DOCTYPE html>
<html>
<head>
<title>Invoice</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
font-family: Arial;
}
body {
padding: 30px;
}
.top {
font-size: 0;
}
.top-left {
width: 50%;
font-size: 16px;
display: inline-block;
vertical-align: top;
}
.top-left .client {
margin-top: 40px;
margin-left: 40px;
}
.top-right {
width: 50%;
font-size: 16px;
display: inline-block;
vertical-align: top;
}
.top-right h2 {
margin-bottom: 20px;
}
.top-right .split {
font-size: 0;
}
.top-right .split .split-left,
.top-right .split .split-right {
width: 40%;
display: inline-block;
vertical-align: top;
font-size: 16px;
}
.top-right .split .split-right {
width: 60%;
}
.items {
margin-top: 50px;
}
.items .items-row {
font-size: 0;
}
.items .items-row .items-row-cell {
padding: 6px 14px;
display: inline-block;
vertical-align: top;
font-size: 14px;
border-bottom: 1px solid #f1f1f1;
}
.items .items-row .items-row-cell.header {
padding: 14px 14px;
font-weight: bold;
background: #f1f1f1;
}
.items .items-row .title {
width: 20%;
}
.items .items-row .quantity {
width: 20%;
}
.items .items-row .unit-price {
width: 20%;
}
.items .items-row .vat-rate {
width: 20%;
}
.items .items-row .sum {
width: 20%;
}
.summary {
margin-top: 50px;
padding: 30px;
font-size: 0;
background: #f1f1f1;
}
.summary .summary-title {
width: 100%;
margin-bottom: 30px;
font-size: 20px;
}
.summary .summary-left {
width: 65%;
display: inline-block;
vertical-align: top;
font-size: 16px;
}
.summary .summary-left strong {
width: 200px;
display: inline-block;
}
.summary .summary-right {
width: 35%;
display: inline-block;
vertical-align: top;
font-size: 16px;
}
.summary .summary-right .split {
font-size: 0;
}
.summary .summary-right .split .split-right,
.summary .summary-right .split .split-left {
width: 50%;
display: inline-block;
vertical-align: top;
font-size: 16px;
}
.summary .summary-right .split .split-right {
text-align: right;
}
</style>
</head>
<body>
<div class="top">
<div class="top-left">
<div class="invoicely">
<h3>{{ team.name }}</h3>
<p>Street name 123</p>
<p>12345 City</p>
</div>
<div class="client">
<p>{{ invoice.client_name }}</p>
{% if invoice.client_address %}
<p>{{ invoice.client_address }}</p>
{% endif %}
{% if invoice.client_zipcode or invoice.client_place %}
<p>{{ invoice.client_zipcode }} {{ invoice.client_place }}</p>
{% endif %}
</div>
</div>
<div class="top-right">
<h2>Invoice {{ invoice.invoice_number }}</h2>
<div class="split">
<div class="split-left">Email</div>
<div class="split-right">invoice@yourteam.com</div>
</div>
<div class="split">
<div class="split-left">Invoice date</div>
<div class="split-right">{{ invoice.created_at|date:'d.m.Y' }}</div>
</div>
<div class="split">
<div class="split-left">Due date</div>
<div class="split-right">{{ invoice.get_due_date|date:'d.m.Y' }}</div>
</div>
{% if invoice.sender_reference %}
<div class="split">
<div class="split-left">Our reference</div>
<div class="split-right">{{ invoice.sender_reference }}</div>
</div>
{% endif %}
{% if invoice.client_contact_reference %}
<div class="split">
<div class="split-left">Their reference</div>
<div class="split-right">{{ invoice.client_contact_reference }}</div>
</div>
{% endif %}
</div>
</div>
<div class="items">
<div class="items-row">
<div class="items-row-cell header title">Title</div>
<div class="items-row-cell header quantity">Quantity</div>
<div class="items-row-cell header unit-price">Unit price</div>
<div class="items-row-cell header vat-rate">Vat rate</div>
<div class="items-row-cell header sum">Sum</div>
{% for item in invoice.items.all %}
<div class="items-row">
<div class="items-row-cell title">{{ item.title }}</div>
<div class="items-row-cell quantity">{{ item.quantity }}</div>
<div class="items-row-cell unit-price">{{ item.unit_price }}</div>
<div class="items-row-cell vat-rate">{{ item.vat_rate }}%</div>
<div class="items-row-cell sum">{{ item.get_gross_amount }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="summary">
<div class="summary-title">
<h2>Summary</h2>
</div>
<div class="summary-left">
<p><strong>Invoice number: </strong>{{ invoice.invoice_number }}</p>
<p><strong>Due date: </strong>{{ invoice.get_due_date|date:'d.m.Y' }}</p>
<p><strong>Bankaccount: </strong>{{ team.bankaccount }}</p>
</div>
<div class="summary-right">
<div class="split">
<div class="split-left">Net amount</div>
<div class="split-right">{{ invoice.net_amount }}</div>
</div>
<div class="split">
<div class="split-left">Vat amount</div>
<div class="split-right">{{ invoice.vat_amount }}</div>
</div>
<div class="split">
<div class="split-left"><strong>Amount to pay</strong></div>
<div class="split-right"><strong>{{ invoice.gross_amount }}</strong></div>
</div>
</div>
</div>
</body>
</html>
❺.モデル関数を作成する
import decimal
from datetime import timedelta
from django.contrib.auth.models import User
from django.db import models
from client.models import Client
from team.models import Team
class Invoice(models.Model):
INVOICE = 'invoice'
CREDIT_NOTE = 'credit_note'
CHOICES_TYPE = (
(INVOICE, 'Invoice'),
(CREDIT_NOTE, 'Credit note')
)
# 見積番号
invoice_number = models.IntegerField(default=1)
# 取引社名
client_name = models.CharField(max_length=255)
# 取引メール
client_email = models.CharField(max_length=255)
# 取引管理番号
client_org_number = models.CharField(max_length=255, blank=True, null=True)
# 取引住所1
client_address1 = models.CharField(max_length=255, blank=True, null=True)
# 取引所2
client_address2 = models.CharField(max_length=255, blank=True, null=True)
# 取引所郵便番号
client_zipcode = models.CharField(max_length=255, blank=True, null=True)
# 本社
client_place = models.CharField(max_length=255, blank=True, null=True)
# 国
client_country = models.CharField(max_length=255, blank=True, null=True)
# 窓口担当者
client_contact_person = models.CharField(max_length=255, blank=True, null=True)
# 決断責任者
client_contact_reference = models.CharField(max_length=255, blank=True, null=True)
# 事務処理担当
sender_reference = models.CharField(max_length=255, blank=True, null=True)
# 見積状態
invoice_type = models.CharField(max_length=20, choices=CHOICES_TYPE, default=INVOICE)
# 見積有効期間
due_days = models.IntegerField(default=14)
is_credit_for = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True)
is_sent = models.BooleanField(default=False)
# 支払状態
is_paid = models.BooleanField(default=False)
# gross (形容詞)とは、収入や売上げなどの、税金や経費を差し引く前の総額、つまり、総計の、という意味です。
# gross (動詞)は、総収益をあげる、という意味になります。
# net (形容詞)とは、税金や経費を差し引いた後の金額、つまり、純益の、という意味です。
gross_amount = models.DecimalField(max_digits=6, decimal_places=2)
# 付加価値税(Value Added Tax:VAT)=日本の消費税
vat_amount = models.DecimalField(max_digits=6, decimal_places=2)
net_amount = models.DecimalField(max_digits=6, decimal_places=2)
discount_amount = models.DecimalField(max_digits=6, decimal_places=2)
team = models.ForeignKey(Team, related_name='invoices', on_delete=models.CASCADE)
client = models.ForeignKey(Client, related_name='invoices', on_delete=models.CASCADE)
created_by = models.ForeignKey(User, related_name='created_invoices', on_delete=models.CASCADE)
modified_by = models.ForeignKey(User, related_name='modified_invoices', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('-created_at',)
def get_due_date(self):
return self.created_at + timedelta(days=self.due_days)
class Item(models.Model):
invoice = models.ForeignKey(Invoice, related_name='items', on_delete=models.CASCADE)
title = models.CharField(max_length=255)
quantity = models.IntegerField(default=1)
unit_price = models.DecimalField(max_digits=6, decimal_places=2)
net_amount = models.DecimalField(max_digits=6, decimal_places=2)
vat_rate = models.IntegerField(default=0)
discount = models.IntegerField(default=0)
def get_gross_amount(self):
vat_rate = decimal.Decimal(self.vat_rate/100)
return self.net_amount + (self.net_amount * vat_rate)
❻.js-file-downloadをインストールする
npm install js-file-download
❼.PDFを作成するためのvueのボタンと機能を作成する
<button @click="getPdf()" class="button is-dark">Download PDF</button>
<script>
export default {
name: 'Invoice',
data() {
return {
}
},
methods: {
const fileDownload = require('js-file-download')
getPdf() {
const invoiceID = this.$route.params.id
axios
.get(`/api/v1/invoices/${invoiceID}/generate_pdf/`, {
responseType: 'blob',
}).then(res => {
fileDownload(res.data, `invoice_${invoiceID}.pdf`);
}).catch(err => {
console.log(err);
})
}
</script>
この記事が気に入ったらサポートをしてみませんか?