forked from GithubBackups/healthchecks
PDF invoices.
This commit is contained in:
parent
399bc39432
commit
9e37b22a70
@ -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
|
||||
|
100
hc/payments/invoices.py
Normal file
100
hc/payments/invoices.py
Normal file
@ -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()
|
61
hc/payments/tests/test_pdf_invoice.py
Normal file
61
hc/payments/tests/test_pdf_invoice.py
Normal file
@ -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)
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -39,7 +39,7 @@
|
||||
</td>
|
||||
<td><code>{{ tx.status }}</code></td>
|
||||
<td>
|
||||
<a href="{% url 'hc-invoice' tx.id %}">View Invoice</a>
|
||||
<a href="{% url 'hc-invoice-pdf' tx.id %}">PDF Invoice</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user