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"
|
template_name = "frontend/404.html"
|
||||||
|
|
||||||
class DemoTemplateView(TemplateView):
|
class DemoTemplateView(TemplateView):
|
||||||
template_name = "mail/verify.html"
|
template_name = "payment/invoice.html"
|
||||||
|
|
||||||
class ImpressumView(TemplateView):
|
class ImpressumView(TemplateView):
|
||||||
template_name = "frontend/impressum.html"
|
template_name = "frontend/impressum.html"
|
||||||
|
|
|
@ -117,6 +117,21 @@ class PersonMixin(models.Model):
|
||||||
first_name = models.CharField("Vorname", max_length=64)
|
first_name = models.CharField("Vorname", max_length=64)
|
||||||
last_name = models.CharField("Nachname", 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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
@ -125,15 +140,6 @@ class Profile(PersonMixin, AddressMixin, PhoneMixin):
|
||||||
verified = models.BooleanField(default=False)
|
verified = models.BooleanField(default=False)
|
||||||
enabled = models.BooleanField(default=True)
|
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:
|
class Meta:
|
||||||
abstract = True
|
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.views.generic import DetailView
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from .models import InvoicePayment
|
from .models.invoicepayment import InvoicePayment
|
||||||
|
|
||||||
class PaymentStatusView(DetailView):
|
class PaymentStatusView(DetailView):
|
||||||
model = InvoicePayment
|
model = InvoicePayment
|
||||||
|
|
|
@ -24,4 +24,3 @@ python-magic
|
||||||
bs4
|
bs4
|
||||||
django-starfield
|
django-starfield
|
||||||
pdfkit
|
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