add proforma invoice support
This commit is contained in:
parent
fef09baa3d
commit
effd900b62
|
@ -14,8 +14,30 @@ def do_work():
|
||||||
The invoice worker creates Invoice objects for shop orders and
|
The invoice worker creates Invoice objects for shop orders and
|
||||||
for custom orders. It also generates PDF files for Invoice objects
|
for custom orders. It also generates PDF files for Invoice objects
|
||||||
that have no PDF. It also emails invoices for shop orders.
|
that have no PDF. It also emails invoices for shop orders.
|
||||||
|
It also generates proforma invoices for all closed orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# check if we need to generate any proforma invoices for shop orders
|
||||||
|
for order in Order.objects.filter(pdf="", open__isnull=True):
|
||||||
|
# generate proforma invoice for this Order
|
||||||
|
pdffile = generate_pdf_letter(
|
||||||
|
filename=order.filename,
|
||||||
|
template="pdf/proforma_invoice.html",
|
||||||
|
formatdict={
|
||||||
|
"hostname": settings.ALLOWED_HOSTS[0],
|
||||||
|
"order": order,
|
||||||
|
"bank": settings.BANKACCOUNT_BANK,
|
||||||
|
"bank_iban": settings.BANKACCOUNT_IBAN,
|
||||||
|
"bank_bic": settings.BANKACCOUNT_SWIFTBIC,
|
||||||
|
"bank_dk_reg": settings.BANKACCOUNT_REG,
|
||||||
|
"bank_dk_accno": settings.BANKACCOUNT_ACCOUNT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# update order object with the file
|
||||||
|
order.pdf.save(order.filename, File(pdffile))
|
||||||
|
order.save()
|
||||||
|
logger.info("Generated proforma invoice PDF for order %s" % order)
|
||||||
|
|
||||||
# check if we need to generate any invoices for shop orders
|
# check if we need to generate any invoices for shop orders
|
||||||
for order in Order.objects.filter(paid=True, invoice__isnull=True):
|
for order in Order.objects.filter(paid=True, invoice__isnull=True):
|
||||||
# generate invoice for this Order
|
# generate invoice for this Order
|
||||||
|
|
18
src/shop/migrations/0058_order_pdf.py
Normal file
18
src/shop/migrations/0058_order_pdf.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.1.7 on 2019-07-08 21:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('shop', '0057_order_notes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='pdf',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='proforma_invoices/'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -128,6 +128,8 @@ class Order(CreatedUpdatedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pdf = models.FileField(null=True, blank=True, upload_to="proforma_invoices/")
|
||||||
|
|
||||||
objects = OrderQuerySet.as_manager()
|
objects = OrderQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -334,6 +336,10 @@ class Order(CreatedUpdatedModel):
|
||||||
# nope
|
# nope
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return "bornhack_proforma_invoice_order_%s.pdf" % self.pk
|
||||||
|
|
||||||
|
|
||||||
class ProductCategory(CreatedUpdatedModel, UUIDModel):
|
class ProductCategory(CreatedUpdatedModel, UUIDModel):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -84,15 +84,28 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if order.open %}
|
{% if order.open %}
|
||||||
{% bootstrap_button "Update order" button_type="submit" button_class="btn-primary" name="update_order" %}
|
{% bootstrap_button "Update order" button_type="submit" button_class="btn-primary btn-lg" name="update_order" icon="edit" %}
|
||||||
{% endif %}
|
|
||||||
{% if not order.paid %}
|
|
||||||
{% bootstrap_button "Cancel order" button_type="submit" button_class="btn-danger" name="cancel_order" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if not order.paid %}
|
|
||||||
{% bootstrap_button "Review and pay" button_type="submit" button_class="btn btn-success btn-lg pull-right" name="review_and_pay" %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not order.paid %}
|
||||||
|
{% bootstrap_button "Cancel order" button_type="submit" button_class="btn-danger btn-lg" name="cancel_order" icon="remove" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not order.paid %}
|
||||||
|
{% bootstrap_button "Review and pay" button_type="submit" button_class="btn btn-success btn-lg pull-right" name="review_and_pay" icon="check" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if order.paid %}
|
||||||
|
{% if order.invoice.pdf %}
|
||||||
|
{% url 'shop:download_invoice' pk=order.pk as invoice_download_url %}
|
||||||
|
{% bootstrap_button "Invoice PDF" icon="save-file" href=invoice_download_url button_class="btn-primary btn-lg pull-right" %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if order.pdf %}
|
||||||
|
{% url 'shop:download_invoice' pk=order.pk as invoice_download_url %}
|
||||||
|
{% bootstrap_button "Proforma Invoice PDF" icon="save-file" href=invoice_download_url button_class="btn-primary btn-lg pull-right" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if order.paid %}
|
{% if order.paid %}
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
|
|
|
@ -33,11 +33,20 @@
|
||||||
<td class="text-center">{{ order.paid|truefalseicon }}</td>
|
<td class="text-center">{{ order.paid|truefalseicon }}</td>
|
||||||
<td class="text-center">{{ order.handed_out_status }}</td>
|
<td class="text-center">{{ order.handed_out_status }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if order.paid %}
|
||||||
{% if order.invoice.pdf %}
|
{% if order.invoice.pdf %}
|
||||||
{% url 'shop:download_invoice' pk=order.pk as invoice_download_url %}
|
{% url 'shop:download_invoice' pk=order.pk as invoice_download_url %}
|
||||||
{% bootstrap_button "PDF" icon="save-file" href=invoice_download_url button_class="btn-primary btn-xs" %}
|
{% bootstrap_button "Invoice" icon="save-file" href=invoice_download_url button_class="btn-primary btn-xs" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
N/A
|
Not generated yet!
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if order.pdf %}
|
||||||
|
{% url 'shop:download_invoice' pk=order.pk as invoice_download_url %}
|
||||||
|
{% bootstrap_button "Proforma Invoice" icon="save-file" href=invoice_download_url button_class="btn-primary btn-xs" %}
|
||||||
|
{% else %}
|
||||||
|
Not generated yet!
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
95
src/shop/templates/pdf/proforma_invoice.html
Normal file
95
src/shop/templates/pdf/proforma_invoice.html
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load shop_tags %}
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
|
||||||
|
<table style="width:100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 75%;"> </td>
|
||||||
|
<td>
|
||||||
|
<h3>
|
||||||
|
Order Date: {{ order.created|date:"b jS, Y" }}<br>
|
||||||
|
Order #{{ order.pk }}<br>
|
||||||
|
Proforma Invoice
|
||||||
|
</h3>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% if order.invoice_address %}
|
||||||
|
<h2>CUSTOMER</h2>
|
||||||
|
<p class="lead">{{ order.invoice_address|linebreaks }}</p>
|
||||||
|
{% else %}
|
||||||
|
<h3>Customer: {{ order.user.email }}</h3>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
<h2>PROFORMA INVOICE</h2>
|
||||||
|
<table style="width:90%; margin:1em;">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<b>Name
|
||||||
|
<td>
|
||||||
|
<b>Quantity
|
||||||
|
<td align="right">
|
||||||
|
<b>Price
|
||||||
|
<td align="right">
|
||||||
|
<b>Total
|
||||||
|
|
||||||
|
<tr><td style="height: 1px; line-height: 1px; border-top: 1pt solid black;" colspan="4"> </td></tr>
|
||||||
|
|
||||||
|
{% for order_product in order.orderproductrelation_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ order_product.product.name }}
|
||||||
|
<td>
|
||||||
|
{{ order_product.quantity }}
|
||||||
|
<td align="right">
|
||||||
|
{{ order_product.product.price|currency }}
|
||||||
|
<td align="right">
|
||||||
|
{{ order_product.total|currency }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<tr><td style="height: 1px; line-height: 1px; border-top: 1pt solid black;" colspan="4"> </td></tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<td>
|
||||||
|
<strong>Included Danish VAT (25%)</strong>
|
||||||
|
<td align="right">
|
||||||
|
{{ order.vat|currency }}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<td>
|
||||||
|
<strong>Invoice Total</strong>
|
||||||
|
<td align="right">
|
||||||
|
{{ order.total|currency }}
|
||||||
|
|
||||||
|
<tr><td colspan="2"></td><td style="height: 1px; line-height: 1px; border-top: 1pt solid black; border-bottom: 1pt solid black;" colspan="4"> </td></tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>This is a proforma invoice. The order has not been paid. The payment options are:</h3>
|
||||||
|
<h4>Bank Transfer</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Bank: {{ bank }}
|
||||||
|
<li>DK Reg. {{ bank_dk_reg }}, DK account no. {{ bank_dk_accno }}</li>
|
||||||
|
<li>BIC: {{ bank_bic }}, IBAN: {{ bank_iban }}</li>
|
||||||
|
<li>Please remember to add Order number in the transfer notes, and pay the transfer fees in your end.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Credit Card</h4>
|
||||||
|
<ul>
|
||||||
|
<li>https://{{ hostname }}{% url 'shop:epay_form' pk=order.pk %} (requires login)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Blockchain (multiple currencies)</h4>
|
||||||
|
<ul>
|
||||||
|
<li>https://{{ hostname }}{% url 'shop:coinify_pay' pk=order.pk %} (requires login)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Cash</h4>
|
||||||
|
<ul>
|
||||||
|
<li>https://{{ hostname }}{% url 'shop:cash' pk=order.pk %} (requires geographical proximity to an organiser)</li>
|
||||||
|
</ul>
|
||||||
|
|
|
@ -148,21 +148,6 @@ class EnsureOrderIsNotCancelledMixin(SingleObjectMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EnsureOrderHasInvoicePDFMixin(SingleObjectMixin):
|
|
||||||
model = Order
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if not self.get_object().invoice.pdf:
|
|
||||||
messages.error(request, "This order has no invoice yet!")
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("shop:order_detail", kwargs={"pk": self.get_object().pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
return super(EnsureOrderHasInvoicePDFMixin, self).dispatch(
|
|
||||||
request, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Shop views
|
# Shop views
|
||||||
class ShopIndexView(ListView):
|
class ShopIndexView(ListView):
|
||||||
model = Product
|
model = Product
|
||||||
|
@ -402,19 +387,32 @@ class OrderReviewAndPayView(
|
||||||
class DownloadInvoiceView(
|
class DownloadInvoiceView(
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
EnsureUserOwnsOrderMixin,
|
EnsureUserOwnsOrderMixin,
|
||||||
EnsurePaidOrderMixin,
|
|
||||||
EnsureOrderHasInvoicePDFMixin,
|
|
||||||
SingleObjectMixin,
|
SingleObjectMixin,
|
||||||
View,
|
View,
|
||||||
):
|
):
|
||||||
model = Order
|
model = Order
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
The file we return is determined by the orders paid status.
|
||||||
|
If the order is unpaid we return a proforma invoice PDF
|
||||||
|
If the order is paid we return a normal Invoice PDF
|
||||||
|
"""
|
||||||
|
if self.get_object().paid:
|
||||||
|
pdfobj = self.get_object().invoice
|
||||||
|
else:
|
||||||
|
pdfobj = self.get_object()
|
||||||
|
|
||||||
|
if not pdfobj.pdf:
|
||||||
|
messages.error(request, "No PDF has been generated yet!")
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse_lazy("shop:order_detail", kwargs={"pk": self.get_object().pk})
|
||||||
|
)
|
||||||
response = HttpResponse(content_type="application/pdf")
|
response = HttpResponse(content_type="application/pdf")
|
||||||
response["Content-Disposition"] = (
|
response["Content-Disposition"] = (
|
||||||
'attachment; filename="%s"' % self.get_object().invoice.filename
|
'attachment; filename="%s"' % pdfobj.filename
|
||||||
)
|
)
|
||||||
response.write(self.get_object().invoice.pdf.read())
|
response.write(pdfobj.pdf.read())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
def generate_pdf_letter(filename, template, formatdict):
|
def generate_pdf_letter(filename, template, formatdict):
|
||||||
|
logger.debug("Generating PDF with filename %s and template %s" % (filename, template))
|
||||||
|
|
||||||
# conjure up a fake request for PDFTemplateResponse
|
# conjure up a fake request for PDFTemplateResponse
|
||||||
request = RequestFactory().get("/")
|
request = RequestFactory().get("/")
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
|
@ -63,3 +65,4 @@ def generate_pdf_letter(filename, template, formatdict):
|
||||||
returnfile = io.BytesIO()
|
returnfile = io.BytesIO()
|
||||||
finalpdf.write(returnfile)
|
finalpdf.write(returnfile)
|
||||||
return returnfile
|
return returnfile
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue