見出し画像

Django➕VueでPDF自動作成機能実装

利用するためにクライアントのPCに事前にwkhtmltopdfを導入

htttp://wkhtmltopdf.org

スクリーンショット 2021-08-15 午後3.26.01

❶.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>

この記事が気に入ったらサポートをしてみませんか?