diff --git a/.gitignore b/.gitignore index ec901a3..5c039d9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ dist/ *.iso *.sqlite3 .DS_Store +static/CACHE/ diff --git a/matrixhosting/static/matrixhosting/css/invoice.css b/matrixhosting/static/matrixhosting/css/invoice.css new file mode 100644 index 0000000..60ee335 --- /dev/null +++ b/matrixhosting/static/matrixhosting/css/invoice.css @@ -0,0 +1,113 @@ +body { + font-family: Avenir; + background: white; + font-weight: 500; + line-height: 1.1em; + font-size: 16px; + margin: auto; +} +p { + display: block; + -webkit-margin-before: 14px; + -webkit-margin-after: 14px; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; +} +.bold { + font-weight: bold; +} +.d1 { + line-height:1.1em; + width: 60%; + float: left; +} +.d2 { + line-height:1.5em; + padding-top: 15px; + width: 40%; + float: left; +} +.d4 { + line-height:1.5em; + width:40%; + float: left; +} +.b1 { + width: 45%; + float: left; +} +.b2 { + width: 55%; + float: left; + text-align: right; + left: 0; +} +.d5 { + width: 100%; +} +.d6 { + width: 68%; + float: left; + font-size: 13px; +} +.d7 { + width: 32%; + float: left; +} +.wf { + width: 100%; +} +hr { + border: 0; + clear:both; + display: inline-block; + width: 100%; + background-color:gray; + height: 1px; + } + .tl { + text-align: left; + margin-left: 5px; + } + + .tr { + text-align: right; + margin-right: 5px; + float: right; + } + .tc { + text-align: center; + } + .pc p { + display: block; + -webkit-margin-before: 3px; + -webkit-margin-after: 5px; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; +} + .th { + border-top: 1px solid gray; + border-bottom: 1px solid gray; + + } + .ts { + font-size: 14px; + } + .icon { + width: 16px; + height: 14px; + vertical-align: middle; + margin-right: 2px; + } + .footer { + margin-top: 70px; + font-size: 14px; + } + + .footer p { + display: block; + -webkit-margin-before: 5px; + -webkit-margin-after: 5px; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; +} \ No newline at end of file diff --git a/matrixhosting/static/matrixhosting/images/call.png b/matrixhosting/static/matrixhosting/images/call.png new file mode 100644 index 0000000..e774362 Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/call.png differ diff --git a/matrixhosting/static/matrixhosting/images/company-large.jpg b/matrixhosting/static/matrixhosting/images/company-large.jpg new file mode 100644 index 0000000..32d48cd Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/company-large.jpg differ diff --git a/matrixhosting/static/matrixhosting/images/company-small.jpg b/matrixhosting/static/matrixhosting/images/company-small.jpg new file mode 100644 index 0000000..a7b575f Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/company-small.jpg differ diff --git a/matrixhosting/static/matrixhosting/images/home.png b/matrixhosting/static/matrixhosting/images/home.png new file mode 100644 index 0000000..24428e7 Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/home.png differ diff --git a/matrixhosting/static/matrixhosting/images/msg.png b/matrixhosting/static/matrixhosting/images/msg.png new file mode 100644 index 0000000..3b7b0c7 Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/msg.png differ diff --git a/matrixhosting/static/matrixhosting/images/twitter.png b/matrixhosting/static/matrixhosting/images/twitter.png new file mode 100644 index 0000000..4db6da0 Binary files /dev/null and b/matrixhosting/static/matrixhosting/images/twitter.png differ diff --git a/matrixhosting/static/matrixhosting/js/payment.js b/matrixhosting/static/matrixhosting/js/payment.js index 14d806e..6fdf6fd 100644 --- a/matrixhosting/static/matrixhosting/js/payment.js +++ b/matrixhosting/static/matrixhosting/js/payment.js @@ -8,7 +8,50 @@ function setBrandIcon(brand) { brandIconElement.classList.add(pfClass); } +function fetch_pricing() { + var url = '/pricing/' + $('input[name="pricing_name"]').val() + '/calculate/'; + var cores = $('#cores').val(); + var memory = $('#memory').val(); + var storage = $('#storage').val(); + $.ajax({ + type: 'GET', + url: url, + data: { cores: cores, memory: memory, storage: storage}, + dataType: 'json', + success: function (data) { + if (data && data['total']) { + $('#total').text(data['total']); + } + } + }); +}; + +function incrementValue(e) { + var valueElement = $(e.target).parent().parent().find('input'); + var step = $(valueElement).attr('step'); + var min = parseInt($(valueElement).attr('min')); + var max = parseInt($(valueElement).attr('max')); + var new_value = 0; + if (e.data.inc == 1) { + new_value = Math.min(parseInt($(valueElement).val()) + parseInt(step) * e.data.inc, max); + } else { + new_value = Math.max(parseInt($(valueElement).val()) + parseInt(step) * e.data.inc, min); + } + $(valueElement).val(new_value); + fetch_pricing(); + return false; +}; + + $(document).ready(function () { + if ($('#pricing_name') != undefined) { + fetch_pricing(); + } + + $('.fa-plus-circle.right').bind('click', {inc: 1}, incrementValue); + + $('.fa-minus-circle.left').bind('click', {inc: -1}, incrementValue); + var hasCreditcard = window.hasCreditcard || false; if (hasCreditcard && window.stripeKey) { var stripe = Stripe(window.stripeKey); @@ -154,7 +197,7 @@ $(document).ready(function () { }); $('#checkout-btn').click(function () { - if($('input[name="payment_card"]:checked').size() == 1) { + if($('input[name="payment_card"]:checked').length == 1) { var id = $('input[name="payment_card"]:checked').val(); if (id != 'new') { $('#id_card').val(id); diff --git a/matrixhosting/static/matrixhosting/webfonts/Avenir-Book.ttf b/matrixhosting/static/matrixhosting/webfonts/Avenir-Book.ttf new file mode 100644 index 0000000..84ae914 Binary files /dev/null and b/matrixhosting/static/matrixhosting/webfonts/Avenir-Book.ttf differ diff --git a/matrixhosting/templates/matrixhosting/base.html b/matrixhosting/templates/matrixhosting/base.html index fa4adcf..dcda82d 100644 --- a/matrixhosting/templates/matrixhosting/base.html +++ b/matrixhosting/templates/matrixhosting/base.html @@ -1,4 +1,4 @@ -{% load static i18n %} {% get_current_language as LANGUAGE_CODE %} +{% load static compress i18n %} {% get_current_language as LANGUAGE_CODE %} @@ -22,11 +22,13 @@ type="text/css" /> + {% compress css %} + {% endcompress %} {% block css_extra %} {% endblock css_extra %} @@ -55,6 +57,8 @@ {% block js_extra %} {% endblock js_extra %} - + {% compress js %} + + {% endcompress %} diff --git a/matrixhosting/templates/matrixhosting/invoice.html b/matrixhosting/templates/matrixhosting/invoice.html index 827fae7..dbe8b96 100644 --- a/matrixhosting/templates/matrixhosting/invoice.html +++ b/matrixhosting/templates/matrixhosting/invoice.html @@ -1,49 +1,171 @@ {% load static i18n %} -{% get_current_language as LANGUAGE_CODE %} - + - - - - - - - Hosting Invoice - - - - - - - + + + + {%trans "Invoice: " %} {{bill.id}} + + - -
- -
-
-
- -
-
-

Invoice

-
-
-
-
- - -
-
- - +
+ +
+
+ +
+
+ ungleich glarus ag +
+ Bahnhofstrasse 1
+ 8783 Linthal
+ Switzerland +
+
+
+ +
+ + {{bill.billing_address.full_name}} + +
+ + {{bill.billing_address.owner.email}} +
+ {{bill.billing_address.street}}
+ {{bill.billing_address.city}}
+ {{bill.billing_address.get_country_display}} +
+
+
+
+
+
+ {%trans "Date of invoice:" %} +
+ +
+ {{bill.starting_date|date}} +
+
+
+
+
+ {%trans "Invoice Number:" %} +
+
+ #{{bill.id}} +
+
+
+
+ {%trans "Due Date:" %} + +
+
+ {{bill.due_date}} + +
+
+
+
+
+
+
+ {%trans "INVOICE" %} + {{bill.starting_date|date:"m-Y"}} +
+
+ + + + + + + + + {% for record in bill.bill_records.all %} + + + + + + {% endfor %} +
{%trans "Product" %}{%trans "Quantity" %}{%trans "Amount in " %} CHF
+ {{record.description}} + + {{record.quantity}} + + {{record.subtotal}} +
+
+
+

+ SubTotal + {{bill.subtotal}} + +

+

+ {{vat_rate}} + {{tax_amount}} + +

+
+
+

+ {%trans "CHF" %} + {{bill.sum}} +

+
+
+
+
+ \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/order_confirmation.html b/matrixhosting/templates/matrixhosting/order_confirmation.html index e8f9799..b7899e2 100644 --- a/matrixhosting/templates/matrixhosting/order_confirmation.html +++ b/matrixhosting/templates/matrixhosting/order_confirmation.html @@ -1,6 +1,6 @@ {% extends "matrixhosting/base.html" %} -{% load static i18n %} +{% load static compress i18n %} {% block title %} Request Details {% endblock %} @@ -130,7 +130,7 @@

{% trans "VAT for" %} {{pricing.vat_country}} ({{pricing.vat_percent}}%)

-

CHF

+

{{pricing.vat_amount}} CHF

@@ -203,5 +203,7 @@ aria-hidden="true" data-backdrop="static" data-keyboard="false"> + {% compress js %} + {% endcompress %} {% endblock js_extra %} \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/order_details.html b/matrixhosting/templates/matrixhosting/order_details.html index 799f89c..3654341 100644 --- a/matrixhosting/templates/matrixhosting/order_details.html +++ b/matrixhosting/templates/matrixhosting/order_details.html @@ -1,6 +1,6 @@ {% extends "matrixhosting/base.html" %} -{% load static i18n %} +{% load static compress i18n %} {% block title %} Request Details {% endblock %} @@ -196,7 +196,7 @@ {% endif %}
-
{{request.session.pricing.total|floatformat}}CHF
+
{{request.session.pricing.total|floatformat}}CHF

@@ -269,7 +269,9 @@ (function () { window.stripeKey = "{{stripe_key}}"; window.current_lan = "{{LANGUAGE_CODE}}"; - window.hasCreditcard = "{{show_cards}}"; + {% if show_cards %} + window.hasCreditcard = true; + {% endif %} })(); {%endif%} @@ -278,5 +280,7 @@ - + {% compress js %} + + {% endcompress %} {% endblock js_extra %} \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/order_success.html b/matrixhosting/templates/matrixhosting/order_success.html index 2492cac..d5ab677 100644 --- a/matrixhosting/templates/matrixhosting/order_success.html +++ b/matrixhosting/templates/matrixhosting/order_success.html @@ -50,7 +50,4 @@ {% endblock %} {% block js_extra %} - - - {% endblock js_extra %} \ No newline at end of file diff --git a/matrixhosting/urls.py b/matrixhosting/urls.py index 6954e11..f98ba4d 100644 --- a/matrixhosting/urls.py +++ b/matrixhosting/urls.py @@ -1,7 +1,7 @@ from django.urls import path, include from django.conf import settings from django.conf.urls.static import static - +from wkhtmltopdf.views import PDFTemplateView from .views import * app_name = 'matrixhosting' diff --git a/matrixhosting/utils.py b/matrixhosting/utils.py index 61fb46b..e69de29 100644 --- a/matrixhosting/utils.py +++ b/matrixhosting/utils.py @@ -1,19 +0,0 @@ -import os -from io import BytesIO -from django.http import HttpResponse -from django.template.loader import get_template -from django.conf import settings - -from xhtml2pdf import pisa - -def render_to_pdf(template_src, context_dict={}): - template = get_template(template_src) - html = template.render(context_dict) - result = BytesIO() - # pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result) - links = lambda uri, rel: os.path.join(settings.MEDIA_ROOT, uri.replace(settings.MEDIA_URL, '')) - pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")),dest=result) - - if not pdf.err: - return HttpResponse(result.getvalue(), content_type='application/pdf') - return None \ No newline at end of file diff --git a/matrixhosting/views.py b/matrixhosting/views.py index b25e6e3..7cd0c2a 100644 --- a/matrixhosting/views.py +++ b/matrixhosting/views.py @@ -17,6 +17,7 @@ from django.conf import settings from django.http import ( HttpResponseRedirect, JsonResponse, HttpResponse ) +from wkhtmltopdf.views import PDFTemplateResponse from rest_framework import viewsets, permissions from uncloud_pay.models import PricingPlan @@ -27,7 +28,7 @@ from uncloud_pay.selectors import get_billing_address_for_user, has_enough_balan import uncloud_pay.stripe as uncloud_stripe from .models import VMInstance from .serializers import * -from .utils import render_to_pdf +from .utils import * logger = logging.getLogger(__name__) @@ -195,6 +196,7 @@ class OrderDetailsView(DetailView): request.session.get('order')) if order: bill = Bill.create_next_bill_for_user_address(billing_address) + self.request.session['bill_id'] = bill.id payment= Payment.withdraw(owner=request.user, amount=total, notes=f"BILL #{bill.id}") if payment: #Close the bill as the payment has been added @@ -244,26 +246,40 @@ class OrderSuccessView(DetailView): 'order': self.request.session.get('order'), 'balance': get_balance_for_user(self.request.user) } - # if ('order' not in request.session): - # return HttpResponseRedirect(reverse('matrix:index')) + if ('order' not in request.session): + return HttpResponseRedirect(reverse('matrix:index')) return render(request, self.template_name, context) class InvoiceDownloadView(View): - def get(self, request, *args, **kwargs): - data = { - 'today': datetime.date.today(), - 'amount': 39.99, - 'customer_name': 'Cooper Mann', - 'order_id': 1233434, - } - pdf = render_to_pdf('matrixhosting/invoice.html', data) - if pdf: - response = HttpResponse(pdf, content_type='application/pdf') - content = "inline; filename=invoice.pdf" - content = "attachment; filename=invoice.pdf" - response['Content-Disposition'] = content - return response - return HttpResponse("Not found") + template = 'matrixhosting/invoice.html' + filename = 'invoice.pdf' + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = {'base_url': f'{self.request.scheme}://{self.request.get_host()}'} + bill = Bill.objects.get(id=self.request.session.get('bill_id')) + if bill: + context['bill'] = bill + context['vat_rate'] = str(round(bill.vat_rate * 100, 2)) + '%' + context['tax_amount'] = round(bill.vat_rate * bill.subtotal, 2) + return context + + def get(self, request): + cmd_options = { + 'page_height': 240, + 'page_width':175, + 'orientation': 'Portrait', + 'header_spacing': 65, + 'header_line': False + } + return PDFTemplateResponse(request=request, + template=self.template, + filename = self.filename, + cmd_options= cmd_options, + context= self.get_context_data()) class Dashboard(ListView): diff --git a/requirements.txt b/requirements.txt index 575a52a..7517d42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,8 +7,9 @@ fontawesome-free psycopg2 ldap3 django-allauth +django-compressor xmltodict -xhtml2pdf +django-wkhtmltopdf parsedatetime # Follow are for creating graph models pyparsing diff --git a/uncloud/settings.py b/uncloud/settings.py index 86e0942..c5567c0 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -62,6 +62,8 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.staticfiles', 'django_extensions', + 'compressor', + 'wkhtmltopdf', 'rest_framework', 'django_q', 'notifications', @@ -90,6 +92,7 @@ MIDDLEWARE = [ ] ROOT_URLCONF = 'uncloud.urls' +WKHTMLTOPDF_CMD = "/usr/local/bin/wkhtmltopdf" TEMPLATES = [ { @@ -188,11 +191,13 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' -STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] +STATIC_ROOT = os.path.join(BASE_DIR, "static") STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', ] +COMPRESS_ENABLED = True #VM Deployment TEMPLATE GITLAB_SERVER = env('GITLAB_SERVER') diff --git a/uncloud_pay/migrations/0025_auto_20210803_2118.py b/uncloud_pay/migrations/0025_auto_20210803_2118.py new file mode 100644 index 0000000..5be92a5 --- /dev/null +++ b/uncloud_pay/migrations/0025_auto_20210803_2118.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.4 on 2021-08-03 21:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0024_auto_20210730_1441'), + ] + + operations = [ + migrations.AlterField( + model_name='billrecord', + name='bill', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bill_records', to='uncloud_pay.bill'), + ), + migrations.AlterField( + model_name='payment', + name='type', + field=models.CharField(choices=[('withdraw', 'Withdraw Money'), ('deposit', 'Deposit Money')], default='deposit', max_length=256), + ), + ] diff --git a/uncloud_pay/migrations/0026_remove_order_description.py b/uncloud_pay/migrations/0026_remove_order_description.py new file mode 100644 index 0000000..c81cdfe --- /dev/null +++ b/uncloud_pay/migrations/0026_remove_order_description.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.4 on 2021-08-03 21:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0025_auto_20210803_2118'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='description', + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index e45db0d..d5ccedb 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -649,8 +649,6 @@ class Order(models.Model): customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE, null=True) - description = models.TextField() - product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE) config = models.JSONField() @@ -806,6 +804,19 @@ class Order(models.Model): @property def is_one_time(self): return not self.is_recurring + + @property + def description(self): + desc = self.product.description + "( " + config = json.loads(self.config) + if config and config["cores"]: + desc = f"{desc} {config['cores']} Cores," + if config and config["memory"]: + desc = f"{desc} {config['memory']} RAM," + if config and config["storage"]: + desc = f"{desc} {config['storage']} GB SSD," + desc += " )" + return desc def replace_with(self, new_order): new_order.replaces = self @@ -1011,6 +1022,11 @@ class Bill(models.Model): bill_records = BillRecord.objects.filter(bill=self) return sum([ br.sum for br in bill_records ]) + @property + def subtotal(self): + bill_records = BillRecord.objects.filter(bill=self) + return sum([ br.subtotal for br in bill_records ]) + @property def vat_rate(self): return VATRate.get_vat_rate(self.billing_address, when=self.ending_date) @@ -1114,7 +1130,7 @@ class BillRecord(models.Model): Entry of a bill, dynamically generated from an order. """ - bill = models.ForeignKey(Bill, on_delete=models.CASCADE) + bill = models.ForeignKey(Bill, on_delete=models.CASCADE, related_name='bill_records') order = models.ForeignKey(Order, on_delete=models.CASCADE) creation_date = models.DateTimeField(auto_now_add=True) @@ -1142,12 +1158,30 @@ class BillRecord(models.Model): else: return self.order.one_time_price + @property + def description(self): + if self.order: + return self.order.description + return '' + @property def price(self): if self.is_recurring_record: return self.order.recurring_price else: return self.order.one_time_price + + @property + def subtotal(self): + billing_address_ins = self.order.billing_address + vat_rate = VATRate.get_vat_rate(billing_address_ins) + vat_validation_status = "verified" if billing_address_ins.vat_number_validated_on and billing_address_ins.vat_number_verified else False + config = json.loads(self.order.config) + pricing = uncloud_pay.utils.get_order_total_with_vat( + config["cores"], config["memory"], config["storage"], self.order.pricing_plan.name, + vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status + ) + return pricing['subtotal_after_discount'] def __str__(self): if self.is_recurring_record: diff --git a/uncloud_pay/utils.py b/uncloud_pay/utils.py index cf5d09c..16c2ce7 100644 --- a/uncloud_pay/utils.py +++ b/uncloud_pay/utils.py @@ -124,10 +124,11 @@ def apply_vat_discount(subtotal, pricing_plan, vat_rate=False, vat_validation_st 'amount_with_vat': round(float(discount_amount_with_vat), 2) } subtotal_after_discount = subtotal - discount["amount"] + vat_amount = round(vat_percent * 0.01 * subtotal_after_discount, 2) price_after_discount_with_vat = round((subtotal - discount['amount']) * (1 + vat_percent * 0.01), 2) return (subtotal, round(float(subtotal_after_discount), 2), price_after_discount_with_vat, - round(float(vat), 2), vat_percent, discount) + round(float(vat), 2), vat_percent, vat_amount, discount) def get_order_total_with_vat(cores, memory, storage, @@ -149,13 +150,14 @@ def get_order_total_with_vat(cores, memory, storage, (decimal.Decimal(memory) * pricing.ram_unit_price) + (decimal.Decimal(storage) * (pricing.storage_unit_price)) ) - subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = \ + subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, vat_amount, discount = \ apply_vat_discount(subtotal, pricing, vat_rate, vat_validation_status) return { "name": pricing.name, "subtotal": subtotal, "discount": discount, "vat": vat, "vat_percent": vat_percent, + 'vat_amount': vat_amount, "vat_validation_status": vat_validation_status, "subtotal_after_discount": subtotal_after_discount, "total": price_after_discount_with_vat