Refactor payment app
Generate invoice from HTML Remove now unused dependency on PyInvoice
This commit is contained in:
parent
0a4b55e2fe
commit
d2792b8aa3
13 changed files with 484 additions and 242 deletions
|
@ -7,7 +7,7 @@ class Error404View(TemplateView):
|
|||
template_name = "frontend/404.html"
|
||||
|
||||
class DemoTemplateView(TemplateView):
|
||||
template_name = "mail/verify.html"
|
||||
template_name = "payment/invoice.html"
|
||||
|
||||
class ImpressumView(TemplateView):
|
||||
template_name = "frontend/impressum.html"
|
||||
|
|
|
@ -117,6 +117,21 @@ class PersonMixin(models.Model):
|
|||
first_name = models.CharField("Vorname", max_length=64)
|
||||
last_name = models.CharField("Nachname", max_length=64)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
name = self.full_name_only
|
||||
|
||||
if self.company:
|
||||
name += f" ({self.company})"
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def full_name_only(self):
|
||||
name = " ".join([self.first_name, self.last_name])
|
||||
|
||||
return name
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
@ -125,15 +140,6 @@ class Profile(PersonMixin, AddressMixin, PhoneMixin):
|
|||
verified = models.BooleanField(default=False)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
name = " ".join([self.first_name, self.last_name])
|
||||
|
||||
if self.company:
|
||||
name += f" ({self.company})"
|
||||
|
||||
return name
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
|
|
@ -1,230 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from pyinvoice.models import InvoiceInfo, ServiceProviderInfo, ClientInfo, Item, Transaction, PDFInfo
|
||||
from pyinvoice.templates import SimpleInvoice
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import uuid
|
||||
|
||||
import urllib.request
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
from .functions import invoice_upload_path
|
||||
from .signals import initiate_payment
|
||||
|
||||
from auction.models import Inquiry
|
||||
from localauth.models import PersonMixin, AddressMixin
|
||||
|
||||
class BillingAddress(PersonMixin, AddressMixin):
|
||||
user = models.ForeignKey(get_user_model(), models.CASCADE)
|
||||
|
||||
@classmethod
|
||||
def from_profile(cls, profile):
|
||||
return cls.objects.create(
|
||||
company = profile.company,
|
||||
vat_id = profile.vat_id,
|
||||
first_name = profile.first_name,
|
||||
last_name = profile.last_name,
|
||||
street = profile.street,
|
||||
city = profile.city,
|
||||
zip = profile.zip,
|
||||
state = profile.state,
|
||||
country = profile.country,
|
||||
user = profile.user
|
||||
)
|
||||
|
||||
class Invoice(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
user = models.ForeignKey(get_user_model(), models.PROTECT)
|
||||
billing_address = models.ForeignKey(BillingAddress, models.PROTECT)
|
||||
currency = models.CharField(max_length=3)
|
||||
tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
invoice = models.FileField(null=True, blank=True, upload_to=invoice_upload_path)
|
||||
inquiry = models.OneToOneField(Inquiry, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@property
|
||||
def price_net(self):
|
||||
price = 0
|
||||
|
||||
for item in self.invoiceitem_set.all():
|
||||
price += item.net_total
|
||||
|
||||
return price
|
||||
|
||||
@property
|
||||
def tax(self):
|
||||
return round(self.price_net * self.tax_rate / 100, 2)
|
||||
|
||||
@property
|
||||
def price_gross(self):
|
||||
return self.price_net + self.tax
|
||||
|
||||
@property
|
||||
def payment_instructions(self):
|
||||
return "Alle Preise in %s." % self.currency
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
paid_amount = 0
|
||||
|
||||
for payment in self.invoicepayment_set.all():
|
||||
paid_amount += payment.amount
|
||||
|
||||
return paid_amount - self.price_gross
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
return self.balance >= 0
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
if self.is_paid:
|
||||
try:
|
||||
self.inquiry.process_payment(*args, **kwargs)
|
||||
except Inquiry.DoesNotExist:
|
||||
pass
|
||||
|
||||
self.generate_invoice()
|
||||
|
||||
def generate_invoice(self):
|
||||
output = BytesIO()
|
||||
|
||||
pdfinfo = PDFInfo(title='Rechnung', author='Kumi Media Ltd.', subject='Rechnung')
|
||||
doc = SimpleInvoice(output, pdfinfo)
|
||||
|
||||
doc.is_paid = self.is_paid
|
||||
|
||||
doc.invoice_info = InvoiceInfo("%s%i" % (getValue("billing.prefix", ""), self.id), timezone.now().date(), timezone.now().date())
|
||||
|
||||
provider_data = { key: value for key, value in {
|
||||
"name": getValue("billing.provider.name", False),
|
||||
"street": getValue("billing.provider.street", False),
|
||||
"city": getValue("billing.provider.city", False),
|
||||
"state": getValue("billing.provider.state", False),
|
||||
"country": getValue("billing.provider.country", False),
|
||||
"post_code": getValue("billing.provider.zip", False),
|
||||
"vat_tax_number": getValue("billing.provider.vat_id", False)
|
||||
}.items() if value}
|
||||
|
||||
doc.service_provider_info = ServiceProviderInfo(**provider_data)
|
||||
|
||||
logo_url = getValue("billing.logo_url", False)
|
||||
if logo_url:
|
||||
try:
|
||||
logo = BytesIO(urllib.request.urlopen(logo_url).read())
|
||||
doc.logo(logo)
|
||||
except:
|
||||
pass
|
||||
|
||||
profile = self.user.clientprofile if self.inquiry else self.user.partnerprofile
|
||||
|
||||
client_data = { key: value for key, value in {
|
||||
"name": profile.full_name,
|
||||
"street": profile.street,
|
||||
"city": profile.city,
|
||||
"state": profile.state,
|
||||
"country": profile.country,
|
||||
"post_code": profile.zip,
|
||||
"email": self.user.email,
|
||||
}.items() if value}
|
||||
|
||||
doc.client_info = ClientInfo(**client_data)
|
||||
|
||||
for item in self.invoiceitem_set.all():
|
||||
doc.add_item(Item(item.name, item.description, item.count, item.net_each))
|
||||
|
||||
doc.set_item_tax_rate(self.tax_rate)
|
||||
|
||||
for payment in self.invoicepayment_set.all():
|
||||
doc.add_transaction(Transaction(payment.gateway, payment.gateway_id, payment.timestamp, payment.amount))
|
||||
|
||||
bottom_tip = ("Kunden-UID: %s<br /><br />" % profile.vat_id) if profile.vat_id else ""
|
||||
bottom_tip += (f"{ self.payment_instructions }<br /><br />" if self.payment_instructions else "") + getValue("billing.bottom_tip", "")
|
||||
bottom_tip += "<br /><br />" if bottom_tip else ""
|
||||
bottom_tip += "Rechnung erstellt: %s" % str(timezone.now())
|
||||
|
||||
doc.set_bottom_tip(bottom_tip)
|
||||
|
||||
doc.finish()
|
||||
|
||||
self.invoice = ContentFile(output.getvalue(), "invoice.pdf")
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def from_inquiry(cls, inquiry):
|
||||
invoice = cls.objects.create(
|
||||
user = inquiry.client.user,
|
||||
billing_address = BillingAddress.from_profile(inquiry.client),
|
||||
currency = settings.CURRENCY_CODE,
|
||||
tax_rate = 0,
|
||||
inquiry = inquiry
|
||||
)
|
||||
|
||||
InvoiceItem.objects.create(
|
||||
invoice = invoice,
|
||||
name = "Sicherheitsleistung",
|
||||
description = "Rückzahlbare Sicherheitsleistung zu JourneyJoker-Anfrage #%i" % inquiry.id,
|
||||
count = 1,
|
||||
net_each = inquiry.budget
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
class InvoiceItem(models.Model):
|
||||
invoice = models.ForeignKey(Invoice, models.CASCADE)
|
||||
name = models.CharField(max_length=64)
|
||||
description = models.CharField(max_length=256, null=True, blank=True)
|
||||
count = models.IntegerField()
|
||||
net_each = models.DecimalField(max_digits=11, decimal_places=2)
|
||||
|
||||
@property
|
||||
def net_total(self):
|
||||
return self.net_each * self.count
|
||||
|
||||
class InvoicePayment(PolymorphicModel):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
invoice = models.ForeignKey(Invoice, models.PROTECT)
|
||||
amount = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
gateway_id = models.CharField(max_length=256)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
@property
|
||||
def gateway(self):
|
||||
raise NotImplementedError("%s does not implement gateway" % type(self))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
raise NotImplementedError("%s does not implement status" % type(self))
|
||||
|
||||
@classmethod
|
||||
def initiate(cls, invoice):
|
||||
raise NotImplementedError("%s does not implement initiate()" % cls.__name__)
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
return self.invoice.finalize(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_invoice(cls, invoice, gateway):
|
||||
if not invoice.is_paid:
|
||||
responses = initiate_payment.send_robust(sender=cls, invoice=invoice, gateway=gateway)
|
||||
for handler, response in responses:
|
||||
try:
|
||||
return response["redirect"]
|
||||
except:
|
||||
continue
|
||||
return False
|
4
payment/models/__init__.py
Normal file
4
payment/models/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .billingaddress import BillingAddress
|
||||
from .invoice import Invoice
|
||||
from .invoiceitem import InvoiceItem
|
||||
from .invoicepayment import InvoicePayment
|
22
payment/models/billingaddress.py
Normal file
22
payment/models/billingaddress.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from localauth.models import PersonMixin, AddressMixin
|
||||
|
||||
class BillingAddress(PersonMixin, AddressMixin):
|
||||
user = models.ForeignKey(get_user_model(), models.CASCADE)
|
||||
|
||||
@classmethod
|
||||
def from_profile(cls, profile):
|
||||
return cls.objects.create(
|
||||
company = profile.company,
|
||||
vat_id = profile.vat_id,
|
||||
first_name = profile.first_name,
|
||||
last_name = profile.last_name,
|
||||
street = profile.street,
|
||||
city = profile.city,
|
||||
zip = profile.zip,
|
||||
state = profile.state,
|
||||
country = profile.country,
|
||||
user = profile.user
|
||||
)
|
116
payment/models/invoice.py
Normal file
116
payment/models/invoice.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils import timezone
|
||||
|
||||
import uuid
|
||||
|
||||
from dbsettings.functions import getValue
|
||||
|
||||
from auction.models import Inquiry
|
||||
|
||||
from .billingaddress import BillingAddress
|
||||
|
||||
from ..functions import invoice_upload_path
|
||||
from ..pdfviews import InvoicePDFView
|
||||
|
||||
class InvoiceTypeChoices(models.IntegerChoices):
|
||||
INVOICE = (0, "Rechnung")
|
||||
DEPOSIT = (1, "Sicherheitsleistung")
|
||||
|
||||
class Invoice(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
user = models.ForeignKey(get_user_model(), models.PROTECT)
|
||||
billing_address = models.ForeignKey(BillingAddress, models.PROTECT)
|
||||
currency = models.CharField(max_length=3)
|
||||
tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
invoice = models.FileField(null=True, blank=True, upload_to=invoice_upload_path)
|
||||
inquiry = models.OneToOneField(Inquiry, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
type = models.IntegerField(choices=InvoiceTypeChoices.choices)
|
||||
|
||||
@property
|
||||
def price_net(self):
|
||||
price = 0
|
||||
|
||||
for item in self.invoiceitem_set.all():
|
||||
price += item.net_total
|
||||
|
||||
return price
|
||||
|
||||
@property
|
||||
def tax(self):
|
||||
return round(self.price_net * self.tax_rate / 100, 2)
|
||||
|
||||
@property
|
||||
def price_gross(self):
|
||||
return self.price_net + self.tax
|
||||
|
||||
@property
|
||||
def payment_instructions(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
try:
|
||||
return self.invoice.url
|
||||
except:
|
||||
return False
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
paid_amount = 0
|
||||
|
||||
for payment in self.invoicepayment_set.all():
|
||||
paid_amount += payment.amount
|
||||
|
||||
return paid_amount - self.price_gross
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
return self.balance >= 0
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
if self.is_paid:
|
||||
try:
|
||||
self.inquiry.process_payment(*args, **kwargs)
|
||||
except Inquiry.DoesNotExist:
|
||||
pass
|
||||
|
||||
self.generate_invoice()
|
||||
|
||||
def generate_invoice(self):
|
||||
view = InvoicePDFView()
|
||||
|
||||
bottom_tip = (f"{ self.payment_instructions }<br /><br />" if self.payment_instructions else "") + getValue("billing.bottom_tip", "")
|
||||
bottom_tip += "<br />" if bottom_tip else ""
|
||||
bottom_tip += "Dokument erstellt: %s" % str(timezone.now())
|
||||
|
||||
args = {
|
||||
"type": InvoiceTypeChoices._value2label_map_[self.type],
|
||||
"object": self,
|
||||
"bottom_tip": bottom_tip
|
||||
}
|
||||
|
||||
self.invoice = ContentFile(view.render(**args), "invoice.pdf")
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def from_inquiry(cls, inquiry):
|
||||
invoice = cls.objects.create(
|
||||
user = inquiry.client.user,
|
||||
billing_address = BillingAddress.from_profile(inquiry.client),
|
||||
currency = settings.CURRENCY_CODE,
|
||||
tax_rate = 0,
|
||||
inquiry = inquiry
|
||||
)
|
||||
|
||||
InvoiceItem.objects.create(
|
||||
invoice = invoice,
|
||||
name = "SL",
|
||||
description = "Rückzahlbare Sicherheitsleistung zu JourneyJoker-Anfrage #%i" % inquiry.id,
|
||||
count = 1,
|
||||
net_each = inquiry.budget
|
||||
)
|
||||
|
||||
return invoice
|
14
payment/models/invoiceitem.py
Normal file
14
payment/models/invoiceitem.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.db import models
|
||||
|
||||
from .invoice import Invoice
|
||||
|
||||
class InvoiceItem(models.Model):
|
||||
invoice = models.ForeignKey(Invoice, models.CASCADE)
|
||||
name = models.CharField(max_length=64)
|
||||
description = models.CharField(max_length=256, null=True, blank=True)
|
||||
count = models.IntegerField()
|
||||
net_each = models.DecimalField(max_digits=11, decimal_places=2)
|
||||
|
||||
@property
|
||||
def net_total(self):
|
||||
return self.net_each * self.count
|
43
payment/models/invoicepayment.py
Normal file
43
payment/models/invoicepayment.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
import uuid
|
||||
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from .invoice import Invoice
|
||||
|
||||
from ..signals import initiate_payment
|
||||
|
||||
class InvoicePayment(PolymorphicModel):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
invoice = models.ForeignKey(Invoice, models.PROTECT)
|
||||
amount = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
gateway_id = models.CharField(max_length=256)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
@property
|
||||
def gateway(self):
|
||||
raise NotImplementedError("%s does not implement gateway" % type(self))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
raise NotImplementedError("%s does not implement status" % type(self))
|
||||
|
||||
@classmethod
|
||||
def initiate(cls, invoice):
|
||||
raise NotImplementedError("%s does not implement initiate()" % cls.__name__)
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
return self.invoice.finalize(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_invoice(cls, invoice, gateway):
|
||||
if not invoice.is_paid:
|
||||
responses = initiate_payment.send_robust(sender=cls, invoice=invoice, gateway=gateway)
|
||||
for handler, response in responses:
|
||||
try:
|
||||
return response["redirect"]
|
||||
except:
|
||||
continue
|
||||
return False
|
9
payment/pdfviews.py
Normal file
9
payment/pdfviews.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from pdf.views import PDFView
|
||||
|
||||
class InvoicePDFView(PDFView):
|
||||
template_name = "payment/invoice.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault('type', "Rechnung")
|
||||
kwargs.setdefault('title', f'{kwargs["type"]} #{kwargs["object"].id}')
|
||||
return super().get_context_data(**kwargs)
|
|
@ -1,7 +1,7 @@
|
|||
from django.views.generic import DetailView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from .models import InvoicePayment
|
||||
from .models.invoicepayment import InvoicePayment
|
||||
|
||||
class PaymentStatusView(DetailView):
|
||||
model = InvoicePayment
|
||||
|
|
|
@ -24,4 +24,3 @@ python-magic
|
|||
bs4
|
||||
django-starfield
|
||||
pdfkit
|
||||
git+https://kumig.it/kumisystems/PyInvoice
|
||||
|
|
114
static/payment/invoice/invoice.css
Normal file
114
static/payment/invoice/invoice.css
Normal file
|
@ -0,0 +1,114 @@
|
|||
.invoice-box {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 30px;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.invoice-box table {
|
||||
width: 100%;
|
||||
line-height: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.invoice-box table td {
|
||||
padding: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.invoice-box table tr td:first-child {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.invoice-box table tr td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-box table tr.top table td {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.invoice-box table tr.top table td.title {
|
||||
font-size: 45px;
|
||||
line-height: 45px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
table:not(:first) {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.invoice-box table tr.information table td {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.invoice-box table tr.heading td {
|
||||
background: #eee;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invoice-box table tr.details td {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.invoice-box table tr.item td {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.invoice-box table tr.item.last td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.invoice-box table tr.tax td {
|
||||
border-top: 2px solid #eee;
|
||||
}
|
||||
|
||||
.invoice-box table tr.tax.last td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.invoice-box table tr.total td:last-child {
|
||||
border-top: 2px solid #eee;
|
||||
}
|
||||
|
||||
.invoice-box table tr.total {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invoice-box table tr.details.balance {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.invoice-box table tr.top table td {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-box table tr.information table td {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/** RTL **/
|
||||
.invoice-box.rtl {
|
||||
direction: rtl;
|
||||
font-family: Tahoma, 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.invoice-box.rtl table {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-box.rtl table tr td:nth-child(2) {
|
||||
text-align: left;
|
||||
}
|
145
templates/payment/invoice.html
Normal file
145
templates/payment/invoice.html
Normal file
|
@ -0,0 +1,145 @@
|
|||
{% load static %}
|
||||
{% load dbsetting %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "payment/invoice/invoice.css" %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="invoice-box">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tr class="top">
|
||||
<td colspan="2">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="title">
|
||||
<img src="{% static "frontend/images/logo.png" %}" style="width: 100%; max-width: 300px" />
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ type }} #: {{ object.id }}<br />
|
||||
Datum: {{ object.created }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="information">
|
||||
<td colspan="2">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<b>Empfänger:</b><br />
|
||||
{% if object.billing_address.company %}{{ object.billing_address.company }}<br />{% endif %}
|
||||
{{ object.billing_address.full_name }}<br />
|
||||
{{ object.billing_address.street }}<br />
|
||||
{{ object.billing_address.zip }} {{ object.billing_address.city }}<br />
|
||||
{{ object.billing_address.country.name }}{% if object.billing_address.vat_id %}<br />
|
||||
UID: {{ object.billing_address.vat_id }}{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<b>Absender:</b>
|
||||
{% dbsetting "billing.provider.name" "" %}<br />
|
||||
{% dbsetting "billing.provider.street" "" %}<br />
|
||||
{% dbsetting "billing.provider.zip" "" %} {% dbsetting "billing.provider.city" "" %}<br />
|
||||
{% dbsetting "billing.provider.country" "" %}{% dbsetting "billing.provider.vat_id" "" as vat_id %}{% if vat_id %}<br />
|
||||
UID: {{ vat_id }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<tr class="heading">
|
||||
<td>Produkt</td>
|
||||
<td>Beschreibung</td>
|
||||
<td>Anzahl</td>
|
||||
<td>Einzelpreis</td>
|
||||
<td>Gesamtpreis</td>
|
||||
</tr>
|
||||
|
||||
{% for item in object.invoiceitem_set.all %}
|
||||
<tr class="item{% if forloop.last %} last{% endif %}">
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.count }}</td>
|
||||
<td>{{ object.currency }} {{ item.net_each }}</td>
|
||||
<td>{{ object.currency }} {{ item.net_total }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
<tr class="tax">
|
||||
<td>Zwischensumme</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ object.currency }} {{ object.price_net }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class="tax last">
|
||||
<td>{{ object.tax_rate }}% USt</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ object.currency }} {{ object.tax }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class="total">
|
||||
<td>Gesamtsumme</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ object.currency }} {{ object.price_gross }}</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr class="heading">
|
||||
<td>Zahlungsmethode</td>
|
||||
<td>Zahlungs-ID</td>
|
||||
<td>Zahlungsdatum</td>
|
||||
<td>Betrag</td>
|
||||
</tr>
|
||||
|
||||
{% for payment in object.invoicepayment_set.all %}
|
||||
<tr class="details">
|
||||
<td>{{ payment.gateway }}</td>
|
||||
<td>{{ payment.gateway_id }}</td>
|
||||
<td>{{ payment.timestamp }}</td>
|
||||
<td>{{ object.currency }} {{ payment.amount }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if not object.is_paid %}
|
||||
<tr class="details balance">
|
||||
<td>Offener Betrag</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ object.currency }} {{ object.balance }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
</table>
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr class="heading">
|
||||
<td>Weitere Informationen</td>
|
||||
</tr>
|
||||
|
||||
<tr class="details">
|
||||
<td>{% autoescape off %}{{ bottom_tip }}{% endautoescape %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue