Merge branch 'master' into task/5681/offer-512mb-ram
This commit is contained in:
		
				commit
				
					
						42d6f38f0c
					
				
			
		
					 20 changed files with 1017 additions and 305 deletions
				
			
		|  | @ -1,3 +1,5 @@ | |||
| 2.3: 2018-10-08 | ||||
|     * #5690: Generic payment page - allow admin to add a onetime/monthly product and the frontend for user to pay for this product (PR #666) | ||||
| 2.2.2: 2018-09-28 | ||||
|     * #5721: Set calculator OS list in alphabetical order and set `Devuan Ascii` as the default (PR #668) | ||||
|     * bugfix: Fix some typos and correct DE translations (PR #667) | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ from .cms_models import ( | |||
|     DCLSectionPromoPluginModel, DCLCalculatorPluginModel | ||||
| ) | ||||
| from .models import VMTemplate | ||||
| from datacenterlight.utils import clear_all_session_vars | ||||
| 
 | ||||
| 
 | ||||
| @plugin_pool.register_plugin | ||||
|  | @ -85,6 +86,7 @@ class DCLCalculatorPlugin(CMSPluginBase): | |||
|     require_parent = True | ||||
| 
 | ||||
|     def render(self, context, instance, placeholder): | ||||
|         clear_all_session_vars(context['request']) | ||||
|         context = super(DCLCalculatorPlugin, self).render( | ||||
|             context, instance, placeholder | ||||
|         ) | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ msgid "" | |||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2018-07-05 23:11+0000\n" | ||||
| "POT-Creation-Date: 2018-09-26 20:44+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" | ||||
|  | @ -293,6 +293,9 @@ msgstr "Registrieren" | |||
| msgid "Billing Address" | ||||
| msgstr "Rechnungsadresse" | ||||
| 
 | ||||
| msgid "Make a payment" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Your Order" | ||||
| msgstr "Deine Bestellung" | ||||
| 
 | ||||
|  | @ -336,9 +339,9 @@ msgid "" | |||
| "database." | ||||
| msgstr "" | ||||
| "Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine " | ||||
| "Kreditkartendetails unten an. Die Bezahlung wird über " | ||||
| "<a href=\"https://stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. " | ||||
| "Wir speichern Deine Kreditkartendetails nicht in unserer Datenbank." | ||||
| "Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://" | ||||
| "stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine " | ||||
| "Kreditkartendetails nicht in unserer Datenbank." | ||||
| 
 | ||||
| msgid "" | ||||
| "Please fill in your credit card information below. We are using <a href=" | ||||
|  | @ -395,12 +398,35 @@ msgstr "Bestellungsübersicht" | |||
| msgid "Product" | ||||
| msgstr "Produkt" | ||||
| 
 | ||||
| msgid "Amount" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Description" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Recurring" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Subtotal" | ||||
| msgstr "Zwischensumme" | ||||
| 
 | ||||
| msgid "VAT" | ||||
| msgstr "Mehrwertsteuer" | ||||
| 
 | ||||
| msgid "" | ||||
| "By clicking \"Place order\" this plan will charge your credit card account " | ||||
| "with %(total_price)s CHF/month" | ||||
| msgstr "" | ||||
| "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " | ||||
| "%(vm_total_price)s CHF pro Monat belastet" | ||||
| 
 | ||||
| msgid "" | ||||
| "By clicking \"Place order\" this payment will charge your credit card " | ||||
| "account with a one time amount of %(total_price)s CHF" | ||||
| msgstr "" | ||||
| "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit " | ||||
| "%(vm_total_price)s CHF pro Monat belastet" | ||||
| 
 | ||||
| #, python-format | ||||
| msgid "" | ||||
| "By clicking \"Place order\" this plan will charge your credit card account " | ||||
|  | @ -541,8 +567,33 @@ msgstr "" | |||
| 
 | ||||
| #, python-brace-format | ||||
| msgid "An error occurred while associating the card. Details: {details}" | ||||
| msgstr "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: " | ||||
| "{details}" | ||||
| msgstr "" | ||||
| "Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}" | ||||
| 
 | ||||
| msgid "Confirmation of your payment" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid " This is a monthly recurring plan." | ||||
| msgstr "" | ||||
| 
 | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Hi {name},\n" | ||||
| "\n" | ||||
| "thank you for your order!\n" | ||||
| "We have just received a payment of CHF {amount:.2f} from you.{recurring}\n" | ||||
| "\n" | ||||
| "Cheers,\n" | ||||
| "Your Data Center Light team" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Thank you for the payment." | ||||
| msgstr "Danke für Deine Bestellung." | ||||
| 
 | ||||
| msgid "" | ||||
| "You will soon receive a confirmation email of the payment. You can always " | ||||
| "contact us at info@ungleich.ch for any question that you may have." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Thank you for the order." | ||||
| msgstr "Danke für Deine Bestellung." | ||||
|  |  | |||
|  | @ -179,4 +179,10 @@ footer .dcl-link-separator::before { | |||
| .new-card-button-margin button{ | ||||
|     margin-top: 5px; | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
| } | ||||
| 
 | ||||
| .input-no-border { | ||||
|   border: none !important; | ||||
|   background: transparent !important; | ||||
|   resize: none; | ||||
| } | ||||
|  |  | |||
|  | @ -67,36 +67,49 @@ | |||
|             </div> | ||||
|             <div class="dcl-payment-box"> | ||||
|                 <div class="dcl-payment-section"> | ||||
|                     <h3>{%trans "Your Order" %}</h3> | ||||
|                     <hr class="top-hr"> | ||||
|                     <div class="dcl-payment-order"> | ||||
|                         <p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p> | ||||
|                         <hr> | ||||
|                         <p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p> | ||||
|                         <hr> | ||||
|                         <p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p> | ||||
|                         <hr> | ||||
|                         <p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p> | ||||
|                         <hr> | ||||
|                         <p> | ||||
|                             <strong>{%trans "Total" %}</strong>   | ||||
|                             <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> | ||||
|                         <hr> | ||||
|                         {% if vm_pricing.discount_amount %} | ||||
|                         <p class="mb-0"> | ||||
|                             {%trans "Discount" as discount_name %} | ||||
|                             <strong>{{ vm_pricing.discount_name|default:discount_name }}</strong>   | ||||
|                             <strong class="pull-right text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</strong> | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             ({% trans "Will be applied at checkout" %}) | ||||
|                         </p> | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                     {% if generic_payment_form %} | ||||
|                         <h3>{%trans "Make a payment" %}</h3> | ||||
|                         <hr class="top-hr"> | ||||
|                         <form role="form" id="generic-payment-form" method="post" action="" novalidate> | ||||
|                             {% csrf_token %} | ||||
|                             <input type="hidden" name="product" value="1" /> | ||||
|                             {% for field in generic_payment_form %} | ||||
|                             {% bootstrap_field field type='fields'%} | ||||
|                             {% endfor %} | ||||
|                             <p class="text-danger">{{generic_payment_form.non_field_errors|striptags}}</p> | ||||
|                         </form> | ||||
|                     {% else %} | ||||
|                         <h3>{%trans "Your Order" %}</h3> | ||||
|                         <hr class="top-hr"> | ||||
|                         <div class="dcl-payment-order"> | ||||
|                             <p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p> | ||||
|                             <hr> | ||||
|                             <p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p> | ||||
|                             <hr> | ||||
|                             <p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p> | ||||
|                             <hr> | ||||
|                             <p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p> | ||||
|                             <hr> | ||||
|                             <p> | ||||
|                                 <strong>{%trans "Total" %}</strong>   | ||||
|                                 <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> | ||||
|                             <hr> | ||||
|                             {% if vm_pricing.discount_amount %} | ||||
|                             <p class="mb-0"> | ||||
|                                 {%trans "Discount" as discount_name %} | ||||
|                                 <strong>{{ vm_pricing.discount_name|default:discount_name }}</strong>   | ||||
|                                 <strong class="pull-right text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</strong> | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 ({% trans "Will be applied at checkout" %}) | ||||
|                             </p> | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="dcl-payment-box"> | ||||
|  |  | |||
|  | @ -47,61 +47,88 @@ | |||
|             <hr> | ||||
|             <div> | ||||
|                 <h4>{% trans "Order summary" %}</h4> | ||||
|                 <p> | ||||
|                     <strong>{% trans "Product" %}:</strong>  | ||||
|                     {{ request.session.template.name }} | ||||
|                 </p> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-sm-6"> | ||||
|                     {% if generic_payment_details %} | ||||
|                         <p> | ||||
|                             <span>{% trans "Cores" %}: </span> | ||||
|                             <strong class="pull-right">{{vm.cpu|floatformat}}</strong> | ||||
|                             <strong>{% trans "Product" %}:</strong>  | ||||
|                             {{ generic_payment_details.product_name }} | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             <span>{% trans "Memory" %}: </span> | ||||
|                             <strong class="pull-right">{{vm.memory|intcomma}} GB</strong> | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             <span>{% trans "Disk space" %}: </span> | ||||
|                             <strong class="pull-right">{{vm.disk_size|intcomma}} GB</strong> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div class="col-sm-12"> | ||||
|                         <hr class="thin-hr"> | ||||
|                     </div> | ||||
|                     {% if vm.vat > 0 or vm.discount.amount > 0 %} | ||||
|                         <div class="col-sm-6"> | ||||
|                             <div class="subtotal-price"> | ||||
|                                 {% if vm.vat > 0 %} | ||||
|                         <div class="row"> | ||||
|                             <div class="col-sm-6"> | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Amount" %}: </span> | ||||
|                                     <strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong> | ||||
|                                 </p> | ||||
|                                 {% if generic_payment_details.description %} | ||||
|                                     <p> | ||||
|                                         <strong class="text-lg">{% trans "Subtotal" %} </strong> | ||||
|                                         <strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> | ||||
|                                     </p> | ||||
|                                     <p> | ||||
|                                         <small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small> | ||||
|                                         <strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong> | ||||
|                                         <span>{% trans "Description" %}: </span> | ||||
|                                         <strong class="pull-right">{{generic_payment_details.description}}</strong> | ||||
|                                     </p> | ||||
|                                 {% endif %} | ||||
|                                 {% if vm.discount.amount > 0 %} | ||||
|                                     <p class="text-primary"> | ||||
|                                         {%trans "Discount" as discount_name %} | ||||
|                                         <strong>{{ vm.discount.name|default:discount_name }} </strong> | ||||
|                                         <strong class="pull-right">- {{ vm.discount.amount }} CHF</strong> | ||||
|                                 {% if generic_payment_details.recurring %} | ||||
|                                     <p> | ||||
|                                         <span>{% trans "Recurring" %}: </span> | ||||
|                                         <strong class="pull-right">Yes</strong> | ||||
|                                     </p> | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12"> | ||||
|                             <hr class="thin-hr"> | ||||
|                     {% else %} | ||||
|                         <p> | ||||
|                             <strong>{% trans "Product" %}:</strong>  | ||||
|                             {{ request.session.template.name }} | ||||
|                         </p> | ||||
|                         <div class="row"> | ||||
|                             <div class="col-sm-6"> | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Cores" %}: </span> | ||||
|                                     <strong class="pull-right">{{vm.cpu|floatformat}}</strong> | ||||
|                                 </p> | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Memory" %}: </span> | ||||
|                                     <strong class="pull-right">{{vm.memory|intcomma}} GB</strong> | ||||
|                                 </p> | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Disk space" %}: </span> | ||||
|                                     <strong class="pull-right">{{vm.disk_size|intcomma}} GB</strong> | ||||
|                                 </p> | ||||
|                             </div> | ||||
|                             <div class="col-sm-12"> | ||||
|                                 <hr class="thin-hr"> | ||||
|                             </div> | ||||
|                             {% if vm.vat > 0 or vm.discount.amount > 0 %} | ||||
|                                 <div class="col-sm-6"> | ||||
|                                     <div class="subtotal-price"> | ||||
|                                         {% if vm.vat > 0 %} | ||||
|                                             <p> | ||||
|                                                 <strong class="text-lg">{% trans "Subtotal" %} </strong> | ||||
|                                                 <strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> | ||||
|                                             </p> | ||||
|                                             <p> | ||||
|                                                 <small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small> | ||||
|                                                 <strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong> | ||||
|                                             </p> | ||||
|                                         {% endif %} | ||||
|                                         {% if vm.discount.amount > 0 %} | ||||
|                                             <p class="text-primary"> | ||||
|                                                 {%trans "Discount" as discount_name %} | ||||
|                                                 <strong>{{ vm.discount.name|default:discount_name }} </strong> | ||||
|                                                 <strong class="pull-right">- {{ vm.discount.amount }} CHF</strong> | ||||
|                                             </p> | ||||
|                                         {% endif %} | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                                 <div class="col-sm-12"> | ||||
|                                     <hr class="thin-hr"> | ||||
|                                 </div> | ||||
|                             {% endif %} | ||||
|                             <div class="col-sm-6"> | ||||
|                                 <p class="total-price"> | ||||
|                                     <strong>{% trans "Total" %} </strong> | ||||
|                                     <strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong> | ||||
|                                 </p> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     <div class="col-sm-6"> | ||||
|                         <p class="total-price"> | ||||
|                             <strong>{% trans "Total" %} </strong> | ||||
|                             <strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <hr class="thin-hr"> | ||||
|         </div> | ||||
|  | @ -109,7 +136,15 @@ | |||
|             {% csrf_token %} | ||||
|             <div class="row"> | ||||
|                 <div class="col-sm-8"> | ||||
|                     <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 {{vm_total_price}} CHF/month{% endblocktrans %}.</div> | ||||
|                     {% if generic_payment_details %} | ||||
|                         {% if generic_payment_details.recurring %} | ||||
|                             <div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div> | ||||
|                         {% else %} | ||||
|                             <div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div> | ||||
|                         {% endif %} | ||||
|                     {% else %} | ||||
|                         <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 {{vm_total_price}} CHF/month{% endblocktrans %}.</div> | ||||
|                     {% endif %} | ||||
|                 </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"> | ||||
|  | @ -151,16 +186,5 @@ | |||
| <script type="text/javascript"> | ||||
|     {% trans "Some problem encountered. Please try again later." as err_msg %} | ||||
|     var create_vm_error_message = '{{err_msg|safe}}'; | ||||
|     window.onload = function () { | ||||
|         var locale_dates = document.getElementsByClassName("locale_date"); | ||||
|         var formats = ['YYYY-MM-DD hh:mm a'] | ||||
|         var i; | ||||
|         for (i = 0; i < locale_dates.length; i++) { | ||||
|             var oldDate = moment.utc(locale_dates[i].textContent, formats); | ||||
|             var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f; | ||||
|             locale_dates[i].innerHTML = oldDate.local().format(outputFormat); | ||||
|             locale_dates[i].className += ' done'; | ||||
|         } | ||||
|     }; | ||||
| </script> | ||||
| {%endblock%} | ||||
|  | @ -89,8 +89,14 @@ def create_vm(billing_address_data, stripe_customer_id, specs, | |||
| 
 | ||||
|     create_vm_task.delay(vm_template_id, user, specs, template, order.id) | ||||
| 
 | ||||
|     for session_var in ['specs', 'template', 'billing_address', | ||||
|                         'billing_address_data', 'card_id', | ||||
|                         'token', 'customer']: | ||||
|         if session_var in request.session: | ||||
|             del request.session[session_var] | ||||
|     clear_all_session_vars(request) | ||||
| 
 | ||||
| 
 | ||||
| def clear_all_session_vars(request): | ||||
|     if request.session is not None: | ||||
|         for session_var in ['specs', 'template', 'billing_address', | ||||
|                             'billing_address_data', 'card_id', | ||||
|                             'token', 'customer', 'generic_payment_type', | ||||
|                             'generic_payment_details', 'product_id']: | ||||
|             if session_var in request.session: | ||||
|                 del request.session[session_var] | ||||
|  |  | |||
|  | @ -6,24 +6,31 @@ from django.contrib import messages | |||
| from django.contrib.auth import login, authenticate | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.core.urlresolvers import reverse | ||||
| from django.http import HttpResponseRedirect, JsonResponse | ||||
| from django.http import HttpResponseRedirect, JsonResponse, Http404 | ||||
| from django.shortcuts import render | ||||
| from django.utils.translation import get_language, ugettext_lazy as _ | ||||
| from django.views.decorators.cache import cache_control | ||||
| from django.views.generic import FormView, CreateView, DetailView | ||||
| 
 | ||||
| from hosting.forms import HostingUserLoginForm | ||||
| from hosting.models import HostingOrder, UserCardDetail | ||||
| from hosting.forms import ( | ||||
|     HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm | ||||
| ) | ||||
| from hosting.models import ( | ||||
|     HostingBill, HostingOrder, UserCardDetail, GenericProduct | ||||
| ) | ||||
| from membership.models import CustomUser, StripeCustomer | ||||
| from opennebula_api.serializers import VMTemplateSerializer | ||||
| from utils.forms import BillingAddressForm, BillingAddressFormSignup | ||||
| from utils.forms import ( | ||||
|     BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm, | ||||
|     BillingAddress | ||||
| ) | ||||
| 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 .cms_models import DCLCalculatorPluginModel | ||||
| from .forms import ContactForm | ||||
| from .models import VMTemplate, VMPricing | ||||
| from .utils import get_cms_integration, create_vm | ||||
| from .utils import get_cms_integration, create_vm, clear_all_session_vars | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -114,10 +121,7 @@ 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', | ||||
|                             'pricing_name']: | ||||
|             if session_var in request.session: | ||||
|                 del request.session[session_var] | ||||
|         clear_all_session_vars(request) | ||||
|         return HttpResponseRedirect(reverse('datacenterlight:cms_index')) | ||||
| 
 | ||||
|     def post(self, request): | ||||
|  | @ -265,19 +269,93 @@ class PaymentOrderView(FormView): | |||
|             'login_form': HostingUserLoginForm(prefix='login_form'), | ||||
|             'billing_address_form': billing_address_form, | ||||
|             'cms_integration': get_cms_integration('default'), | ||||
|             'vm_pricing': VMPricing.get_vm_pricing_by_name( | ||||
|                 self.request.session['specs']['pricing_name'] | ||||
|             ) | ||||
|         }) | ||||
| 
 | ||||
|         if ('generic_payment_type' in self.request.session and | ||||
|                 self.request.session['generic_payment_type'] == 'generic'): | ||||
|             if 'product_id' in self.request.session: | ||||
|                 product = GenericProduct.objects.get( | ||||
|                     id=self.request.session['product_id'] | ||||
|                 ) | ||||
|                 context.update({'generic_payment_form': ProductPaymentForm( | ||||
|                     prefix='generic_payment_form', | ||||
|                     initial={'product_name': product.product_name, | ||||
|                              'amount': float(product.get_actual_price()), | ||||
|                              'recurring': product.product_is_subscription, | ||||
|                              'description': product.product_description, | ||||
|                              }, | ||||
|                     product_id=product.id | ||||
|                 ), }) | ||||
|             else: | ||||
|                 context.update({'generic_payment_form': GenericPaymentForm( | ||||
|                     prefix='generic_payment_form', | ||||
|                 ), }) | ||||
|         else: | ||||
|             context.update({ | ||||
|                 'vm_pricing': VMPricing.get_vm_pricing_by_name( | ||||
|                     self.request.session['specs']['pricing_name'] | ||||
|                 ) | ||||
|             }) | ||||
| 
 | ||||
|         return context | ||||
| 
 | ||||
|     @cache_control(no_cache=True, must_revalidate=True, no_store=True) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         if 'specs' not in request.session: | ||||
|         if (('type' in request.GET and request.GET['type'] == 'generic') | ||||
|                 or 'product_slug' in kwargs): | ||||
|             request.session['generic_payment_type'] = 'generic' | ||||
|             if 'generic_payment_details' in request.session: | ||||
|                 request.session.pop('generic_payment_details') | ||||
|                 request.session.pop('product_id') | ||||
|             if 'product_slug' in kwargs: | ||||
|                 logger.debug("Product slug is " + kwargs['product_slug']) | ||||
|                 try: | ||||
|                     product = GenericProduct.objects.get( | ||||
|                         product_slug=kwargs['product_slug'] | ||||
|                     ) | ||||
|                 except GenericProduct.DoesNotExist as dne: | ||||
|                     logger.error( | ||||
|                         "Product '{}' does " | ||||
|                         "not exist".format(kwargs['product_slug']) | ||||
|                     ) | ||||
|                     raise Http404() | ||||
|                 request.session['product_id'] = product.id | ||||
|         elif 'specs' not in request.session: | ||||
|             return HttpResponseRedirect(reverse('datacenterlight:index')) | ||||
|         return self.render_to_response(self.get_context_data()) | ||||
| 
 | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         if 'product' in request.POST: | ||||
|             # query for the supplied product | ||||
|             product = None | ||||
|             try: | ||||
|                 product = GenericProduct.objects.get( | ||||
|                     id=request.POST['generic_payment_form-product_name'] | ||||
|                 ) | ||||
|             except GenericProduct.DoesNotExist as dne: | ||||
|                 logger.error( | ||||
|                     "The requested product '{}' does not exist".format( | ||||
|                         request.POST['generic_payment_form-product_name'] | ||||
|                     ) | ||||
|                 ) | ||||
|             except GenericProduct.MultipleObjectsReturned as mpe: | ||||
|                 logger.error( | ||||
|                     "There seem to be more than one product with " | ||||
|                     "the name {}".format( | ||||
|                         request.POST['generic_payment_form-product_name'] | ||||
|                     ) | ||||
|                 ) | ||||
|                 product = GenericProduct.objects.all( | ||||
|                     product_name=request. | ||||
|                     POST['generic_payment_form-product_name'] | ||||
|                 ).first() | ||||
|             if product is None: | ||||
|                 return JsonResponse({}) | ||||
|             else: | ||||
|                 return JsonResponse({ | ||||
|                     'amount': product.get_actual_price(), | ||||
|                     'isSubscription': product.product_is_subscription | ||||
|                 }) | ||||
|         if 'login_form' in request.POST: | ||||
|             login_form = HostingUserLoginForm( | ||||
|                 data=request.POST, prefix='login_form' | ||||
|  | @ -288,6 +366,13 @@ class PaymentOrderView(FormView): | |||
|                 auth_user = authenticate(email=email, password=password) | ||||
|                 if auth_user: | ||||
|                     login(self.request, auth_user) | ||||
|                     if 'product_slug' in kwargs: | ||||
|                         return HttpResponseRedirect( | ||||
|                             reverse('show_product', | ||||
|                                     kwargs={ | ||||
|                                         'product_slug': kwargs['product_slug']} | ||||
|                                     ) | ||||
|                         ) | ||||
|                     return HttpResponseRedirect( | ||||
|                         reverse('datacenterlight:payment') | ||||
|                     ) | ||||
|  | @ -304,6 +389,50 @@ class PaymentOrderView(FormView): | |||
|                 data=request.POST, | ||||
|             ) | ||||
|         if address_form.is_valid(): | ||||
|             # Check if we are in a generic payment case and handle the generic | ||||
|             # payment details form before we go on to verify payment | ||||
|             if ('generic_payment_type' in request.session and | ||||
|                     self.request.session['generic_payment_type'] == 'generic'): | ||||
|                 if 'product_id' in request.session: | ||||
|                     generic_payment_form = ProductPaymentForm( | ||||
|                         data=request.POST, prefix='generic_payment_form', | ||||
|                         product_id=request.session['product_id'] | ||||
|                     ) | ||||
|                 else: | ||||
|                     generic_payment_form = GenericPaymentForm( | ||||
|                         data=request.POST, prefix='generic_payment_form' | ||||
|                     ) | ||||
|                 if generic_payment_form.is_valid(): | ||||
|                     logger.debug("Generic payment form is valid.") | ||||
|                     if 'product_id' in request.session: | ||||
|                         product = generic_payment_form.product | ||||
|                     else: | ||||
|                         product = generic_payment_form.cleaned_data.get( | ||||
|                             'product_name' | ||||
|                         ) | ||||
|                     gp_details = { | ||||
|                         "product_name": product.product_name, | ||||
|                         "amount": generic_payment_form.cleaned_data.get( | ||||
|                             'amount' | ||||
|                         ), | ||||
|                         "recurring": generic_payment_form.cleaned_data.get( | ||||
|                             'recurring' | ||||
|                         ), | ||||
|                         "description": generic_payment_form.cleaned_data.get( | ||||
|                             'description' | ||||
|                         ), | ||||
|                         "product_id": product.id, | ||||
|                         "product_slug": product.product_slug | ||||
|                     } | ||||
|                     request.session["generic_payment_details"] = ( | ||||
|                         gp_details | ||||
|                     ) | ||||
|                 else: | ||||
|                     logger.debug("Generic payment form invalid") | ||||
|                     context = self.get_context_data() | ||||
|                     context['generic_payment_form'] = generic_payment_form | ||||
|                     context['billing_address_form'] = address_form | ||||
|                     return self.render_to_response(context) | ||||
|             token = address_form.cleaned_data.get('token') | ||||
|             if token is '': | ||||
|                 card_id = address_form.cleaned_data.get('card') | ||||
|  | @ -319,8 +448,8 @@ class PaymentOrderView(FormView): | |||
|                 except UserCardDetail.DoesNotExist as e: | ||||
|                     ex = str(e) | ||||
|                     logger.error("Card Id: {card_id}, Exception: {ex}".format( | ||||
|                             card_id=card_id, ex=ex | ||||
|                         ) | ||||
|                         card_id=card_id, ex=ex | ||||
|                     ) | ||||
|                     ) | ||||
|                     msg = _("An error occurred. Details: {}".format(ex)) | ||||
|                     messages.add_message( | ||||
|  | @ -409,7 +538,8 @@ class OrderConfirmationView(DetailView): | |||
|     @cache_control(no_cache=True, must_revalidate=True, no_store=True) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         context = {} | ||||
|         if 'specs' not in request.session or 'user' not in request.session: | ||||
|         if (('specs' not in request.session or 'user' not in request.session) | ||||
|                 and 'generic_payment_type' not in request.session): | ||||
|             return HttpResponseRedirect(reverse('datacenterlight:index')) | ||||
|         if 'token' in self.request.session: | ||||
|             token = self.request.session['token'] | ||||
|  | @ -427,9 +557,19 @@ class OrderConfirmationView(DetailView): | |||
|             card_detail = UserCardDetail.objects.get(id=card_id) | ||||
|             context['cc_last4'] = card_detail.last4 | ||||
|             context['cc_brand'] = card_detail.brand | ||||
| 
 | ||||
|         if ('generic_payment_type' in request.session and | ||||
|                 self.request.session['generic_payment_type'] == 'generic'): | ||||
|             context.update({ | ||||
|                 'generic_payment_details': | ||||
|                     request.session['generic_payment_details'], | ||||
|             }) | ||||
|         else: | ||||
|             context.update({ | ||||
|                 'vm': request.session.get('specs'), | ||||
|             }) | ||||
|         context.update({ | ||||
|             'site_url': reverse('datacenterlight:index'), | ||||
|             'vm': request.session.get('specs'), | ||||
|             'page_header_text': _('Confirm Order'), | ||||
|             'billing_address_data': ( | ||||
|                 request.session.get('billing_address_data') | ||||
|  | @ -439,11 +579,8 @@ class OrderConfirmationView(DetailView): | |||
|         return render(request, self.template_name, context) | ||||
| 
 | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         template = request.session.get('template') | ||||
|         specs = request.session.get('specs') | ||||
|         user = request.session.get('user') | ||||
|         stripe_api_cus_id = request.session.get('customer') | ||||
|         vm_template_id = template.get('id', 1) | ||||
|         stripe_utils = StripeUtils() | ||||
| 
 | ||||
|         if 'token' in request.session: | ||||
|  | @ -457,7 +594,14 @@ class OrderConfirmationView(DetailView): | |||
|                 response = { | ||||
|                     'status': False, | ||||
|                     'redirect': "{url}#{section}".format( | ||||
|                         url=reverse('datacenterlight:payment'), | ||||
|                         url=(reverse( | ||||
|                             'show_product', | ||||
|                             kwargs={'product_slug': | ||||
|                                     request.session['generic_payment_details'] | ||||
|                                     ['product_slug']} | ||||
|                         ) if 'generic_payment_details' in request.session else | ||||
|                                 reverse('datacenterlight:payment') | ||||
|                         ), | ||||
|                         section='payment_error'), | ||||
|                     'msg_title': str(_('Error.')), | ||||
|                     'msg_body': str( | ||||
|  | @ -473,7 +617,8 @@ class OrderConfirmationView(DetailView): | |||
|                 'brand': card_details_response['brand'], | ||||
|                 'card_id': card_details_response['card_id'] | ||||
|             } | ||||
|             stripe_customer_obj = StripeCustomer.objects.filter(stripe_id=stripe_api_cus_id).first() | ||||
|             stripe_customer_obj = StripeCustomer.objects.filter( | ||||
|                 stripe_id=stripe_api_cus_id).first() | ||||
|             if stripe_customer_obj: | ||||
|                 ucd = UserCardDetail.get_user_card_details( | ||||
|                     stripe_customer_obj, card_details_response | ||||
|  | @ -495,7 +640,16 @@ class OrderConfirmationView(DetailView): | |||
|                         response = { | ||||
|                             'status': False, | ||||
|                             'redirect': "{url}#{section}".format( | ||||
|                                 url=reverse('hosting:payment'), | ||||
|                                 url=(reverse( | ||||
|                                     'show_product', | ||||
|                                     kwargs={'product_slug': | ||||
|                                             request.session | ||||
|                                             ['generic_payment_details'] | ||||
|                                             ['product_slug']} | ||||
|                                     ) if 'generic_payment_details' in | ||||
|                                      request.session else | ||||
|                                      reverse('datacenterlight:payment') | ||||
|                                 ), | ||||
|                                 section='payment_error'), | ||||
|                             'msg_title': str(_('Error.')), | ||||
|                             'msg_body': str( | ||||
|  | @ -527,57 +681,122 @@ class OrderConfirmationView(DetailView): | |||
|             } | ||||
|             return JsonResponse(response) | ||||
| 
 | ||||
|         cpu = specs.get('cpu') | ||||
|         memory = specs.get('memory') | ||||
|         disk_size = specs.get('disk_size') | ||||
|         amount_to_be_charged = specs.get('total_price') | ||||
|         plan_name = StripeUtils.get_stripe_plan_name( | ||||
|             cpu=cpu, | ||||
|             memory=memory, | ||||
|             disk_size=disk_size, | ||||
|             price=amount_to_be_charged | ||||
|         ) | ||||
|         stripe_plan_id = StripeUtils.get_stripe_plan_id( | ||||
|             cpu=cpu, | ||||
|             ram=memory, | ||||
|             ssd=disk_size, | ||||
|             version=1, | ||||
|             app='dcl', | ||||
|             price=amount_to_be_charged | ||||
|         ) | ||||
|         stripe_plan = stripe_utils.get_or_create_stripe_plan( | ||||
|             amount=amount_to_be_charged, | ||||
|             name=plan_name, | ||||
|             stripe_plan_id=stripe_plan_id) | ||||
|         subscription_result = stripe_utils.subscribe_customer_to_plan( | ||||
|             stripe_api_cus_id, | ||||
|             [{"plan": stripe_plan.get( | ||||
|                 'response_object').stripe_plan_id}]) | ||||
|         stripe_subscription_obj = subscription_result.get('response_object') | ||||
|         # Check if the subscription was approved and is active | ||||
|         if (stripe_subscription_obj is None | ||||
|                 or stripe_subscription_obj.status != 'active'): | ||||
|             # At this point, we have created a Stripe API card and | ||||
|             # associated it with the customer; but the transaction failed | ||||
|             # due to some reason. So, we would want to dissociate this card | ||||
|             # here. | ||||
|             # ... | ||||
|         if ('generic_payment_type' in request.session and | ||||
|                 self.request.session['generic_payment_type'] == 'generic'): | ||||
|             gp_details = self.request.session['generic_payment_details'] | ||||
|             if gp_details['recurring']: | ||||
|                 # generic recurring payment | ||||
|                 logger.debug("Commencing a generic recurring payment") | ||||
|             else: | ||||
|                 # generic one time payment | ||||
|                 logger.debug("Commencing a one time payment") | ||||
|                 charge_response = stripe_utils.make_charge( | ||||
|                     amount=gp_details['amount'], | ||||
|                     customer=stripe_api_cus_id | ||||
|                 ) | ||||
|                 stripe_onetime_charge = charge_response.get('response_object') | ||||
| 
 | ||||
|             msg = subscription_result.get('error') | ||||
|             messages.add_message(self.request, messages.ERROR, msg, | ||||
|                                  extra_tags='failed_payment') | ||||
|             response = { | ||||
|                 'status': False, | ||||
|                 'redirect': "{url}#{section}".format( | ||||
|                     url=reverse('datacenterlight:payment'), | ||||
|                     section='payment_error'), | ||||
|                 'msg_title': str(_('Error.')), | ||||
|                 'msg_body': str( | ||||
|                     _('There was a payment related error.' | ||||
|                       ' On close of this popup, you will be redirected back to' | ||||
|                       ' the payment page.')) | ||||
|             } | ||||
|             return JsonResponse(response) | ||||
|                 # Check if the payment was approved | ||||
|                 if not stripe_onetime_charge: | ||||
|                     msg = charge_response.get('error') | ||||
|                     messages.add_message(self.request, messages.ERROR, msg, | ||||
|                                          extra_tags='failed_payment') | ||||
|                     response = { | ||||
|                         'status': False, | ||||
|                         'redirect': "{url}#{section}".format( | ||||
|                             url=(reverse('show_product', kwargs={ | ||||
|                                 'product_slug': gp_details['product_slug']} | ||||
|                                          ) if 'generic_payment_details' in | ||||
|                                               request.session else | ||||
|                                  reverse('datacenterlight:payment') | ||||
|                                  ), | ||||
|                             section='payment_error'), | ||||
|                         'msg_title': str(_('Error.')), | ||||
|                         'msg_body': str( | ||||
|                             _('There was a payment related error.' | ||||
|                               ' On close of this popup, you will be redirected' | ||||
|                               ' back to the payment page.')) | ||||
|                     } | ||||
|                     return JsonResponse(response) | ||||
| 
 | ||||
|         if ('generic_payment_type' not in request.session or | ||||
|                 (request.session['generic_payment_details']['recurring'])): | ||||
|             if 'generic_payment_details' in request.session: | ||||
|                 amount_to_be_charged = ( | ||||
|                     round( | ||||
|                         request.session['generic_payment_details']['amount'], | ||||
|                         2 | ||||
|                     ) | ||||
|                 ) | ||||
|                 plan_name = "generic-{0}-{1:.2f}".format( | ||||
|                     request.session['generic_payment_details']['product_id'], | ||||
|                     amount_to_be_charged | ||||
|                 ) | ||||
|                 stripe_plan_id = plan_name | ||||
|             else: | ||||
|                 template = request.session.get('template') | ||||
|                 specs = request.session.get('specs') | ||||
|                 vm_template_id = template.get('id', 1) | ||||
| 
 | ||||
|                 cpu = specs.get('cpu') | ||||
|                 memory = specs.get('memory') | ||||
|                 disk_size = specs.get('disk_size') | ||||
|                 amount_to_be_charged = specs.get('total_price') | ||||
|                 plan_name = StripeUtils.get_stripe_plan_name( | ||||
|                     cpu=cpu, | ||||
|                     memory=memory, | ||||
|                     disk_size=disk_size, | ||||
|                     price=amount_to_be_charged | ||||
|                 ) | ||||
|                 stripe_plan_id = StripeUtils.get_stripe_plan_id( | ||||
|                     cpu=cpu, | ||||
|                     ram=memory, | ||||
|                     ssd=disk_size, | ||||
|                     version=1, | ||||
|                     app='dcl', | ||||
|                     price=amount_to_be_charged | ||||
|                 ) | ||||
|             stripe_plan = stripe_utils.get_or_create_stripe_plan( | ||||
|                 amount=amount_to_be_charged, | ||||
|                 name=plan_name, | ||||
|                 stripe_plan_id=stripe_plan_id) | ||||
|             subscription_result = stripe_utils.subscribe_customer_to_plan( | ||||
|                 stripe_api_cus_id, | ||||
|                 [{"plan": stripe_plan.get( | ||||
|                     'response_object').stripe_plan_id}]) | ||||
|             stripe_subscription_obj = subscription_result.get('response_object') | ||||
|             # Check if the subscription was approved and is active | ||||
|             if (stripe_subscription_obj is None | ||||
|                     or stripe_subscription_obj.status != 'active'): | ||||
|                 # At this point, we have created a Stripe API card and | ||||
|                 # associated it with the customer; but the transaction failed | ||||
|                 # due to some reason. So, we would want to dissociate this card | ||||
|                 # here. | ||||
|                 # ... | ||||
| 
 | ||||
|                 msg = subscription_result.get('error') | ||||
|                 messages.add_message(self.request, messages.ERROR, msg, | ||||
|                                      extra_tags='failed_payment') | ||||
|                 response = { | ||||
|                     'status': False, | ||||
|                     'redirect': "{url}#{section}".format( | ||||
|                         url=(reverse( | ||||
|                             'show_product', | ||||
|                             kwargs={'product_slug': | ||||
|                                     request.session['generic_payment_details'] | ||||
|                                     ['product_slug']} | ||||
|                         ) if 'generic_payment_details' in request.session else | ||||
|                                 reverse('datacenterlight:payment') | ||||
|                         ), | ||||
|                         section='payment_error' | ||||
|                     ), | ||||
|                     'msg_title': str(_('Error.')), | ||||
|                     'msg_body': str( | ||||
|                         _('There was a payment related error.' | ||||
|                           ' On close of this popup, you will be redirected back to' | ||||
|                           ' the payment page.')) | ||||
|                 } | ||||
|                 return JsonResponse(response) | ||||
| 
 | ||||
|         # Create user if the user is not logged in and if he is not already | ||||
|         # registered | ||||
|  | @ -647,6 +866,118 @@ class OrderConfirmationView(DetailView): | |||
|             'user': custom_user.id | ||||
|         }) | ||||
| 
 | ||||
|         if 'generic_payment_type' in request.session: | ||||
|             stripe_cus = StripeCustomer.objects.filter( | ||||
|                 stripe_id=stripe_api_cus_id | ||||
|             ).first() | ||||
|             billing_address = BillingAddress( | ||||
|                 cardholder_name=billing_address_data['cardholder_name'], | ||||
|                 street_address=billing_address_data['street_address'], | ||||
|                 city=billing_address_data['city'], | ||||
|                 postal_code=billing_address_data['postal_code'], | ||||
|                 country=billing_address_data['country'] | ||||
|             ) | ||||
|             billing_address.save() | ||||
| 
 | ||||
|             order = HostingOrder.create( | ||||
|                 price=self.request | ||||
|                           .session['generic_payment_details']['amount'], | ||||
|                 customer=stripe_cus, | ||||
|                 billing_address=billing_address, | ||||
|                 vm_pricing=VMPricing.get_default_pricing() | ||||
|             ) | ||||
| 
 | ||||
|             # Create a Hosting Bill | ||||
|             HostingBill.create(customer=stripe_cus, | ||||
|                                billing_address=billing_address) | ||||
| 
 | ||||
|             # Create Billing Address for User if he does not have one | ||||
|             if not stripe_cus.user.billing_addresses.count(): | ||||
|                 billing_address_data.update({ | ||||
|                     'user': stripe_cus.user.id | ||||
|                 }) | ||||
|                 billing_address_user_form = UserBillingAddressForm( | ||||
|                     billing_address_data | ||||
|                 ) | ||||
|                 billing_address_user_form.is_valid() | ||||
|                 billing_address_user_form.save() | ||||
| 
 | ||||
|             if self.request.session['generic_payment_details']['recurring']: | ||||
|                 # Associate the given stripe subscription with the order | ||||
|                 order.set_subscription_id( | ||||
|                     stripe_subscription_obj.id, card_details_dict | ||||
|                 ) | ||||
|             else: | ||||
|                 # Associate the given stripe charge id with the order | ||||
|                 order.set_stripe_charge(stripe_onetime_charge) | ||||
| 
 | ||||
|             # Set order status approved | ||||
|             order.set_approved() | ||||
|             order.generic_payment_description = gp_details["description"] | ||||
|             order.generic_product_id = gp_details["product_id"] | ||||
|             order.save() | ||||
|             # send emails | ||||
|             context = { | ||||
|                 'name': user.get('name'), | ||||
|                 'email': user.get('email'), | ||||
|                 'amount': gp_details['amount'], | ||||
|                 'description': gp_details['description'], | ||||
|                 'recurring': gp_details['recurring'], | ||||
|                 'product_name': gp_details['product_name'], | ||||
|                 'product_id': gp_details['product_id'], | ||||
|                 'order_id': order.id | ||||
|             } | ||||
| 
 | ||||
|             email_data = { | ||||
|                 'subject': (settings.DCL_TEXT + | ||||
|                             " Payment received from %s" % context['email']), | ||||
|                 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, | ||||
|                 'to': ['info@ungleich.ch'], | ||||
|                 'body': "\n".join( | ||||
|                     ["%s=%s" % (k, v) for (k, v) in context.items()]), | ||||
|                 'reply_to': [context['email']], | ||||
|             } | ||||
|             send_plain_email_task.delay(email_data) | ||||
| 
 | ||||
|             email_data = { | ||||
|                 'subject': _("Confirmation of your payment"), | ||||
|                 'from_email': settings.DCL_SUPPORT_FROM_ADDRESS, | ||||
|                 'to': [user.get('email')], | ||||
|                 'body': _("Hi {name},\n\n" | ||||
|                           "thank you for your order!\n" | ||||
|                           "We have just received a payment of CHF {amount:.2f}" | ||||
|                           " from you.{recurring}\n\n" | ||||
|                           "Cheers,\nYour Data Center Light team".format( | ||||
|                                  name=user.get('name'), | ||||
|                                  amount=gp_details['amount'], | ||||
|                                  recurring=( | ||||
|                                      _(' This is a monthly recurring plan.') | ||||
|                                      if gp_details['recurring'] else '' | ||||
|                                  ) | ||||
|                              ) | ||||
|                           ), | ||||
|                 'reply_to': ['info@ungleich.ch'], | ||||
|             } | ||||
|             send_plain_email_task.delay(email_data) | ||||
| 
 | ||||
|             response = { | ||||
|                 'status': True, | ||||
|                 'redirect': ( | ||||
|                     reverse('hosting:orders') | ||||
|                     if request.user.is_authenticated() | ||||
|                     else reverse('datacenterlight:index') | ||||
|                 ), | ||||
|                 'msg_title': str(_('Thank you for the payment.')), | ||||
|                 'msg_body': str( | ||||
|                     _('You will soon receive a confirmation email of the ' | ||||
|                       'payment. You can always contact us at ' | ||||
|                       'info@ungleich.ch for any question that you may have.') | ||||
|                 ) | ||||
|             } | ||||
|             clear_all_session_vars(request) | ||||
| 
 | ||||
|             return JsonResponse(response) | ||||
| 
 | ||||
|         user = { | ||||
|             'name': custom_user.name, | ||||
|             'email': custom_user.email, | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ from django.conf import settings | |||
| from hosting.views import ( | ||||
|     RailsHostingView, DjangoHostingView, NodeJSHostingView | ||||
| ) | ||||
| from datacenterlight.views import PaymentOrderView | ||||
| from membership import urls as membership_urls | ||||
| from ungleich_page.views import LandingView | ||||
| from django.views.generic import RedirectView | ||||
|  | @ -29,6 +30,9 @@ urlpatterns = [ | |||
|     url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")), | ||||
|     url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')), | ||||
|     url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog), | ||||
|     url(r'^product/(?P<product_slug>[\w-]+)/$', | ||||
|         PaymentOrderView.as_view(), | ||||
|         name='show_product'), | ||||
| ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) | ||||
| 
 | ||||
| urlpatterns += i18n_patterns( | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| from .models import HostingOrder, HostingBill, HostingPlan | ||||
| 
 | ||||
| from .models import HostingOrder, HostingBill, HostingPlan, GenericProduct | ||||
| 
 | ||||
| admin.site.register(HostingOrder) | ||||
| admin.site.register(HostingBill) | ||||
| admin.site.register(HostingPlan) | ||||
| admin.site.register(GenericProduct) | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ | |||
| 
 | ||||
| from membership.models import CustomUser | ||||
| from utils.hosting_utils import get_all_public_keys | ||||
| from .models import UserHostingKey | ||||
| from .models import UserHostingKey, GenericProduct | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -52,6 +52,93 @@ class HostingUserLoginForm(forms.Form): | |||
|             raise forms.ValidationError(_("User does not exist")) | ||||
| 
 | ||||
| 
 | ||||
| class ProductModelChoiceField(forms.ModelChoiceField): | ||||
|     def label_from_instance(self, obj): | ||||
|         return obj.product_name | ||||
| 
 | ||||
| 
 | ||||
| class GenericPaymentForm(forms.Form): | ||||
|     product_name = ProductModelChoiceField( | ||||
|         queryset=GenericProduct.objects.all().order_by('product_name'), | ||||
|         empty_label=_("Choose a product"), | ||||
|     ) | ||||
|     amount = forms.FloatField( | ||||
|         widget=forms.TextInput( | ||||
|             attrs={'placeholder': _('Amount in CHF'), | ||||
|                    'readonly': 'readonly', } | ||||
|         ), | ||||
|         max_value=999999, | ||||
|         min_value=1, | ||||
|         label=_('Amount in CHF') | ||||
|     ) | ||||
|     recurring = forms.BooleanField(required=False, | ||||
|                                    label=_("Recurring monthly"), ) | ||||
|     description = forms.CharField( | ||||
|         widget=forms.Textarea(attrs={'style': "height: 60px;"}), | ||||
|         required=False | ||||
|     ) | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = GenericProduct | ||||
|         fields = ['product_name', 'amount', 'recurring', 'description'] | ||||
| 
 | ||||
|     def clean_amount(self): | ||||
|         amount = self.cleaned_data.get('amount') | ||||
|         if (float(self.cleaned_data.get('product_name').get_actual_price()) != | ||||
|                 amount): | ||||
|             raise forms.ValidationError(_("Amount field does not match")) | ||||
|         return amount | ||||
| 
 | ||||
|     def clean_recurring(self): | ||||
|         recurring = self.cleaned_data.get('recurring') | ||||
|         if (self.cleaned_data.get('product_name').product_is_subscription != | ||||
|                 (True if recurring else False)): | ||||
|             raise forms.ValidationError(_("Recurring field does not match")) | ||||
|         return recurring | ||||
| 
 | ||||
| 
 | ||||
| class ProductPaymentForm(GenericPaymentForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         product_id = kwargs.pop('product_id', None) | ||||
|         if product_id is not None: | ||||
|             self.product = GenericProduct.objects.get(id=product_id) | ||||
|         super(ProductPaymentForm, self).__init__(*args, **kwargs) | ||||
|         self.fields['product_name'] = forms.CharField( | ||||
|             widget=forms.TextInput( | ||||
|                 attrs={'placeholder': _('Product name'), | ||||
|                        'readonly': 'readonly'} | ||||
|             ) | ||||
|         ) | ||||
|         if self.product.product_is_subscription: | ||||
|             self.fields['amount'].label = "{amt} ({payment_type})".format( | ||||
|                 amt=_('Amount in CHF'), | ||||
|                 payment_type=_('Monthly subscription') | ||||
|             ) | ||||
|         else: | ||||
|             self.fields['amount'].label = "{amt} ({payment_type})".format( | ||||
|                 amt=_('Amount in CHF'), | ||||
|                 payment_type=_('One time payment') | ||||
|             ) | ||||
|         self.fields['recurring'].widget = forms.HiddenInput() | ||||
|         self.fields['product_name'].widget.attrs['class'] = 'input-no-border' | ||||
|         self.fields['amount'].widget.attrs['class'] = 'input-no-border' | ||||
|         self.fields['description'].widget.attrs['class'] = 'input-no-border' | ||||
| 
 | ||||
|     def clean_amount(self): | ||||
|         amount = self.cleaned_data.get('amount') | ||||
|         if (self.product is None or | ||||
|                 float(self.product.get_actual_price()) != amount): | ||||
|             raise forms.ValidationError(_("Amount field does not match")) | ||||
|         return amount | ||||
| 
 | ||||
|     def clean_recurring(self): | ||||
|         recurring = self.cleaned_data.get('recurring') | ||||
|         if (self.product.product_is_subscription != | ||||
|                 (True if recurring else False)): | ||||
|             raise forms.ValidationError(_("Recurring field does not match")) | ||||
|         return recurring | ||||
| 
 | ||||
| 
 | ||||
| class HostingUserSignupForm(forms.ModelForm): | ||||
|     confirm_password = forms.CharField(label=_("Confirm Password"), | ||||
|                                        widget=forms.PasswordInput()) | ||||
|  | @ -111,7 +198,7 @@ class UserHostingKeyForm(forms.ModelForm): | |||
|                 public_key=openssh_pubkey_str).first().name | ||||
|             KEY_EXISTS_MESSAGE = _( | ||||
|                 "This key exists already with the name \"%(name)s\"") % { | ||||
|                 'name': key_name} | ||||
|                                      'name': key_name} | ||||
|             raise forms.ValidationError(KEY_EXISTS_MESSAGE) | ||||
| 
 | ||||
|         with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file: | ||||
|  |  | |||
							
								
								
									
										41
									
								
								hosting/migrations/0048_auto_20181003_0757.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								hosting/migrations/0048_auto_20181003_0757.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.4 on 2018-10-03 07:57 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import utils.mixins | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('hosting', '0047_auto_20180821_1240'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='GenericProduct', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('product_name', models.CharField(default='', max_length=128)), | ||||
|                 ('product_slug', models.SlugField(help_text='An optional html id for the Section. Required to set as target of a link on page', unique=True)), | ||||
|                 ('product_description', models.CharField(default='', max_length=500)), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True)), | ||||
|                 ('product_price', models.DecimalField(decimal_places=2, max_digits=6)), | ||||
|                 ('product_vat', models.DecimalField(decimal_places=4, default=0, max_digits=6)), | ||||
|                 ('product_is_subscription', models.BooleanField(default=True)), | ||||
|             ], | ||||
|             bases=(utils.mixins.AssignPermissionsMixin, models.Model), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='hostingorder', | ||||
|             name='generic_payment_description', | ||||
|             field=models.CharField(max_length=500, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='hostingorder', | ||||
|             name='generic_product', | ||||
|             field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.GenericProduct'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										20
									
								
								hosting/migrations/0049_auto_20181005_0736.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								hosting/migrations/0049_auto_20181005_0736.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9.4 on 2018-10-05 07:36 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('hosting', '0048_auto_20181003_0757'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='genericproduct', | ||||
|             name='product_slug', | ||||
|             field=models.SlugField(help_text='An mandatory unique slug for the product', unique=True), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -9,8 +9,8 @@ from django.utils.functional import cached_property | |||
| 
 | ||||
| from datacenterlight.models import VMPricing, VMTemplate | ||||
| from membership.models import StripeCustomer, CustomUser | ||||
| from utils.models import BillingAddress | ||||
| from utils.mixins import AssignPermissionsMixin | ||||
| from utils.models import BillingAddress | ||||
| from utils.stripe_utils import StripeUtils | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
|  | @ -61,6 +61,30 @@ class OrderDetail(AssignPermissionsMixin, models.Model): | |||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class GenericProduct(AssignPermissionsMixin, models.Model): | ||||
|     permissions = ('view_genericproduct',) | ||||
|     product_name = models.CharField(max_length=128, default="") | ||||
|     product_slug = models.SlugField( | ||||
|         unique=True, | ||||
|         help_text=( | ||||
|             'An mandatory unique slug for the product' | ||||
|         ) | ||||
|     ) | ||||
|     product_description = models.CharField(max_length=500, default="") | ||||
|     created_at = models.DateTimeField(auto_now_add=True) | ||||
|     product_price = models.DecimalField(max_digits=6, decimal_places=2) | ||||
|     product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0) | ||||
|     product_is_subscription = models.BooleanField(default=True) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.product_name | ||||
| 
 | ||||
|     def get_actual_price(self): | ||||
|         return round( | ||||
|             self.product_price + (self.product_price * self.product_vat), 2 | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class HostingOrder(AssignPermissionsMixin, models.Model): | ||||
|     ORDER_APPROVED_STATUS = 'Approved' | ||||
|     ORDER_DECLINED_STATUS = 'Declined' | ||||
|  | @ -80,7 +104,13 @@ class HostingOrder(AssignPermissionsMixin, models.Model): | |||
|         OrderDetail, null=True, blank=True, default=None, | ||||
|         on_delete=models.SET_NULL | ||||
|     ) | ||||
| 
 | ||||
|     generic_product = models.ForeignKey( | ||||
|         GenericProduct, null=True, blank=True, default=None, | ||||
|         on_delete=models.SET_NULL | ||||
|     ) | ||||
|     generic_payment_description = models.CharField( | ||||
|         max_length=500, null=True | ||||
|     ) | ||||
|     permissions = ('view_hostingorder',) | ||||
| 
 | ||||
|     class Meta: | ||||
|  | @ -89,11 +119,18 @@ class HostingOrder(AssignPermissionsMixin, models.Model): | |||
|         ) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return ("Order Nr: #{} - VM_ID: {} - {} - {} - " | ||||
|                 "Specs: {} - Price: {}").format( | ||||
|         hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - " | ||||
|                              "Specs: {} - Price: {}").format( | ||||
|             self.id, self.vm_id, self.customer.user.email, self.created_at, | ||||
|             self.order_detail, self.price | ||||
|         ) | ||||
|         if self.generic_product_id is not None: | ||||
|             hosting_order_str += " - Generic Payment" | ||||
|             if self.stripe_charge_id is not None: | ||||
|                 hosting_order_str += " - One time charge" | ||||
|             else: | ||||
|                 hosting_order_str += " - Recurring" | ||||
|         return hosting_order_str | ||||
| 
 | ||||
|     @cached_property | ||||
|     def status(self): | ||||
|  |  | |||
|  | @ -22,6 +22,39 @@ function setBrandIcon(brand) { | |||
| 
 | ||||
| 
 | ||||
| $(document).ready(function () { | ||||
|     $(function () { | ||||
|         $("select#id_generic_payment_form-product_name").change(function () { | ||||
|             var gp_form = $('#generic-payment-form'); | ||||
|             $.ajax({ | ||||
|                 url: gp_form.attr('action'), | ||||
|                 type: 'POST', | ||||
|                 data: gp_form.serialize(), | ||||
|                 init: function () { | ||||
|                     console.log("init") | ||||
|                 }, | ||||
|                 success: function (data) { | ||||
|                     if (data.amount !== undefined) { | ||||
|                         $("#id_generic_payment_form-amount").val(data.amount); | ||||
|                         if (data.isSubscription) { | ||||
|                             $('#id_generic_payment_form-recurring').prop('checked', true); | ||||
|                         } else { | ||||
|                             $('#id_generic_payment_form-recurring').prop('checked', false); | ||||
|                         } | ||||
|                     } else { | ||||
|                         $("#id_generic_payment_form-amount").val(''); | ||||
|                         $('#id_generic_payment_form-recurring').prop('checked', false); | ||||
|                         console.log("No product found") | ||||
|                     } | ||||
|                 }, | ||||
|                 error: function (xmlhttprequest, textstatus, message) { | ||||
|                     $("#id_generic_payment_form-amount").val(''); | ||||
|                     $('#id_generic_payment_form-recurring').prop('checked', false); | ||||
|                     console.log("Error fetching product") | ||||
|                 } | ||||
|             }); | ||||
|         }) | ||||
|     }); | ||||
| 
 | ||||
|     $.ajaxSetup({ | ||||
|         beforeSend: function (xhr, settings) { | ||||
|             function getCookie(name) { | ||||
|  | @ -124,17 +157,35 @@ $(document).ready(function () { | |||
|         $('#billing-form').submit(); | ||||
|     } | ||||
| 
 | ||||
|     function getCookie(name) { | ||||
|         var value = "; " + document.cookie; | ||||
|         var parts = value.split("; " + name + "="); | ||||
|         if (parts.length === 2) return parts.pop().split(";").shift(); | ||||
|     } | ||||
| 
 | ||||
|     function submitBillingForm() { | ||||
|         var billing_form = $('#billing-form'); | ||||
|         var recurring_input = $('#id_generic_payment_form-recurring'); | ||||
|         billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />'); | ||||
|         billing_form.append('<input type="hidden" name="generic_payment_form-amount" value="' + $('#id_generic_payment_form-amount').val() + '" />'); | ||||
|         if (recurring_input.attr('type') === 'hidden') { | ||||
|             billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.val() === 'True' ? 'on' : '') + '" />'); | ||||
|         } else { | ||||
|             billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />'); | ||||
|         } | ||||
|         billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />'); | ||||
|         billing_form.submit(); | ||||
|     } | ||||
| 
 | ||||
|     var $form_new = $('#payment-form-new'); | ||||
|     $form_new.submit(payWithStripe_new); | ||||
| 
 | ||||
|     function payWithStripe_new(e) { | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         function stripeTokenHandler(token) { | ||||
|             // Insert the token ID into the form so it gets submitted to the server
 | ||||
|             $('#id_token').val(token.id); | ||||
|             $('#billing-form').submit(); | ||||
|             submitBillingForm(); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -196,10 +247,10 @@ $(document).ready(function () { | |||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     $('.credit-card-info .btn.choice-btn').click(function(){ | ||||
|             var id = this.dataset['id_card']; | ||||
|             $('#id_card').val(id); | ||||
|             $('#billing-form').submit(); | ||||
|     $('.credit-card-info .btn.choice-btn').click(function () { | ||||
|         var id = this.dataset['id_card']; | ||||
|         $('#id_card').val(id); | ||||
|         submitBillingForm(); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -134,3 +134,15 @@ $(document).ready(function() { | |||
|         $(this).find('.modal-footer .btn').addClass('hide'); | ||||
|     }) | ||||
| }); | ||||
| 
 | ||||
| window.onload = function () { | ||||
|     var locale_dates = document.getElementsByClassName("locale_date"); | ||||
|     var formats = ['YYYY-MM-DD hh:mm a']; | ||||
|     var i; | ||||
|     for (i = 0; i < locale_dates.length; i++) { | ||||
|         var oldDate = moment.utc(locale_dates[i].textContent, formats); | ||||
|         var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f; | ||||
|         locale_dates[i].innerHTML = oldDate.local().format(outputFormat); | ||||
|         locale_dates[i].className += ' done'; | ||||
|     } | ||||
| }; | ||||
|  | @ -39,7 +39,7 @@ | |||
|                     {% endif %} | ||||
|                 </span> | ||||
|             </p> | ||||
|             {% if order %} | ||||
|             {% if order and vm %} | ||||
|                 <p> | ||||
|                     <strong>{% trans "Status" %}: </strong> | ||||
|                     <strong> | ||||
|  | @ -93,77 +93,104 @@ | |||
|             <hr> | ||||
|             <div> | ||||
|                 <h4>{% trans "Order summary" %}</h4> | ||||
|                 <p> | ||||
|                     <strong>{% trans "Product" %}:</strong>  | ||||
|                     {% if vm.name %} | ||||
|                         {{ vm.name }} | ||||
|                     {% else %} | ||||
|                         {{ request.session.template.name }} | ||||
|                     {% endif %} | ||||
|                 </p> | ||||
|                 <div class="row"> | ||||
|                     <div class="col-sm-6"> | ||||
|                         {% if vm.created_at %} | ||||
|                             <p> | ||||
|                                 <span>{% trans "Period" %}: </span> | ||||
|                                 <span> | ||||
|                                     <span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span> | ||||
|                                 </span> | ||||
|                             </p> | ||||
|                 {% if vm %} | ||||
|                     <p> | ||||
|                         <strong>{% trans "Product" %}:</strong>  | ||||
|                         {% if vm.name %} | ||||
|                             {{ vm.name }} | ||||
|                         {% else %} | ||||
|                             {{ request.session.template.name }} | ||||
|                         {% endif %} | ||||
|                         <p> | ||||
|                             <span>{% trans "Cores" %}: </span> | ||||
|                             {% if vm.cores %} | ||||
|                                 <strong class="pull-right">{{vm.cores|floatformat}}</strong> | ||||
|                             {% else %} | ||||
|                                 <strong class="pull-right">{{vm.cpu|floatformat}}</strong> | ||||
|                             {% endif %} | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             <span>{% trans "Memory" %}: </span> | ||||
|                             <strong class="pull-right">{{vm.memory}} GB</strong> | ||||
|                         </p> | ||||
|                         <p> | ||||
|                             <span>{% trans "Disk space" %}: </span> | ||||
|                             <strong class="pull-right">{{vm.disk_size}} GB</strong> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div class="col-sm-12"> | ||||
|                         <hr class="thin-hr"> | ||||
|                     </div> | ||||
|                     {% if vm.vat > 0 or vm.discount.amount > 0 %} | ||||
|                     </p> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-6"> | ||||
|                             <div class="subtotal-price"> | ||||
|                                 {% if vm.vat > 0 %} | ||||
|                                     <p> | ||||
|                                         <strong>{% trans "Subtotal" %} </strong> | ||||
|                                         <strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> | ||||
|                                     </p> | ||||
|                                     <p> | ||||
|                                         <small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small> | ||||
|                                         <strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong> | ||||
|                                     </p> | ||||
|                             {% if vm.created_at %} | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Period" %}: </span> | ||||
|                                     <span> | ||||
|                                         <span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span> | ||||
|                                     </span> | ||||
|                                 </p> | ||||
|                             {% endif %} | ||||
|                             <p> | ||||
|                                 <span>{% trans "Cores" %}: </span> | ||||
|                                 {% if vm.cores %} | ||||
|                                     <strong class="pull-right">{{vm.cores|floatformat}}</strong> | ||||
|                                 {% else %} | ||||
|                                     <strong class="pull-right">{{vm.cpu|floatformat}}</strong> | ||||
|                                 {% endif %} | ||||
|                                 {% if vm.discount.amount > 0 %} | ||||
|                                     <p class="text-primary"> | ||||
|                                         {%trans "Discount" as discount_name %} | ||||
|                                         <strong>{{ vm.discount.name|default:discount_name }} </strong> | ||||
|                                         <strong class="pull-right">- {{ vm.discount.amount }} CHF</strong> | ||||
|                                     </p> | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 <span>{% trans "Memory" %}: </span> | ||||
|                                 <strong class="pull-right">{{vm.memory}} GB</strong> | ||||
|                             </p> | ||||
|                             <p> | ||||
|                                 <span>{% trans "Disk space" %}: </span> | ||||
|                                 <strong class="pull-right">{{vm.disk_size}} GB</strong> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                         <div class="col-sm-12"> | ||||
|                             <hr class="thin-hr"> | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     <div class="col-sm-6"> | ||||
|                         <p class="total-price"> | ||||
|                             <strong>{% trans "Total" %} </strong> | ||||
|                             <strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong> | ||||
|                         </p> | ||||
|                         {% if vm.vat > 0 or vm.discount.amount > 0 %} | ||||
|                             <div class="col-sm-6"> | ||||
|                                 <div class="subtotal-price"> | ||||
|                                     {% if vm.vat > 0 %} | ||||
|                                         <p> | ||||
|                                             <strong>{% trans "Subtotal" %} </strong> | ||||
|                                             <strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong> | ||||
|                                         </p> | ||||
|                                         <p> | ||||
|                                             <small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small> | ||||
|                                             <strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong> | ||||
|                                         </p> | ||||
|                                     {% endif %} | ||||
|                                     {% if vm.discount.amount > 0 %} | ||||
|                                         <p class="text-primary"> | ||||
|                                             {%trans "Discount" as discount_name %} | ||||
|                                             <strong>{{ vm.discount.name|default:discount_name }} </strong> | ||||
|                                             <strong class="pull-right">- {{ vm.discount.amount }} CHF</strong> | ||||
|                                         </p> | ||||
|                                     {% endif %} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <div class="col-sm-12"> | ||||
|                                 <hr class="thin-hr"> | ||||
|                             </div> | ||||
|                         {% endif %} | ||||
|                         <div class="col-sm-6"> | ||||
|                             <p class="total-price"> | ||||
|                                 <strong>{% trans "Total" %} </strong> | ||||
|                                 <strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 {% else %} | ||||
|                     <p> | ||||
|                         <strong>{% trans "Product" %}:</strong>  | ||||
|                         {{ product_name }} | ||||
|                     </p> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-sm-6"> | ||||
|                             <p> | ||||
|                                 <span>{% trans "Amount" %}: </span> | ||||
|                                 <strong class="pull-right">{{order.price|floatformat:2|intcomma}} CHF</strong> | ||||
|                             </p> | ||||
|                             {% if order.generic_payment_description %} | ||||
|                                 <p> | ||||
|                                     <span>{% trans "Description" %}: </span> | ||||
|                                     <strong class="pull-right">{{order.generic_payment_description}}</strong> | ||||
|                                 </p> | ||||
|                             {% endif %} | ||||
|                             {% if order.subscription_id %} | ||||
|                             <p> | ||||
|                                 <span>{% trans "Recurring" %}: </span> | ||||
|                                 <strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong> | ||||
|                             </p> | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|             <hr class="thin-hr"> | ||||
|         </div> | ||||
|  | @ -229,17 +256,6 @@ | |||
| <script type="text/javascript"> | ||||
|     {% trans "Some problem encountered. Please try again later." as err_msg %} | ||||
|     var create_vm_error_message = '{{err_msg|safe}}'; | ||||
|     window.onload = function () { | ||||
|         var locale_dates = document.getElementsByClassName("locale_date"); | ||||
|         var formats = ['YYYY-MM-DD hh:mm a'] | ||||
|         var i; | ||||
|         for (i = 0; i < locale_dates.length; i++) { | ||||
|             var oldDate = moment.utc(locale_dates[i].textContent, formats); | ||||
|             var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f; | ||||
|             locale_dates[i].innerHTML = oldDate.local().format(outputFormat); | ||||
|             locale_dates[i].className += ' done'; | ||||
|         } | ||||
|     }; | ||||
| </script> | ||||
| {%endblock%} | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ | |||
|             {% for order in orders %} | ||||
|                 <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-bighalf locale_date" data-header="{% trans 'Date' %}">{{ order.created_at | date:'Y-m-d h:i a' }}</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> | ||||
|  |  | |||
|  | @ -18,8 +18,8 @@ | |||
|                     <h3>{%trans "Billing Address" %}</h3> | ||||
|                     <hr> | ||||
|                     <form role="form" id="billing-form" method="post" action="" novalidate> | ||||
|                         {% csrf_token %} | ||||
|                         {% for field in form %} | ||||
|                             {% csrf_token %} | ||||
|                             {% bootstrap_field field show_label=False type='fields' bound_css_class='' %} | ||||
|                         {% endfor %} | ||||
|                         <div class="form-group text-right"> | ||||
|  |  | |||
|  | @ -59,7 +59,8 @@ from .forms import ( | |||
| ) | ||||
| from .mixins import ProcessVMSelectionMixin, HostingContextMixin | ||||
| from .models import ( | ||||
|     HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail | ||||
|     HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail, | ||||
|     GenericProduct | ||||
| ) | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
|  | @ -862,32 +863,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): | |||
|                 raise Http404 | ||||
| 
 | ||||
|         if obj is not None: | ||||
|             # invoice for previous order | ||||
|             try: | ||||
|                 vm_detail = VMDetail.objects.get(vm_id=obj.vm_id) | ||||
|                 context['vm'] = vm_detail.__dict__ | ||||
|                 context['vm']['name'] = '{}-{}'.format( | ||||
|                     context['vm']['configuration'], context['vm']['vm_id']) | ||||
|                 price, vat, vat_percent, discount = 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']['discount'] = discount | ||||
|                 context['vm']['vat_percent'] = vat_percent | ||||
|                 context['vm']['total_price'] = price + vat - discount['amount'] | ||||
|                 context['subscription_end_date'] = vm_detail.end_date() | ||||
|             except VMDetail.DoesNotExist: | ||||
|             if obj.generic_product_id is not None: | ||||
|                 # generic payment case | ||||
|                 logger.debug("Generic payment case") | ||||
|                 context['product_name'] = GenericProduct.objects.get( | ||||
|                     id=obj.generic_product_id | ||||
|                 ).product_name | ||||
|             else: | ||||
|                 # invoice for previous order | ||||
|                 logger.debug("Invoice of VM order") | ||||
|                 try: | ||||
|                     manager = OpenNebulaManager( | ||||
|                         email=owner.email, password=owner.password | ||||
|                     ) | ||||
|                     vm = manager.get_vm(obj.vm_id) | ||||
|                     context['vm'] = VirtualMachineSerializer(vm).data | ||||
|                     vm_detail = VMDetail.objects.get(vm_id=obj.vm_id) | ||||
|                     context['vm'] = vm_detail.__dict__ | ||||
|                     context['vm']['name'] = '{}-{}'.format( | ||||
|                         context['vm']['configuration'], context['vm']['vm_id']) | ||||
|                     price, vat, vat_percent, discount = get_vm_price_with_vat( | ||||
|                         cpu=context['vm']['cores'], | ||||
|                         ssd_size=context['vm']['disk_size'], | ||||
|  | @ -899,23 +888,43 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView): | |||
|                     context['vm']['price'] = price | ||||
|                     context['vm']['discount'] = discount | ||||
|                     context['vm']['vat_percent'] = vat_percent | ||||
|                     context['vm']['total_price'] = ( | ||||
|                             price + vat - discount['amount'] | ||||
|                     ) | ||||
|                 except WrongIdError: | ||||
|                     messages.error( | ||||
|                         self.request, | ||||
|                         _('The VM you are looking for is unavailable at the ' | ||||
|                           'moment. Please contact Data Center Light support.') | ||||
|                     ) | ||||
|                     self.kwargs['error'] = 'WrongIdError' | ||||
|                     context['error'] = 'WrongIdError' | ||||
|                 except ConnectionRefusedError: | ||||
|                     messages.error( | ||||
|                         self.request, | ||||
|                         _('In order to create a VM, you need to create/upload ' | ||||
|                           'your SSH KEY first.') | ||||
|                     ) | ||||
|                     context['vm']['total_price'] = price + vat - discount['amount'] | ||||
|                     context['subscription_end_date'] = vm_detail.end_date() | ||||
|                 except VMDetail.DoesNotExist: | ||||
|                     try: | ||||
|                         manager = OpenNebulaManager( | ||||
|                             email=owner.email, password=owner.password | ||||
|                         ) | ||||
|                         vm = manager.get_vm(obj.vm_id) | ||||
|                         context['vm'] = VirtualMachineSerializer(vm).data | ||||
|                         price, vat, vat_percent, discount = 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']['discount'] = discount | ||||
|                         context['vm']['vat_percent'] = vat_percent | ||||
|                         context['vm']['total_price'] = ( | ||||
|                                 price + vat - discount['amount'] | ||||
|                         ) | ||||
|                     except WrongIdError: | ||||
|                         messages.error( | ||||
|                             self.request, | ||||
|                             _('The VM you are looking for is unavailable at the ' | ||||
|                               'moment. Please contact Data Center Light support.') | ||||
|                         ) | ||||
|                         self.kwargs['error'] = 'WrongIdError' | ||||
|                         context['error'] = 'WrongIdError' | ||||
|                     except ConnectionRefusedError: | ||||
|                         messages.error( | ||||
|                             self.request, | ||||
|                             _('In order to create a VM, you need to create/upload ' | ||||
|                               'your SSH KEY first.') | ||||
|                         ) | ||||
|         else: | ||||
|             # new order, confirm payment | ||||
|             if 'token' in self.request.session: | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue