From 9e37b22a70b9f875c790ea82e780384b85aca989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Thu, 30 Nov 2017 00:23:37 +0200 Subject: [PATCH] PDF invoices. --- .travis.yml | 2 +- hc/payments/invoices.py | 100 ++++++++++++++++++++++++++ hc/payments/tests/test_pdf_invoice.py | 61 ++++++++++++++++ hc/payments/urls.py | 4 ++ hc/payments/views.py | 19 ++++- templates/payments/billing.html | 2 +- 6 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 hc/payments/invoices.py create mode 100644 hc/payments/tests/test_pdf_invoice.py diff --git a/.travis.yml b/.travis.yml index 85883703..a815c0f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "3.5" install: - pip install -r requirements.txt - - pip install braintree coveralls mock mysqlclient + - pip install braintree coveralls mock mysqlclient reportlab env: - DB=sqlite - DB=mysql diff --git a/hc/payments/invoices.py b/hc/payments/invoices.py new file mode 100644 index 00000000..4529c0e5 --- /dev/null +++ b/hc/payments/invoices.py @@ -0,0 +1,100 @@ +# coding: utf-8 + +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import inch +from reportlab.pdfgen import canvas + + +W, H = A4 + + +def f(dt): + return dt.strftime("%b. %-d, %Y") + + +class PdfInvoice(canvas.Canvas): + def __init__(self, fileobj): + super(PdfInvoice, self).__init__(fileobj, pagesize=A4, + pageCompression=0) + self.head_y = H - inch * 0.5 + + def linefeed(self): + self.head_y -= inch / 8 + + def print(self, s, align="left", size=10, bold=False): + self.head_y -= inch / 24 + self.linefeed() + self.setFont("Helvetica-Bold" if bold else "Helvetica", size) + + if align == "left": + self.drawString(inch * 0.5, self.head_y, s) + elif align == "right": + self.drawRightString(W - inch * 0.5, self.head_y, s) + elif align == "center": + self.drawCentredString(W / 2, self.head_y, s) + + self.head_y -= inch / 24 + + def hr(self): + self.setLineWidth(inch / 72 / 8) + self.line(inch * 0.5, self.head_y, W - inch * 0.5, self.head_y) + + def print_row(self, items, align="left", bold=False, size=10): + self.head_y -= inch / 8 + self.linefeed() + + self.setFont("Helvetica-Bold" if bold else "Helvetica", size) + + self.drawString(inch * 0.5, self.head_y, items[0]) + self.drawString(inch * 3.5, self.head_y, items[1]) + self.drawString(inch * 5.5, self.head_y, items[2]) + self.drawRightString(W - inch * 0.5, self.head_y, items[3]) + + self.head_y -= inch / 8 + + def render(self, tx, bill_to): + width, height = A4 + invoice_id = "MS-HC-%s" % tx.id.upper() + self.setTitle(invoice_id) + + self.print("SIA Monkey See Monkey Do", size=16) + self.linefeed() + self.print("Gaujas iela 4-2") + self.print("Valmiera, LV-4201, Latvia") + self.print("VAT: LV44103100701") + self.linefeed() + + created = f(tx.created_at) + self.print("Date Issued: %s" % created, align="right") + self.print("Invoice Id: %s" % invoice_id, align="right") + self.linefeed() + + self.hr() + self.print_row(["Description", "Start", "End", tx.currency_iso_code], + bold=True) + self.hr() + start = f(tx.subscription_details.billing_period_start_date) + end = f(tx.subscription_details.billing_period_end_date) + if tx.currency_iso_code == "USD": + amount = "$%s" % tx.amount + elif tx.currency_iso_code == "EUR": + amount = "€%s" % tx.amount + else: + amount = "%s %s" % (tx.currency_iso_code, tx.amount) + + self.print_row(["healthchecks.io paid plan", start, end, amount]) + + self.hr() + self.print_row(["", "", "", "Total: %s" % amount], bold=True) + self.linefeed() + + self.print("Bill to:", bold=True) + for s in bill_to.split("\n"): + self.print(s.strip()) + + self.linefeed() + self.print("If you have a credit card on file it will be " + "automatically charged within 24 hours.", align="center") + + self.showPage() + self.save() diff --git a/hc/payments/tests/test_pdf_invoice.py b/hc/payments/tests/test_pdf_invoice.py new file mode 100644 index 00000000..e8817a85 --- /dev/null +++ b/hc/payments/tests/test_pdf_invoice.py @@ -0,0 +1,61 @@ +from mock import Mock, patch + +from django.utils.timezone import now +from hc.payments.models import Subscription +from hc.test import BaseTestCase +import six + + +class PdfInvoiceTestCase(BaseTestCase): + + def setUp(self): + super(PdfInvoiceTestCase, self).setUp() + self.sub = Subscription(user=self.alice) + self.sub.subscription_id = "test-id" + self.sub.customer_id = "test-customer-id" + self.sub.save() + + self.tx = Mock() + self.tx.id = "abc123" + self.tx.customer_details.id = "test-customer-id" + self.tx.created_at = now() + self.tx.currency_iso_code = "USD" + self.tx.amount = 5 + self.tx.subscription_details.billing_period_start_date = now() + self.tx.subscription_details.billing_period_end_date = now() + + @patch("hc.payments.views.braintree") + def test_it_works(self, mock_braintree): + + mock_braintree.Transaction.find.return_value = self.tx + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/invoice/pdf/abc123/") + with open("/home/cepe/rez.pdf", "wb") as f: + f.write(r.content) + self.assertTrue(six.b("ABC123") in r.content) + self.assertTrue(six.b("alice@example.org") in r.content) + + @patch("hc.payments.views.braintree") + def test_it_checks_customer_id(self, mock_braintree): + + tx = Mock() + tx.id = "abc123" + tx.customer_details.id = "test-another-customer-id" + tx.created_at = None + mock_braintree.Transaction.find.return_value = tx + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/invoice/pdf/abc123/") + self.assertEqual(r.status_code, 403) + + @patch("hc.payments.views.braintree") + def test_it_shows_company_data(self, mock_braintree): + self.profile.bill_to = "Alice and Partners" + self.profile.save() + + mock_braintree.Transaction.find.return_value = self.tx + + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/invoice/pdf/abc123/") + self.assertTrue(six.b("Alice and Partners") in r.content) diff --git a/hc/payments/urls.py b/hc/payments/urls.py index e55b42a1..e86f2d50 100644 --- a/hc/payments/urls.py +++ b/hc/payments/urls.py @@ -15,6 +15,10 @@ urlpatterns = [ views.invoice, name="hc-invoice"), + url(r'^invoice/pdf/([\w-]+)/$', + views.pdf_invoice, + name="hc-invoice-pdf"), + url(r'^pricing/create_plan/$', views.create_plan, name="hc-create-plan"), diff --git a/hc/payments/views.py b/hc/payments/views.py index d7030d16..84909472 100644 --- a/hc/payments/views.py +++ b/hc/payments/views.py @@ -2,11 +2,12 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import (HttpResponseBadRequest, HttpResponseForbidden, - JsonResponse) + JsonResponse, HttpResponse) from django.shortcuts import redirect, render from django.views.decorators.http import require_POST from hc.payments.forms import BillToForm +from hc.payments.invoices import PdfInvoice from hc.payments.models import Subscription if settings.USE_PAYMENTS: @@ -216,3 +217,19 @@ def invoice(request, transaction_id): ctx = {"tx": transaction} return render(request, "payments/invoice.html", ctx) + + +@login_required +def pdf_invoice(request, transaction_id): + sub = Subscription.objects.get(user=request.user) + transaction = braintree.Transaction.find(transaction_id) + if transaction.customer_details.id != sub.customer_id: + return HttpResponseForbidden() + + response = HttpResponse(content_type='application/pdf') + filename = "MS-HC-%s.pdf" % transaction.id.upper() + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + + bill_to = request.user.profile.bill_to or request.user.email + PdfInvoice(response).render(transaction, bill_to) + return response diff --git a/templates/payments/billing.html b/templates/payments/billing.html index 279ff89b..79411cab 100644 --- a/templates/payments/billing.html +++ b/templates/payments/billing.html @@ -39,7 +39,7 @@ {{ tx.status }} - View Invoice + PDF Invoice {% empty %}