mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 06:05:34 +00:00
Invoices to email.
This commit is contained in:
parent
01c3a13922
commit
39bc12e351
14 changed files with 188 additions and 30 deletions
|
@ -58,3 +58,15 @@ def verify_email(to, ctx):
|
||||||
|
|
||||||
def report(to, ctx):
|
def report(to, ctx):
|
||||||
send("report", to, ctx)
|
send("report", to, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def invoice(to, ctx, filename, pdf_data):
|
||||||
|
ctx["SITE_ROOT"] = settings.SITE_ROOT
|
||||||
|
subject = render('emails/invoice-subject.html', ctx).strip()
|
||||||
|
text = render('emails/invoice-body-text.html', ctx)
|
||||||
|
html = render('emails/invoice-body-html.html', ctx)
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(subject, text, to=(to, ))
|
||||||
|
msg.attach_alternative(html, "text/html")
|
||||||
|
msg.attach(filename, pdf_data, "application/pdf")
|
||||||
|
msg.send()
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django import forms
|
|
||||||
|
|
||||||
|
|
||||||
class BillToForm(forms.Form):
|
|
||||||
bill_to = forms.CharField(max_length=500, required=False)
|
|
|
@ -93,8 +93,6 @@ class PdfInvoice(Canvas):
|
||||||
self.text(s.strip())
|
self.text(s.strip())
|
||||||
|
|
||||||
self.linefeed()
|
self.linefeed()
|
||||||
self.text("If you have a credit card on file it will be "
|
|
||||||
"automatically charged within 24 hours.", align="center")
|
|
||||||
|
|
||||||
self.showPage()
|
self.showPage()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
20
hc/payments/migrations/0004_subscription_send_invoices.py
Normal file
20
hc/payments/migrations/0004_subscription_send_invoices.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2018-01-09 12:52
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('payments', '0003_subscription_address_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscription',
|
||||||
|
name='send_invoices',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
if settings.USE_PAYMENTS:
|
if settings.USE_PAYMENTS:
|
||||||
import braintree
|
import braintree
|
||||||
|
@ -29,6 +30,7 @@ class Subscription(models.Model):
|
||||||
subscription_id = models.CharField(max_length=10, blank=True)
|
subscription_id = models.CharField(max_length=10, blank=True)
|
||||||
plan_id = models.CharField(max_length=10, blank=True)
|
plan_id = models.CharField(max_length=10, blank=True)
|
||||||
address_id = models.CharField(max_length=2, blank=True)
|
address_id = models.CharField(max_length=2, blank=True)
|
||||||
|
send_invoices = models.BooleanField(default=True)
|
||||||
|
|
||||||
objects = SubscriptionManager()
|
objects = SubscriptionManager()
|
||||||
|
|
||||||
|
@ -176,6 +178,13 @@ class Subscription(models.Model):
|
||||||
|
|
||||||
return self._address
|
return self._address
|
||||||
|
|
||||||
|
def flattened_address(self):
|
||||||
|
if self.address_id:
|
||||||
|
ctx = {"a": self.address}
|
||||||
|
return render_to_string("payments/address_plain.html", ctx)
|
||||||
|
else:
|
||||||
|
return self.user.email
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transactions(self):
|
def transactions(self):
|
||||||
if not hasattr(self, "_tx"):
|
if not hasattr(self, "_tx"):
|
||||||
|
|
16
hc/payments/tests/test_billing.py
Normal file
16
hc/payments/tests/test_billing.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from hc.payments.models import Subscription
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class SetPlanTestCase(BaseTestCase):
|
||||||
|
|
||||||
|
@patch("hc.payments.models.braintree")
|
||||||
|
def test_it_saves_send_invoices_flag(self, mock):
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
|
||||||
|
form = {"save_send_invoices": True}
|
||||||
|
self.client.post("/accounts/profile/billing/", form)
|
||||||
|
sub = Subscription.objects.get()
|
||||||
|
self.assertFalse(sub.send_invoices)
|
|
@ -55,11 +55,12 @@ class PdfInvoiceTestCase(BaseTestCase):
|
||||||
|
|
||||||
@skipIf(reportlab is None, "reportlab not installed")
|
@skipIf(reportlab is None, "reportlab not installed")
|
||||||
@patch("hc.payments.models.braintree")
|
@patch("hc.payments.models.braintree")
|
||||||
def test_it_shows_company_data(self, mock_braintree):
|
def test_it_shows_company_data(self, mock):
|
||||||
self.profile.bill_to = "Alice and Partners"
|
self.sub.address_id = "aa"
|
||||||
self.profile.save()
|
self.sub.save()
|
||||||
|
|
||||||
mock_braintree.Transaction.find.return_value = self.tx
|
mock.Transaction.find.return_value = self.tx
|
||||||
|
mock.Address.find.return_value = {"company": "Alice and Partners"}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get("/invoice/pdf/abc123/")
|
r = self.client.get("/invoice/pdf/abc123/")
|
||||||
|
|
|
@ -35,4 +35,5 @@ urlpatterns = [
|
||||||
views.get_client_token,
|
views.get_client_token,
|
||||||
name="hc-get-client-token"),
|
name="hc-get-client-token"),
|
||||||
|
|
||||||
|
url(r'^pricing/charge/$', views.charge_webhook),
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,9 +4,11 @@ from django.http import (HttpResponseBadRequest, HttpResponseForbidden,
|
||||||
JsonResponse, HttpResponse)
|
JsonResponse, HttpResponse)
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
import six
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
|
from hc.lib import emails
|
||||||
from hc.payments.invoices import PdfInvoice
|
from hc.payments.invoices import PdfInvoice
|
||||||
from hc.payments.models import Subscription
|
from hc.payments.models import Subscription
|
||||||
|
|
||||||
|
@ -37,15 +39,37 @@ def billing(request):
|
||||||
# subscription object is not created just by viewing a page.
|
# subscription object is not created just by viewing a page.
|
||||||
sub = Subscription.objects.filter(user_id=request.user.id).first()
|
sub = Subscription.objects.filter(user_id=request.user.id).first()
|
||||||
|
|
||||||
|
send_invoices_status = "default"
|
||||||
|
if request.method == "POST":
|
||||||
|
if "save_send_invoices" in request.POST:
|
||||||
|
sub = Subscription.objects.for_user(request.user)
|
||||||
|
sub.send_invoices = "send_invoices" in request.POST
|
||||||
|
sub.save()
|
||||||
|
send_invoices_status = "success"
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"page": "billing",
|
"page": "billing",
|
||||||
"profile": request.profile,
|
"profile": request.profile,
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
"num_checks": Check.objects.filter(user=request.user).count(),
|
"num_checks": Check.objects.filter(user=request.user).count(),
|
||||||
"team_size": request.profile.member_set.count() + 1,
|
"team_size": request.profile.member_set.count() + 1,
|
||||||
"team_max": request.profile.team_limit + 1
|
"team_max": request.profile.team_limit + 1,
|
||||||
|
"send_invoices_status": send_invoices_status,
|
||||||
|
"set_plan_status": "default",
|
||||||
|
"address_status": "default",
|
||||||
|
"payment_method_status": "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "set_plan_status" in request.session:
|
||||||
|
ctx["set_plan_status"] = request.session.pop("set_plan_status")
|
||||||
|
|
||||||
|
if "address_status" in request.session:
|
||||||
|
ctx["address_status"] = request.session.pop("address_status")
|
||||||
|
|
||||||
|
if "payment_method_status" in request.session:
|
||||||
|
ctx["payment_method_status"] = \
|
||||||
|
request.session.pop("payment_method_status")
|
||||||
|
|
||||||
return render(request, "accounts/billing.html", ctx)
|
return render(request, "accounts/billing.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +129,7 @@ def set_plan(request):
|
||||||
profile.sms_sent = 0
|
profile.sms_sent = 0
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
request.session["first_charge"] = True
|
request.session["set_plan_status"] = "success"
|
||||||
return redirect("hc-billing")
|
return redirect("hc-billing")
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,6 +141,7 @@ def address(request):
|
||||||
if error:
|
if error:
|
||||||
return log_and_bail(request, error)
|
return log_and_bail(request, error)
|
||||||
|
|
||||||
|
request.session["address_status"] = "success"
|
||||||
return redirect("hc-billing")
|
return redirect("hc-billing")
|
||||||
|
|
||||||
ctx = {"a": sub.address}
|
ctx = {"a": sub.address}
|
||||||
|
@ -136,6 +161,7 @@ def payment_method(request):
|
||||||
if error:
|
if error:
|
||||||
return log_and_bail(request, error)
|
return log_and_bail(request, error)
|
||||||
|
|
||||||
|
request.session["payment_method_status"] = "success"
|
||||||
return redirect("hc-billing")
|
return redirect("hc-billing")
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
|
@ -167,15 +193,29 @@ def pdf_invoice(request, transaction_id):
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
filename = "MS-HC-%s.pdf" % transaction.id.upper()
|
filename = "MS-HC-%s.pdf" % transaction.id.upper()
|
||||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||||
|
PdfInvoice(response).render(transaction, sub.flattened_address())
|
||||||
bill_to = []
|
|
||||||
if sub.address_id:
|
|
||||||
ctx = {"a": sub.address}
|
|
||||||
bill_to = render_to_string("payments/address_plain.html", ctx)
|
|
||||||
elif request.user.profile.bill_to:
|
|
||||||
bill_to = request.user.profile.bill_to
|
|
||||||
else:
|
|
||||||
bill_to = request.user.email
|
|
||||||
|
|
||||||
PdfInvoice(response).render(transaction, bill_to)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_POST
|
||||||
|
def charge_webhook(request):
|
||||||
|
sig = str(request.POST["bt_signature"])
|
||||||
|
payload = str(request.POST["bt_payload"])
|
||||||
|
|
||||||
|
import braintree
|
||||||
|
doc = braintree.WebhookNotification.parse(sig, payload)
|
||||||
|
if doc.kind != "subscription_charged_successfully":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
sub = Subscription.objects.get(subscription_id=doc.subscription.id)
|
||||||
|
if sub.send_invoices:
|
||||||
|
transaction = doc.subscription.transactions[0]
|
||||||
|
filename = "MS-HC-%s.pdf" % transaction.id.upper()
|
||||||
|
|
||||||
|
sink = six.BytesIO()
|
||||||
|
PdfInvoice(sink).render(transaction, sub.flattened_address())
|
||||||
|
ctx = {"tx": transaction}
|
||||||
|
emails.invoice(sub.user.email, ctx, filename, sink.getvalue())
|
||||||
|
|
||||||
|
return HttpResponse()
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-{{ set_plan_status }}">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>Billing Plan</h2>
|
<h2>Billing Plan</h2>
|
||||||
|
|
||||||
|
@ -96,9 +96,14 @@
|
||||||
Change Billing Plan
|
Change Billing Plan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% if set_plan_status == "success" %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
Your billing plan has been updated!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-{{ payment_method_status }}">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>Payment Method</h2>
|
<h2>Payment Method</h2>
|
||||||
{% if sub.payment_method_token %}
|
{% if sub.payment_method_token %}
|
||||||
|
@ -113,10 +118,15 @@
|
||||||
class="btn btn-default pull-right">
|
class="btn btn-default pull-right">
|
||||||
Change Payment Method</button>
|
Change Payment Method</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% if payment_method_status == "success" %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
Your payment method has been updated!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-{{ address_status }}">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>Billing Address</h2>
|
<h2>Billing Address</h2>
|
||||||
|
|
||||||
|
@ -137,7 +147,7 @@
|
||||||
Change Billing Address
|
Change Billing Address
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if status == "info" %}
|
{% if address_status == "success" %}
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
Your billing address has been updated!
|
Your billing address has been updated!
|
||||||
</div>
|
</div>
|
||||||
|
@ -146,6 +156,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-{{ send_invoices_status }}">
|
||||||
|
<div class="panel-body settings-block">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<h2>Invoices to Email</h2>
|
||||||
|
|
||||||
|
<label class="checkbox-container">
|
||||||
|
<input
|
||||||
|
name="send_invoices"
|
||||||
|
type="checkbox"
|
||||||
|
{% if sub.send_invoices %}checked{% endif %}>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Send the invoice to {{ request.user.email }}
|
||||||
|
each time my payment method is successfully charged.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="save_send_invoices"
|
||||||
|
class="btn btn-default pull-right">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if send_invoices_status == "success" %}
|
||||||
|
<div class="panel-footer">
|
||||||
|
Your preferences have been updated!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>Billing History</h2>
|
<h2>Billing History</h2>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel panel-{{ api_status }}">
|
<div class="panel panel-{{ send_invoices_status }}">
|
||||||
<div class="panel-body settings-block">
|
<div class="panel-body settings-block">
|
||||||
<h2>API Access</h2>
|
<h2>API Access</h2>
|
||||||
{% if profile.api_key %}
|
{% if profile.api_key %}
|
||||||
|
|
15
templates/emails/invoice-body-html.html
Normal file
15
templates/emails/invoice-body-html.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "emails/base.html" %}
|
||||||
|
{% load hc_extras %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
Hello,<br />
|
||||||
|
Here's your invoice from {% site_name %} for
|
||||||
|
{{ tx.subscription_details.billing_period_start_date }} - {{ tx.subscription_details.billing_period_end_date }}
|
||||||
|
.</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_more %}
|
||||||
|
Thanks,<br />
|
||||||
|
The {% escaped_site_name %} Team
|
||||||
|
{% endblock %}
|
8
templates/emails/invoice-body-text.html
Normal file
8
templates/emails/invoice-body-text.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% load hc_extras %}
|
||||||
|
Hello,
|
||||||
|
|
||||||
|
Here's your invoice from {% site_name %}.
|
||||||
|
|
||||||
|
--
|
||||||
|
Regards,
|
||||||
|
{% site_name %}
|
2
templates/emails/invoice-subject.html
Normal file
2
templates/emails/invoice-subject.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% load hc_extras %}
|
||||||
|
Invoice from {% site_name %}
|
Loading…
Add table
Reference in a new issue