merge master

This commit is contained in:
Arvind Tiwari 2018-04-20 20:30:45 +05:30
commit 275ea47e41
32 changed files with 530 additions and 125 deletions

1
.gitignore vendored
View file

@ -42,3 +42,4 @@ secret-key
# to keep empty dirs
!.gitkeep
*.orig

View file

@ -1,3 +1,7 @@
next:
* bgfix: [all] Make /blog available on all domains
* #4367: [dcl] email logo resolution fix
* #4376: [cms] dcl promo section plugin link color changed to brighter shade
1.6.5: 2018-04-08
* #4396: [ungleich] add favicon to ungleich blog
* #4327: [dcl] fix navbar logo repeat

View file

@ -2,6 +2,7 @@ from django.contrib import admin
from cms.admin.placeholderadmin import PlaceholderAdminMixin
from cms.extensions import PageExtensionAdmin
from .cms_models import CMSIntegration, CMSFaviconExtension
from .models import VMPricing
class CMSIntegrationAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
@ -14,3 +15,4 @@ class CMSFaviconExtensionAdmin(PageExtensionAdmin):
admin.site.register(CMSIntegration, CMSIntegrationAdmin)
admin.site.register(CMSFaviconExtension, CMSFaviconExtensionAdmin)
admin.site.register(VMPricing)

View file

@ -9,6 +9,8 @@ from djangocms_text_ckeditor.fields import HTMLField
from filer.fields.file import FilerFileField
from filer.fields.image import FilerImageField
from datacenterlight.models import VMPricing
class CMSIntegration(models.Model):
name = models.CharField(
@ -284,3 +286,12 @@ class DCLSectionPromoPluginModel(CMSPlugin):
if self.background_image:
extra_classes += ' promo-with-bg'
return extra_classes
class DCLCustomPricingModel(CMSPlugin):
pricing = models.ForeignKey(
VMPricing,
related_name="dcl_custom_pricing_vm_pricing",
help_text='Choose a pricing that will be associated with this '
'Calculator'
)

View file

@ -6,9 +6,9 @@ from .cms_models import (
DCLFooterPluginModel, DCLLinkPluginModel, DCLNavbarDropdownPluginModel,
DCLSectionIconPluginModel, DCLSectionImagePluginModel,
DCLSectionPluginModel, DCLNavbarPluginModel,
DCLSectionPromoPluginModel
DCLSectionPromoPluginModel, DCLCustomPricingModel
)
from .models import VMTemplate
from .models import VMTemplate, VMPricing
@plugin_pool.register_plugin
@ -75,13 +75,13 @@ class DCLSectionPromoPlugin(CMSPluginBase):
@plugin_pool.register_plugin
class DCLCalculatorPlugin(CMSPluginBase):
module = "Datacenterlight"
name = "DCL Calculator Plugin"
name = "DCL Calculator Section Plugin"
model = DCLSectionPluginModel
render_template = "datacenterlight/cms/calculator.html"
cache = False
allow_children = True
child_classes = [
'DCLSectionPromoPlugin', 'UngleichHTMLPlugin'
'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCustomPricingPlugin'
]
def render(self, context, instance, placeholder):
@ -89,15 +89,38 @@ class DCLCalculatorPlugin(CMSPluginBase):
context, instance, placeholder
)
context['templates'] = VMTemplate.objects.all()
context['children_to_side'] = []
context['children_to_content'] = []
pricing_plugin_model = None
if instance.child_plugin_instances is not None:
context['children_to_content'].extend(
instance.child_plugin_instances
)
for child in instance.child_plugin_instances:
if child.__class__.__name__ == 'DCLCustomPricingModel':
# The second clause is just to make sure we pick up the
# most recent CustomPricing, if more than one is present
if (pricing_plugin_model is None or child.pricing_id >
pricing_plugin_model.model.pricing_id):
pricing_plugin_model = child
if pricing_plugin_model:
context['vm_pricing'] = VMPricing.get_vm_pricing_by_name(
name=pricing_plugin_model.pricing.name
)
else:
context['vm_pricing'] = VMPricing.get_default_pricing()
return context
@plugin_pool.register_plugin
class DCLCustomPricingPlugin(CMSPluginBase):
module = "Datacenterlight"
name = "DCL Custom Pricing Plugin"
model = DCLCustomPricingModel
render_plugin = False
@plugin_pool.register_plugin
class DCLBannerListPlugin(CMSPluginBase):
module = "Datacenterlight"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-30 21:29+0000\n"
"POT-Creation-Date: 2018-04-17 19:26+0000\n"
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -72,9 +72,9 @@ msgstr "Data Center Light Account Aktivierung"
#, python-format
msgid ""
"You can activate your Data Center Light account by clicking <a "
"href=\"%(base_url)s%(activation_link)s\" style=\"text-decoration: none; "
"color: #4382c8; font-weight: 400;\">here</a>."
"You can activate your Data Center Light account by clicking <a href="
"\"%(base_url)s%(activation_link)s\" style=\"text-decoration: none; color: "
"#4382c8; font-weight: 400;\">here</a>."
msgstr ""
"Klicke <a href=\"%(base_url)s%(activation_link)s\"style=\"text-decoration: "
"none; color: #4382c8; font-weight: 400;\">hier</a> um deinen Data Center "
@ -97,12 +97,13 @@ msgstr "Deine E-Mail-Adresse"
msgid "Password"
msgstr "Passwort"
#, python-format
msgid ""
"You can reset your password <a href=\"%(base_url)s%(reset_password_url)s\" "
"style=\"text-decoration: none; color: #4382c8; font-weight: 400;\">here</a>."
msgstr ""
"Du kannst dein Passwort <a href=\"%(base_url)s%(reset_password_url)s\" "
"style=\"text-decoration: none; color: #4382c8; font-weight: 400;\">hier</a> "
"Du kannst dein Passwort <a href=\"%(base_url)s%(reset_password_url)s\" style="
"\"text-decoration: none; color: #4382c8; font-weight: 400;\">hier</a> "
"zurücksetzen."
msgid "Your Data Center Light Team"
@ -160,21 +161,6 @@ msgstr "Weiter"
msgid "Home"
msgstr "Home"
msgid "Highlights"
msgstr ""
msgid "Scale out"
msgstr "Skalierung"
msgid "Reliable and light"
msgstr "Zuverlässig und leicht"
msgid "Pricing"
msgstr "Preise"
msgid "Order VM"
msgstr "VM bestellen"
msgid "Contact"
msgstr "Kontakt"
@ -184,6 +170,9 @@ msgstr "Nutzungsbedingungen"
msgid "Finally, an affordable VM hosting in Switzerland!"
msgstr "Endlich: bezahlbares VM Hosting in der Schweiz"
msgid "Highlights"
msgstr ""
msgid "I want it!"
msgstr "Das will ich haben!"
@ -214,6 +203,9 @@ msgstr ""
"mit FOSS (Free Open Source Software) arbeitet und wir daher auf "
"Lizenzgebühren verzichten können."
msgid "Scale out"
msgstr "Skalierung"
msgid ""
"We don't use special hardware. We use commodity hardware: we buy computers "
"that you buy. Just many more and put them in a cozy home for computers "
@ -223,6 +215,9 @@ msgstr ""
"erschwingliche Systeme. Bei grösserer Auslastung werden mehr "
"Standardkomponenten hinzugekauft und skalieren so das Datencenter."
msgid "Reliable and light"
msgstr "Zuverlässig und leicht"
msgid ""
"Our VMs are located in Switzerland, with reliable power supply and fast "
"internet connection. Our VM costs less thanks to our featherlight "
@ -232,8 +227,7 @@ msgstr ""
"Energieversorgung, sowie schneller Internetverbindung ausgestattet. Unser "
"Angebot ist aufgrund unserer leichten Infrastruktur überaus kostengünstig."
msgid ""
"Simple and affordable: Try our virtual machine with featherlight price."
msgid "Simple and affordable: Try our virtual machine with featherlight price."
msgstr ""
"Einfach und bezahlbar: Teste nun unsere virtuellen Maschinen mit "
"federleichten Preisen."
@ -314,6 +308,9 @@ msgstr "Gesamt"
msgid "including VAT"
msgstr "inkl. Mehrwertsteuer"
msgid "excluding VAT"
msgstr "exkl. Mehrwertsteuer"
msgid "Month"
msgstr "Monat"
@ -321,13 +318,13 @@ msgid "Credit Card"
msgstr "Kreditkarte"
msgid ""
"Please fill in your credit card information below. We are using <a "
"href=\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do "
"not store your information in our database."
"Please fill in your credit card information below. We are using <a href="
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
"store your information in our database."
msgstr ""
"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen <a "
"href=\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung "
"und speichern keine Informationen in unserer Datenbank."
"Bitte fülle Deine Kreditkarteninformationen unten aus. Wir nutzen <a href="
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
"speichern keine Informationen in unserer Datenbank."
msgid ""
"You are not making any payment yet. After submitting your card information, "
@ -383,13 +380,18 @@ msgstr "Bestellungsübersicht"
msgid "Product"
msgstr "Produkt"
#, python-format
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "VAT"
msgstr "Mehrwertsteuer"
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with the fee of %(vm_price)sCHF/month"
"with the fee of %(vm_total_price)s CHF/month"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF "
"pro Monat belastet"
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
msgid "Place order"
msgstr "Bestellen"
@ -459,21 +461,21 @@ msgid ""
"community."
msgstr ""
"Data Center Light ist ein Teil der Free und Opens Source Software (FOSS) "
"Bewegung.<br/> Wir sind damit gross geworden, leben damit und glauben "
"daran.<br/> Je weiter wir mit unserem Data Center Light vorankommen, desto "
"mehr können wir etwas an die FOSS Community zurückgeben."
"Bewegung.<br/> Wir sind damit gross geworden, leben damit und glauben daran."
"<br/> Je weiter wir mit unserem Data Center Light vorankommen, desto mehr "
"können wir etwas an die FOSS Community zurückgeben."
msgid "We bring the future to you."
msgstr "Wir bringen die Zukunft zu dir."
msgid ""
"Data Center Light uses the most modern technologies out there.<br>Your VM "
"needs only IPv6. Data Center Light provides<br> transparent two-way "
"IPv6/IPv4 translation."
"needs only IPv6. Data Center Light provides<br> transparent two-way IPv6/"
"IPv4 translation."
msgstr ""
"Data Center Light verwendet die zur Zeit modernsten Technologien.<br/>Deine "
"VM läuft mit IPv6. Data Center Light bietet eine transparente "
"IPv6/IPv4-Zweiweglösung."
"VM läuft mit IPv6. Data Center Light bietet eine transparente IPv6/IPv4-"
"Zweiweglösung."
msgid ""
" No more spinning metal plates! Data Center Light uses only SSDs. We keep "
@ -497,6 +499,10 @@ msgstr "Ungültige RAM-Grösse"
msgid "Invalid storage size"
msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
msgid "Confirm Order"
msgstr "Bestellung Bestätigen"
@ -520,6 +526,12 @@ msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
#~ msgid "Pricing"
#~ msgstr "Preise"
#~ msgid "Order VM"
#~ msgstr "VM bestellen"
#~ msgid "Enter name"
#~ msgstr "Name"
@ -533,18 +545,19 @@ msgstr ""
#~ msgstr "Anfrage verschickt"
#~ msgid ""
#~ "Thank you for your subscription! You will receive a confirmation mail from "
#~ "our team"
#~ "Thank you for your subscription! You will receive a confirmation mail "
#~ "from our team"
#~ msgstr ""
#~ "Vielen dank für Ihre Anmeldung. Sie erhalten in kürze eine Bestätigungsmail "
#~ "von unserem Team"
#~ "Vielen dank für Ihre Anmeldung. Sie erhalten in kürze eine "
#~ "Bestätigungsmail von unserem Team"
#~ msgid "Thank you for your request."
#~ msgstr "Vielen Dank für Deine Anfrage."
#~ msgid "You are one step away from being our beta tester!"
#~ msgstr ""
#~ "Sie sind nur noch einen Schritt davon entfernt, unser Beta-Tester zu werden!"
#~ "Sie sind nur noch einen Schritt davon entfernt, unser Beta-Tester zu "
#~ "werden!"
#~ msgid ""
#~ "Currently we are running our tests to make sure everything runs perfectly."
@ -553,8 +566,8 @@ msgstr ""
#~ "sicherzustellen."
#~ msgid ""
#~ "In the meantime, we would like to ask you a little patience<br/> until our "
#~ "team contacts you with beta access."
#~ "In the meantime, we would like to ask you a little patience<br/> until "
#~ "our team contacts you with beta access."
#~ msgstr ""
#~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie "
#~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld."
@ -564,8 +577,8 @@ msgstr ""
#~ msgid "Thank you for order! Our team will contact you via email"
#~ msgstr ""
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich mit"
#~ " Dir via E-Mail in Verbindung."
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich "
#~ "mit Dir via E-Mail in Verbindung."
#~ msgid "Affordable VM hosting based in Switzerland"
#~ msgstr "Bezahlbares VM Hosting in der Schweiz"
@ -581,18 +594,18 @@ msgstr ""
#~ msgid ""
#~ "Our VMs are hosted in Glarus, Switzerland, and our website is currently "
#~ "running in BETA mode. If you want more information that you did not find on "
#~ "our website, or if your order is more detailed, or if you encounter any "
#~ "technical hiccups, please contact us at support@datacenterlight.ch, our team"
#~ " will get in touch with you asap."
#~ "running in BETA mode. If you want more information that you did not find "
#~ "on our website, or if your order is more detailed, or if you encounter "
#~ "any technical hiccups, please contact us at support@datacenterlight.ch, "
#~ "our team will get in touch with you asap."
#~ msgstr ""
#~ "Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden sich"
#~ " zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren und "
#~ "hast auf unserer Website nicht genügend Informationen gefunden? Möchtest "
#~ "eine detailliertere Bestellung aufgeben? Bist du auf technische Probleme "
#~ "gestossen, die du uns mitteilen möchtest? Dann zögere nicht und kontaktiere "
#~ "uns unter support@datacenterlight.ch. Unser Team wird sich umgehend um dein "
#~ "Anliegen kümmern!"
#~ "Unsere VMs werden in der Schweiz im Kanton Glarus gehostet und befinden "
#~ "sich zur Zeit noch in der BETA-Phase. Möchtest du mehr über uns erfahren "
#~ "und hast auf unserer Website nicht genügend Informationen gefunden? "
#~ "Möchtest eine detailliertere Bestellung aufgeben? Bist du auf technische "
#~ "Probleme gestossen, die du uns mitteilen möchtest? Dann zögere nicht und "
#~ "kontaktiere uns unter support@datacenterlight.ch. Unser Team wird sich "
#~ "umgehend um dein Anliegen kümmern!"
#~ msgid "is not a proper name"
#~ msgstr "ist kein gültiger Name"
@ -610,12 +623,14 @@ msgstr ""
#~ "\n"
#~ "Hi,\n"
#~ "\n"
#~ "You can activate your %(dcl_text)s account by clicking here %(base_url)s%(activation_link)s\n"
#~ "You can activate your %(dcl_text)s account by clicking here %(base_url)s"
#~ "%(activation_link)s\n"
#~ msgstr ""
#~ "\n"
#~ "Hallo,\n"
#~ "\n"
#~ "Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst %(base_url)s%(activation_link)s\n"
#~ "Du kannst deinen %(dcl_text)s Account aktivieren, indem du hier klickst "
#~ "%(base_url)s%(activation_link)s\n"
#~ msgid "Your"
#~ msgstr "Dein"
@ -650,12 +665,14 @@ msgstr ""
#~ msgid "I want to have it!"
#~ msgstr "Das möchte ich haben!"
#~ msgid "Reuse existing factory halls intead of building an expensive building."
#~ msgid ""
#~ "Reuse existing factory halls intead of building an expensive building."
#~ msgstr ""
#~ "Nachhaltigkeit: Wiederverwendung ehemaliger Fabrikhallen an Stelle der "
#~ "Errichtung eines neuen Gebäudes"
#~ msgid "Being creative, using modern and alternative design for a datacenter."
#~ msgid ""
#~ "Being creative, using modern and alternative design for a datacenter."
#~ msgstr ""
#~ "Kreativität: Verwendung eines modernen und alternativen Designs für unser "
#~ "Datencenter"
@ -678,8 +695,8 @@ msgstr ""
#~ msgstr "Standort des Datacenters ist in der Schweiz"
#~ msgid ""
#~ " WARNING: We are currently running in BETA mode. We hope you won't encounter"
#~ " any hiccups, but if you do, please let us know at "
#~ " WARNING: We are currently running in BETA mode. We hope you won't "
#~ "encounter any hiccups, but if you do, please let us know at "
#~ "support@datacenterlight.ch"
#~ msgstr ""
#~ " Achtung: Wir befinden uns zurzeit im Beta-Release. Wir hoffen, dass Sie "
@ -693,8 +710,8 @@ msgstr ""
#~ msgstr "Unser Versprechen"
#~ msgid ""
#~ "Instead of creating an expensive SLA for availability, we promise that we do"
#~ " our best to run things as smooth as possible."
#~ "Instead of creating an expensive SLA for availability, we promise that we "
#~ "do our best to run things as smooth as possible."
#~ msgstr ""
#~ "Anstatt eines SLAs (Service Levle Agreements) zu vereinbaren,setzen wir "
#~ "unsere persönliche Arbeitskraft ein, um Ihnen ein sorgenfreiesHosting zu "

View file

@ -0,0 +1,36 @@
from django.core.management.base import BaseCommand
from datacenterlight.models import VMPricing
class Command(BaseCommand):
help = '''Creates default VMPricing object'''
DEFAULT_VMPRICING_NAME = 'default'
def handle(self, *args, **options):
self.create_default_vm_pricing()
def create_default_vm_pricing(self):
obj, created = VMPricing.objects.get_or_create(
name=self.DEFAULT_VMPRICING_NAME,
defaults={
"vat_inclusive": True,
"cores_unit_price": 5,
"ram_unit_price": 2,
"ssd_unit_price": 0.6,
"hdd_unit_price": 0.01
}
)
if created:
print(
'Successfully created {} VMPricing object'.format(
self.DEFAULT_VMPRICING_NAME
)
)
else:
print(
'{} VMPricing exists already.'.format(
self.DEFAULT_VMPRICING_NAME
)
)

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-04-15 22:36
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cms', '0014_auto_20160404_1908'),
('datacenterlight', '0018_auto_20180403_1930'),
]
operations = [
migrations.CreateModel(
name='DCLCustomPricingModel',
fields=[
('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
],
options={
'abstract': False,
},
bases=('cms.cmsplugin',),
),
migrations.CreateModel(
name='VMPricing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('vat_inclusive', models.BooleanField(default=True)),
('vat_percentage', models.DecimalField(blank=True, decimal_places=5, default=0, max_digits=7)),
('cores_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
('ram_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
('ssd_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
('hdd_unit_price', models.DecimalField(decimal_places=6, default=0, max_digits=7)),
],
),
migrations.AddField(
model_name='dclcustompricingmodel',
name='pricing',
field=models.ForeignKey(help_text='Choose a pricing that will be associated with this Calculator', on_delete=django.db.models.deletion.CASCADE, related_name='dcl_custom_pricing_vm_pricing', to='datacenterlight.VMPricing'),
),
]

View file

@ -1,5 +1,9 @@
import logging
from django.db import models
logger = logging.getLogger(__name__)
class VMTemplate(models.Model):
name = models.CharField(max_length=50)
@ -12,6 +16,59 @@ class VMTemplate(models.Model):
return vm_template
class VMPricing(models.Model):
name = models.CharField(max_length=255, unique=True)
vat_inclusive = models.BooleanField(default=True)
vat_percentage = models.DecimalField(
max_digits=7, decimal_places=5, blank=True, default=0
)
cores_unit_price = models.DecimalField(
max_digits=7, decimal_places=5, default=0
)
ram_unit_price = models.DecimalField(
max_digits=7, decimal_places=5, default=0
)
ssd_unit_price = models.DecimalField(
max_digits=7, decimal_places=5, default=0
)
hdd_unit_price = models.DecimalField(
max_digits=7, decimal_places=6, default=0
)
def __str__(self):
return self.name + ' => ' + ' - '.join([
'{}/Core'.format(self.cores_unit_price.normalize()),
'{}/GB RAM'.format(self.ram_unit_price.normalize()),
'{}/GB SSD'.format(self.ssd_unit_price.normalize()),
'{}/GB HDD'.format(self.hdd_unit_price.normalize()),
'{}% VAT'.format(self.vat_percentage.normalize())
if not self.vat_inclusive else 'VAT-Incl', ]
)
@classmethod
def get_vm_pricing_by_name(cls, name):
try:
pricing = VMPricing.objects.get(name=name)
except Exception as e:
logger.error(
"Error getting VMPricing with name {name}. "
"Details: {details}. Attempting to return default"
"pricing.".format(name=name, details=str(e))
)
pricing = VMPricing.get_default_pricing()
return pricing
@classmethod
def get_default_pricing(cls):
""" Returns the default pricing or None """
try:
default_pricing = VMPricing.objects.get(name='default')
except Exception as e:
logger.error(str(e))
default_pricing = None
return default_pricing
class StripePlan(models.Model):
"""
A model to store Data Center Light's created Stripe plans

View file

@ -120,6 +120,11 @@
.header_slider .intro-cap {
font-size: 3.25em;
}
.header_slider > .carousel .item .container {
padding-left: 0;
padding-right: 0;
}
}
.header_slider .intro_lead {

View file

@ -1231,6 +1231,15 @@ footer {
background-position: center;
}
.promo-section.promo-with-bg a {
color: #87B6EA;
}
.promo-section.promo-with-bg a:hover,
.promo-section.promo-with-bg a:focus {
color: #77a6da;
}
.promo-section h3 {
font-weight: 700;
font-size: 36px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -171,7 +171,18 @@
}
function _calcPricing() {
var total = (cardPricing['cpu'].value * 5) + (2 * cardPricing['ram'].value) + (0.6 * cardPricing['storage'].value);
if(typeof window.coresUnitPrice === 'undefined'){
window.coresUnitPrice = 5;
}
if(typeof window.ramUnitPrice === 'undefined'){
window.coresUnitPrice = 2;
}
if(typeof window.ssdUnitPrice === 'undefined'){
window.ssdUnitPrice = 0.6;
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
(cardPricing['storage'].value * window.ssdUnitPrice);
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}

View file

@ -19,6 +19,8 @@ from utils.forms import UserBillingAddressForm
from utils.mailer import BaseEmail
from utils.models import BillingAddress
from .models import VMPricing
logger = get_task_logger(__name__)
@ -56,7 +58,8 @@ def create_vm_task(self, vm_template_id, user, specs, template,
"Running create_vm_task on {}".format(current_task.request.hostname))
vm_id = None
try:
final_price = specs.get('price')
final_price = (specs.get('total_price') if 'total_price' in specs
else specs.get('price'))
billing_address = BillingAddress(
cardholder_name=billing_address_data['cardholder_name'],
street_address=billing_address_data['street_address'],
@ -94,17 +97,22 @@ def create_vm_task(self, vm_template_id, user, specs, template,
if vm_id is None:
raise Exception("Could not create VM")
vm_pricing = VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
) if 'pricing_name' in specs else VMPricing.get_default_pricing()
# Create a Hosting Order
order = HostingOrder.create(
price=final_price,
vm_id=vm_id,
customer=customer,
billing_address=billing_address
billing_address=billing_address,
vm_pricing=vm_pricing
)
# Create a Hosting Bill
HostingBill.create(
customer=customer, billing_address=billing_address)
customer=customer, billing_address=billing_address
)
# Create Billing Address for User if he does not have one
if not customer.user.billing_addresses.count():
@ -130,12 +138,16 @@ def create_vm_task(self, vm_template_id, user, specs, template,
'cores': specs.get('cpu'),
'memory': specs.get('memory'),
'storage': specs.get('disk_size'),
'price': specs.get('price'),
'price': final_price,
'template': template.get('name'),
'vm_name': vm.get('name'),
'vm_id': vm['vm_id'],
'order_id': order.id
}
if 'pricing_name' in specs:
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
))
email_data = {
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,

View file

@ -14,7 +14,7 @@
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>

View file

@ -14,7 +14,7 @@
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>

View file

@ -1,4 +1,16 @@
{% load staticfiles i18n%}
{% if vm_pricing %}
<script type="application/javascript">
window.vat_inclusive = {% if vm_pricing.vat_inclusive %}true{% else %}false{% endif%};
window.vat_percentage = {{vm_pricing.vat_percentage|default:0}};
window.coresUnitPrice = {{vm_pricing.cores_unit_price|default:0}};
window.ramUnitPrice = {{vm_pricing.ram_unit_price|default:0}};
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
</script>
{% endif %}
<form id="order_form" method="POST" action="{% url 'datacenterlight:index' %}" data-toggle="validator" role="form">
{% csrf_token %}
<div class="title">
@ -7,9 +19,11 @@
<div class="price">
<span id="total">15</span>
<span>CHF/{% trans "month" %}</span>
{% if vm_pricing.vat_inclusive %}
<div class="price-text">
<p>{% trans "VAT included" %}</p>
</div>
{% endif %}
</div>
<div class="descriptions">
<div class="description form-group">
@ -78,5 +92,6 @@
</select>
</div>
</div>
<input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input>
<input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input>
</form>

View file

@ -78,7 +78,7 @@
<hr>
<p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p>
<hr>
<p class="last-p"><strong>{%trans "Total" %}</strong>&nbsp;&nbsp;<small>({%trans "including VAT" %})</small> <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong></p>
<p class="last-p"><strong>{%trans "Total" %}</strong>&nbsp;&nbsp;<small>({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})</small> <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong></p>
</div>
</div>
</div>

View file

@ -65,9 +65,19 @@
<span>{% trans "Disk space" %}: </span>
<span class="pull-right">{{vm.disk_size|intcomma}} GB</span>
</p>
{% if vm.vat > 0 %}
<p>
<span>{% trans "Total" %}</span>
<span class="pull-right">{{vm.price|intcomma}} CHF</span>
<strong>{% trans "Subtotal" %}: </strong>
<span class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</span>
</p>
<p>
<span>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%): </span>
<span class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</span>
</p>
{% endif %}
<p>
<strong>{% trans "Total" %}</strong>
<span class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</span>
</p>
</div>
</div>
@ -78,7 +88,7 @@
{% csrf_token %}
<div class="row">
<div class="col-sm-8">
<div class="dcl-place-order-text">{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with the fee of {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
</div>
<div class="col-sm-4 order-confirm-btn text-right">
<button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal">

View file

@ -19,11 +19,11 @@ from hosting.models import HostingOrder
from membership.models import CustomUser, StripeCustomer
from opennebula_api.serializers import VMTemplateSerializer
from utils.forms import BillingAddressForm, BillingAddressFormSignup
from utils.hosting_utils import get_vm_price
from utils.hosting_utils import get_vm_price_with_vat
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
from .forms import ContactForm
from .models import VMTemplate
from .models import VMTemplate, VMPricing
from .utils import get_cms_integration
logger = logging.getLogger(__name__)
@ -93,7 +93,8 @@ class IndexView(CreateView):
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
def get(self, request, *args, **kwargs):
for session_var in ['specs', 'user', 'billing_address_data']:
for session_var in ['specs', 'user', 'billing_address_data',
'pricing_name']:
if session_var in request.session:
del request.session[session_var]
return HttpResponseRedirect(reverse('datacenterlight:cms_index'))
@ -106,12 +107,30 @@ class IndexView(CreateView):
storage = request.POST.get('storage')
storage_field = forms.IntegerField(validators=[self.validate_storage])
template_id = int(request.POST.get('config'))
pricing_name = request.POST.get('pricing_name')
vm_pricing = VMPricing.get_vm_pricing_by_name(pricing_name)
template = VMTemplate.objects.filter(
opennebula_vm_template_id=template_id
).first()
template_data = VMTemplateSerializer(template).data
referer_url = request.META['HTTP_REFERER']
if vm_pricing is None:
vm_pricing_name_msg = _(
"Incorrect pricing name. Please contact support"
"{support_email}".format(
support_email=settings.DCL_SUPPORT_FROM_ADDRESS
)
)
messages.add_message(
self.request, messages.ERROR, vm_pricing_name_msg,
extra_tags='pricing'
)
return HttpResponseRedirect(referer_url + "#order_form")
else:
vm_pricing_name = vm_pricing.name
try:
cores = cores_field.clean(cores)
except ValidationError as err:
@ -139,14 +158,21 @@ class IndexView(CreateView):
)
return HttpResponseRedirect(referer_url + "#order_form")
amount_to_be_charged = get_vm_price(
cpu=cores, memory=memory, disk_size=storage
price, vat, vat_percent = get_vm_price_with_vat(
cpu=cores,
memory=memory,
ssd_size=storage,
pricing_name=vm_pricing_name
)
specs = {
'cpu': cores,
'memory': memory,
'disk_size': storage,
'price': amount_to_be_charged
'price': price,
'vat': vat,
'vat_percent': vat_percent,
'total_price': price + vat,
'pricing_name': vm_pricing_name
}
request.session['specs'] = specs
request.session['template'] = template_data
@ -220,7 +246,10 @@ class PaymentOrderView(FormView):
'site_url': reverse('datacenterlight:index'),
'login_form': HostingUserLoginForm(prefix='login_form'),
'billing_address_form': billing_address_form,
'cms_integration': get_cms_integration('default')
'cms_integration': get_cms_integration('default'),
'vm_pricing': VMPricing.get_vm_pricing_by_name(
self.request.session['specs']['pricing_name']
)
})
return context
@ -393,7 +422,7 @@ class OrderConfirmationView(DetailView):
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
amount_to_be_charged = specs.get('price')
amount_to_be_charged = specs.get('total_price')
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
memory=memory,
disk_size=disk_size)
@ -489,7 +518,7 @@ class OrderConfirmationView(DetailView):
stripe_subscription_obj.id, card_details_dict)
for session_var in ['specs', 'template', 'billing_address',
'billing_address_data',
'token', 'customer']:
'token', 'customer', 'pricing_name']:
if session_var in request.session:
del request.session[session_var]

View file

@ -67,6 +67,7 @@ urlpatterns += i18n_patterns(
include('ungleich_page.urls',
namespace='ungleich_page'),
name='ungleich_page'),
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
)
urlpatterns += [

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2018-04-16 00:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0019_auto_20180415_2236'),
('hosting', '0043_vmdetail'),
]
operations = [
migrations.AddField(
model_name='hostingorder',
name='vm_pricing',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='datacenterlight.VMPricing'),
preserve_default=False,
),
]

View file

@ -6,6 +6,8 @@ from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from Crypto.PublicKey import RSA
from datacenterlight.models import VMPricing
from membership.models import StripeCustomer, CustomUser
from utils.models import BillingAddress
from utils.mixins import AssignPermissionsMixin
@ -53,6 +55,7 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
stripe_charge_id = models.CharField(max_length=100, null=True)
price = models.FloatField()
subscription_id = models.CharField(max_length=100, null=True)
vm_pricing = models.ForeignKey(VMPricing)
permissions = ('view_hostingorder',)
@ -70,12 +73,13 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
@classmethod
def create(cls, price=None, vm_id=None, customer=None,
billing_address=None):
billing_address=None, vm_pricing=None):
instance = cls.objects.create(
price=price,
vm_id=vm_id,
customer=customer,
billing_address=billing_address
billing_address=billing_address,
vm_pricing=vm_pricing
)
instance.assign_permissions(customer.user)
return instance

View file

@ -14,7 +14,7 @@
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>

View file

@ -14,7 +14,7 @@
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{base_url}}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>

View file

@ -14,7 +14,7 @@
<table style="width: 100%; border-spacing: 0; border-collapse: collapse; max-width: 560px;">
<tr>
<td>
<img src="{{ base_url }}{% static 'datacenterlight/img/logo_black.png' %}" style="width: 200px; height: 50px;">
<img src="{{ base_url }}{% static 'datacenterlight/img/datacenterlight.png' %}" style="max-width: 200px;">
</td>
</tr>
<tr>

View file

@ -74,7 +74,7 @@
<center style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
<table cellpadding="0" cellspacing="0" width="600" class="w320" style="border-collapse: collapse !important; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;"><tr style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important;">
<td class="pull-left mobile-header-padding-left" style="vertical-align: middle; border-collapse: collapse; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; font-size: 14px; color: #777777; text-align: left; line-height: 21px; width: 290px; padding-left: 10px;" align="left" valign="middle">
<a href="{{base_url}}" style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; color: #676767; text-decoration: none !important;"><img width="137" src="{{base_url}}{% static "hosting/img/logo_black.png" %}" alt="logo" style="max-width: 600px; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; border: none;"></a>
<a href="{{base_url}}" style="font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; color: #676767; text-decoration: none !important;"><img width="137" src="{{base_url}}{% static 'hosting/img/datacenterlight.png' %}" alt="logo" style="max-width: 600px; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; border: none;"></a>
</td>
<td class="pull-right mobile-header-padding-right" style="color: #4d4d4d; border-collapse: collapse; font-family: 'Oxygen', 'Helvetica Neue', 'Arial', 'sans-serif' !important; font-size: 14px; text-align: right; line-height: 21px; width: 290px; padding-left: 10px;" align="right">
</td>

View file

@ -127,9 +127,19 @@
<span>{% trans "Disk space" %}: </span>
<span class="pull-right">{{vm.disk_size}} GB</span>
</p>
{% if vm.vat > 0 %}
<p>
<span>{% trans "Total" %}</span>
<span class="pull-right">{{vm.price|intcomma}} CHF</span>
<strong>{% trans "Subtotal" %}: </strong>
<span class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</span>
</p>
<p>
<span>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%): </span>
<span class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</span>
</p>
{% endif %}
<p>
<strong>{% trans "Total" %}</strong>
<span class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</span>
</p>
</div>
</div>

View file

@ -29,7 +29,7 @@
<tr>
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y H:i" }}</td>
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|intcomma }}</td>
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>
</td>

View file

@ -45,7 +45,7 @@
<h2 class="vm-detail-title">{% trans "Billing" %} <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon"></h2>
<div class="vm-vmid">
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
<div class="vm-item-lg">{{virtual_machine.price|floatformat|intcomma}} CHF/{% trans "Month" %}</div>
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}</div>
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
</div>
</div>

View file

@ -42,7 +42,7 @@ from utils.forms import (
BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm,
ResendActivationEmailForm
)
from utils.hosting_utils import get_vm_price
from utils.hosting_utils import get_vm_price, get_vm_price_with_vat
from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
@ -749,11 +749,17 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
context['vm']['price'] = get_vm_price(
price, vat, vat_percent = get_vm_price_with_vat(
cpu=context['vm']['cores'],
disk_size=context['vm']['disk_size'],
memory=context['vm']['memory']
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.vm_pricing.name
if obj.vm_pricing else 'default')
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + vat
context['subscription_end_date'] = vm_detail.end_date()
except VMDetail.DoesNotExist:
try:
@ -762,6 +768,17 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
price, vat, vat_percent = get_vm_price_with_vat(
cpu=context['vm']['cores'],
ssd_size=context['vm']['disk_size'],
memory=context['vm']['memory'],
pricing_name=(obj.vm_pricing.name
if obj.vm_pricing else 'default')
)
context['vm']['vat'] = vat
context['vm']['price'] = price
context['vm']['vat_percent'] = vat_percent
context['vm']['total_price'] = price + vat
except WrongIdError:
messages.error(
self.request,
@ -1100,7 +1117,8 @@ class VirtualMachineView(LoginRequiredMixin, View):
context = {
'virtual_machine': serializer.data,
'order': HostingOrder.objects.get(
vm_id=serializer.data['vm_id'])
vm_id=serializer.data['vm_id']
)
}
except Exception as ex:
logger.debug("Exception generated {}".format(str(ex)))

View file

@ -1,6 +1,8 @@
import decimal
import logging
from oca.pool import WrongIdError
from datacenterlight.models import VMPricing
from hosting.models import UserHostingKey, VMDetail
from opennebula_api.serializers import VirtualMachineSerializer
@ -49,14 +51,74 @@ def get_or_create_vm_detail(user, manager, vm_id):
return vm_detail_obj
def get_vm_price(cpu, memory, disk_size):
def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
"""
A helper function that computes price of a VM from given cpu, ram and
ssd parameters
:param cpu: Number of cores of the VM
:param memory: RAM of the VM
:param disk_size: Disk space of the VM
:param disk_size: Disk space of the VM (SSD)
:param hdd_size: The HDD size
:param pricing_name: The pricing name to be used
:return: The price of the VM
"""
return (cpu * 5) + (memory * 2) + (disk_size * 0.6)
try:
pricing = VMPricing.objects.get(name=pricing_name)
except Exception as ex:
logger.error(
"Error getting VMPricing object for {pricing_name}."
"Details: {details}".format(
pricing_name=pricing_name, details=str(ex)
)
)
return None
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(disk_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
return float(price)
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
pricing_name='default'):
"""
A helper function that computes price of a VM from given cpu, ram and
ssd, hdd and the pricing parameters
:param cpu: Number of cores of the VM
:param memory: RAM of the VM
:param ssd_size: Disk space of the VM (SSD)
:param hdd_size: The HDD size
:param pricing_name: The pricing name to be used
:return: The a tuple containing the price of the VM, the VAT and the
VAT percentage
"""
try:
pricing = VMPricing.objects.get(name=pricing_name)
except Exception as ex:
logger.error(
"Error getting VMPricing object for {pricing_name}."
"Details: {details}".format(
pricing_name=pricing_name, details=str(ex)
)
)
return None
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
if pricing.vat_inclusive:
vat = decimal.Decimal(0)
vat_percent = decimal.Decimal(0)
else:
vat = price * pricing.vat_percentage * decimal.Decimal(0.01)
vat_percent = pricing.vat_percentage
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
return float(price), float(vat), float(vat_percent)