Browse Source

Invoices to email.

pull/149/head
Pēteris Caune 7 years ago
parent
commit
39bc12e351
14 changed files with 187 additions and 29 deletions
  1. +12
    -0
      hc/lib/emails.py
  2. +0
    -5
      hc/payments/forms.py
  3. +0
    -2
      hc/payments/invoices.py
  4. +20
    -0
      hc/payments/migrations/0004_subscription_send_invoices.py
  5. +9
    -0
      hc/payments/models.py
  6. +16
    -0
      hc/payments/tests/test_billing.py
  7. +5
    -4
      hc/payments/tests/test_pdf_invoice.py
  8. +1
    -0
      hc/payments/urls.py
  9. +53
    -13
      hc/payments/views.py
  10. +45
    -4
      templates/accounts/billing.html
  11. +1
    -1
      templates/accounts/profile.html
  12. +15
    -0
      templates/emails/invoice-body-html.html
  13. +8
    -0
      templates/emails/invoice-body-text.html
  14. +2
    -0
      templates/emails/invoice-subject.html

+ 12
- 0
hc/lib/emails.py View File

@ -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()

+ 0
- 5
hc/payments/forms.py View File

@ -1,5 +0,0 @@
from django import forms
class BillToForm(forms.Form):
bill_to = forms.CharField(max_length=500, required=False)

+ 0
- 2
hc/payments/invoices.py View File

@ -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
- 0
hc/payments/migrations/0004_subscription_send_invoices.py View 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),
),
]

+ 9
- 0
hc/payments/models.py View File

@ -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
- 0
hc/payments/tests/test_billing.py View 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="[email protected]", password="password")
form = {"save_send_invoices": True}
self.client.post("/accounts/profile/billing/", form)
sub = Subscription.objects.get()
self.assertFalse(sub.send_invoices)

+ 5
- 4
hc/payments/tests/test_pdf_invoice.py View File

@ -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):
self.profile.bill_to = "Alice and Partners"
self.profile.save()
def test_it_shows_company_data(self, mock):
self.sub.address_id = "aa"
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="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get("/invoice/pdf/abc123/") r = self.client.get("/invoice/pdf/abc123/")


+ 1
- 0
hc/payments/urls.py View File

@ -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),
] ]

+ 53
- 13
hc/payments/views.py View File

@ -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())
return response
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
@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()

+ 45
- 4
templates/accounts/billing.html View File

@ -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>


+ 1
- 1
templates/accounts/profile.html View File

@ -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
- 0
templates/emails/invoice-body-html.html View 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
- 0
templates/emails/invoice-body-text.html View File

@ -0,0 +1,8 @@
{% load hc_extras %}
Hello,
Here's your invoice from {% site_name %}.
--
Regards,
{% site_name %}

+ 2
- 0
templates/emails/invoice-subject.html View File

@ -0,0 +1,2 @@
{% load hc_extras %}
Invoice from {% site_name %}

Loading…
Cancel
Save