Compare commits

..

No commits in common. "master" and "feature/yearly-subscription" have entirely different histories.

92 changed files with 1118 additions and 4626 deletions

View file

@ -1 +0,0 @@
.git

3
.gitignore vendored
View file

@ -10,7 +10,7 @@ __pycache__/
.ropeproject/
#django
local_settings.py
Pipfile
media/
!media/keep
/CACHE/
@ -43,4 +43,3 @@ secret-key
# to keep empty dirs
!.gitkeep
*.orig
.vscode/settings.json

127
Changelog
View file

@ -1,130 +1,3 @@
3.2: 2021-02-07
* 8816: Update order confirmation text to better prepared for payment dispute
* supportticket#22990: Fix: can't add a deleted card
3.1: 2021-01-11
* 8781: Fix error is setting a default card (MR!746)
3.0: 2021-01-07
* 8393: Implement SCA for stripe payments (MR!745)
* 8691: Implment check_vm_templates management command (MR!744)
2.14: 2020-12-07
* 8692: Create a script that fixes django db for the order after celery error (MR!743)
2.13: 2020-12-02
* 8654: Fix 500 error on invoices list for the user contact+devuanhosting.com@virus.media (MR!742)
* 8593: Escape user's ssh key in xml-rpc call to create VM (MR!741)
2.12.1: 2020-07-21
* 8307: Introduce "Exclude vat calculations" for Generic Products (MR!740)
* Change DE VAT rate to 16% from 19% (MR!739)
2.12: 2020-06-23
* 7894: Show one time payment invoices (MR!738)
2.11: 2020-06-11
* Bugfix: Correct the wrong constant name (caused payment to go thru and showing error and VMs not instantiated)
2.10.8: 2020-06-10
* #8102: Refactor MAX_TIME_TO_WAIT_FOR_VM_TERMINATE to increase time to poll whether VM has been terminated or not (MR!737)
2.10.7: 2020-05-25
* Bugfix: Handle VM templates deleted in OpenNebula but VM instances still existing (MR!736)
Notes for deployment:
When deploying define a UPDATED_TEMPLATES string represented dictionary value in .env
```
# Represents Template Ids that were
# deleted and the new template Id to look for the template
# definition
UPDATED_TEMPLATES="{1: 100}"
```
2.10.6: 2020-03-25
* Bugfix: Handle Nonetype for discount's name (MR!735)
2.10.5: 2020-03-17
* Introduce base price for VMs and let admins add stripe_coupon_id (MR!730)
Notes for deployment:
1. Add env variable `VM_BASE_PRICE`
2. Migrate datacenterlight app. This introduces the stripe_coupon_code field in the VMPricing.
3. Create a coupon in stripe with the desired value and note down the stripe's coupon id
4. Update the discount amount and set the corresponding coupon id in the admin
2.10.3b: 2020-03-05
* #7773: Use username for communicating with opennebula all the time
2.10.2b: 2020-02-25
* #7764: Fix uid represented as bytestring
* #7769: [hosting] ssh private key download feature does not work well on Firefox
2.10.1: 2020-02-02:
* Changes the pricing structure of generic products into the pre vat and with vat (like that for VM)
* Shows product name (if exists) in the invoices list if it belongs to a generic product
* Small bugfixes (right alignment of price in the invoice list, show prices with 2 decimal places etc)
2.10: 2020-02-01
* Feature: Introduce new design to show VAT exclusive/VAT inclusive pricing together
* Feature: Separate VAT and discount in Stripe
* Feature: Show Stripe invoices until we have a better way of showing them elegantly
* Bugfix: Fix bug where VAT is set to 0 because user set a valid VAT number before but later chose not to use any VAT number
2.9.5: 2020-01-20
* Feature: Show invoices directly from stripe (MR!727)
2.9.4: 2020-01-10
* Bugfix: Buying VPN generic item caused 500 error
2.9.3: 2020-01-05
* Feature: Add StripeTaxRate model to save tax rates created in Stripe
* Bugfix: Add vat rates for CY, IL and LI manually
2.9.2: 2020-01-02
* Bugfix: Improve admin email for terminate vm (include subscription details and subscription amount) (MR!726)
2.9.1: 2019-12-31
* Bugfix: Error handling tax_id updated webhook
2.9: 2019-12-31
* Feature: Enable saving user's VAT Number and validate it (MR!725)
Notes for deployment:
1. Migrate db for utils app
./manage.py migrate utils
2. Uninstall old version and install a more recent version of stripe
```
source venv/bin/activate
./manage.py shell
pip uninstall stripe
pip install stripe==2.41.0
```
3. Create tax id updated webhook
```
./manage.py webhook --create \
--webhook_endpoint https://datacenterlight.ch/en-us/webhooks/ \
--events_csv customer.tax_id.updated
```
4. From the secret obtained in 3, setup an environment variable
```
WEBHOOK_SECRET='whsec......'
```
5. Deploy
2.8.2: 2019-12-24
* Bugfix: [dcl calculator plugin] Set the POST action url explicitly
2.8.1: 2019-12-24
* [dcl cms navbar plugin]: Provide an option to show non transparent navar always
2.8: 2019-12-20
* ldap_migration: Migrate django users to Ldap
Notes for deployment:
```
1. Git Pull
2. Ensure the newly dependencies in requirements.txt are installed
3. Put new values in .env
4. Run migrations
5. Restart uwsgi
```
2.7.3: 2019-12-18
* Bugfix: Swiss VAT being wrongly added to non-EU customers
2.7.2: 2019-12-17
* Add vat rates for AD, TK and IS
* Improve billing address' string representation
Notes for deployment:
- Import the newly added vat rates into db
```
./manage.py import_vat_rates vat_rates.csv
```
2.7.1: 2019-12-14
* feature: Add management command to list active VM customers (MR!723)
2.7: 2019-12-9
* feature: EU VAT for new subscriptions (MR!721)
Notes for deployment:
- Add the following to .env file
- FIRST_VM_ID_AFTER_EU_VAT=<to VM_ID from which we begin EU VAT>
- PRE_EU_VAT_RATE=whatever the rate was before introduction of EU VAT (7.7 for example)
2.6.10: 2019-11-16
* translation: Add DE translations for features in 2.6.{8,9} by moep (MR!719)
2.6.9: 2019-11-15
* feature: Allow creating yearly subscriptions for Generic Products (MR!718)
Notes for deployment:

View file

@ -1,18 +0,0 @@
FROM python:3.10.0-alpine3.15
WORKDIR /usr/src/app
RUN apk add --update --no-cache \
git \
build-base \
openldap-dev \
python3-dev \
libpq-dev \
&& rm -rf /var/cache/apk/*
# FIX https://github.com/python-ldap/python-ldap/issues/432
RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY ./ .

View file

@ -10,35 +10,13 @@ Requirements
Install
=======
.. note::
lxml that is one of the dependency of dynamicweb couldn't
get build on Python 3.7 so, please use Python 3.5.
First install packages from requirements.archlinux.txt or
requirements.debian.txt based on your distribution.
The quick way:
``pip install -r requirements.txt``
Next find the dump.db file on stagging server. Path for the file is under the base application folder.
or you can create one for yourself by running the following commands on dynamicweb server
.. code:: sh
sudo su - postgres
pg_dump app > /tmp/postgres_db.bak
exit
cp /tmp/postgres_db.bak /root/postgres_db.bak
Now, you can download this using sftp.
Install the postgresql server and import the database::
``psql -d app -U root < dump.db``
``psql -d app < dump.db``
**No migration is needed after a clean install, and You are ready to start developing.**
@ -47,9 +25,9 @@ Development
Project is separated in master branch and development branch, and feature branches.
Master branch is currently used on `Digital Glarus <https://digitalglarus.ungleich.ch/en-us/digitalglarus/>`_ and `Ungleich blog <https://digitalglarus.ungleich.ch/en-us/blog/>`_.
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
After You have completed the task, create a pull request and ask someone to review the code from other developers.
After You have complited the task create a pull request and ask someone to review the code from other developers.
**Cheat sheet for branching and forking**:

View file

@ -14,12 +14,6 @@ help:
@echo ' make rsync_upload '
@echo ' make install_debian_packages '
buildimage:
docker build -t dynamicweb:$$(git describe) .
releaseimage: buildimage
./release.sh
collectstatic:
$(PY?) $(BASEDIR)/manage.py collectstatic

View file

@ -184,11 +184,6 @@ class DCLNavbarPluginModel(CMSPlugin):
default=True,
help_text='Uncheck this if you do not want to show login/dashboard.'
)
show_non_transparent_navbar_always = models.BooleanField(
default=False,
help_text='Check this if you want to show non transparent navbar only.'
'(Useful when we want to setup a simple page)'
)
def get_logo_dark(self):
# used only if atleast one logo exists

View file

@ -1,6 +1,5 @@
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from django.conf import settings
from .cms_models import (
DCLBannerItemPluginModel, DCLBannerListPluginModel, DCLContactPluginModel,
@ -101,7 +100,6 @@ class DCLCalculatorPlugin(CMSPluginBase):
vm_type=instance.vm_type
).order_by('name')
context['instance'] = instance
context['vm_base_price'] = settings.VM_BASE_PRICE
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
return context

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-07 11:10+0000\n"
"POT-Creation-Date: 2019-11-15 14:29+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"
@ -20,7 +20,7 @@ msgstr ""
"X-Translated-Using: django-rosetta 0.8.1\n"
msgid "CMS Favicon"
msgstr "CMS Favicon"
msgstr ""
#, python-format
msgid "Your New VM %(vm_name)s at Data Center Light"
@ -52,7 +52,7 @@ msgid "Login"
msgstr "Anmelden"
msgid "Dashboard"
msgstr "Dashboard"
msgstr ""
msgid "Thank you for contacting us."
msgstr "Nachricht gesendet."
@ -64,7 +64,7 @@ msgid "Get in touch with us!"
msgstr "Sende uns eine Nachricht."
msgid "Name"
msgstr "Name"
msgstr ""
msgid "Please enter your name."
msgstr "Bitte gib Deinen Namen ein."
@ -108,7 +108,7 @@ msgid "Your account details are as follows"
msgstr "Deine Account Details sind unten aufgelistet"
msgid "Username"
msgstr "Benutzername"
msgstr "Username"
msgid "Your email address"
msgstr "Deine E-Mail-Adresse"
@ -144,8 +144,8 @@ msgid ""
"the heart of Switzerland."
msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz."
msgid "Try now, order a VM. VM price starts from only 11.5 CHF per month."
msgstr "Unser Angebot beginnt bei 11.5 CHF pro Monat. Probier's jetzt aus!"
msgid "Try now, order a VM. VM price starts from only 15CHF per month."
msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
msgid "ORDER VM"
msgstr "VM BESTELLEN"
@ -155,7 +155,7 @@ msgid "Please enter a value in range %(min_ram)s - 200."
msgstr "Bitte gib einen Wert von %(min_ram)s bis 200 ein."
msgid "VM hosting"
msgstr "VM Hosting"
msgstr ""
msgid "month"
msgstr "Monat"
@ -207,24 +207,24 @@ msgstr ""
msgid "Only wants you to pay for what you actually need."
msgstr ""
"Du möchtest nur das bezahlen, was du auch wirklich brauchst: Wähle deine "
"Möchte, dass du nur bezahlst, was du auch wirklich brauchst: Wähle deine "
"Ressourcen individuell aus!"
msgid ""
"Is creative, using a modern and alternative design for a data center in "
"order to make it more sustainable and affordable at the same time."
msgstr ""
"Es ist kreativ, da es sich ein modernes und alternatives Layout zu "
"Nutzemacht um Nachhaltigkeit zu fördern und somit erschwingliche Preise "
"bieten zu können."
"Ist kreativ, indem es sich ein modernes und alternatives Layout zu Nutze "
"macht um Nachhaltigkeit zu fördern und somit erschwingliche Preise bieten zu "
"können."
msgid ""
"Cuts down the costs for you by using FOSS (Free Open Source Software) "
"exclusively, wherefore we can save money from paying licenses."
msgstr ""
"Um unnötige Kosten zu sparen werden, wird ausschliesslich Software aufBasis "
"von FOSS (Free Open Source Software) eingesetzt und dadurch können auf "
"Lizenzgebühren verzichtet werden."
"Sorgt dafür, dass unnötige Kosten erspart werden, indem es ausschliesslich "
"mit FOSS (Free Open Source Software) arbeitet und wir daher auf "
"Lizenzgebühren verzichten können."
msgid "Scale out"
msgstr "Skalierung"
@ -311,7 +311,7 @@ msgid "Billing Address"
msgstr "Rechnungsadresse"
msgid "Make a payment"
msgstr "Tätige eine Bezahlung"
msgstr ""
msgid "Your Order"
msgstr "Deine Bestellung"
@ -406,19 +406,6 @@ msgstr "Datum"
msgid "Billed to"
msgstr "Rechnungsadresse"
msgid "VAT Number"
msgstr "MwSt-Nummer"
msgid "Your VAT number has been verified"
msgstr "Deine MwSt-Nummer wurde überprüft"
msgid ""
"Your VAT number is under validation. VAT will be adjusted, once the "
"validation is complete."
msgstr ""
"Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, sobald "
"die Validierung abgeschlossen ist."
msgid "Payment method"
msgstr "Bezahlmethode"
@ -431,63 +418,66 @@ msgstr "Bestellungsübersicht"
msgid "Product"
msgstr "Produkt"
msgid "Description"
msgstr "Beschreibung"
msgid "Recurring"
msgstr "Wiederholend"
msgid "Price Before VAT"
msgstr "Preis ohne MwSt."
msgid "Pre VAT"
msgstr "Exkl. MwSt."
msgid "Price"
msgstr "Preise"
msgid "VAT for"
msgstr "MwSt für"
msgstr ""
msgid "Your Price in Total"
msgstr "Dein Gesamtpreis"
msgid "Total Amount"
msgstr "Gesamtsumme"
msgid "Amount"
msgstr ""
msgid "Description"
msgstr ""
msgid "Recurring"
msgstr ""
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "VAT"
msgstr "Mehrwertsteuer"
#, fuzzy, python-format
#| msgid ""
#| "By clicking \"Place order\" this plan will charge your credit card "
#| "account with %(total_price)s CHF/month"
msgid ""
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(total_price)s CHF/year"
msgstr ""
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(total_price)s CHF pro Jahr belastet"
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 "
"%(total_price)s CHF pro Monat belastet"
#, fuzzy, python-format
#| msgid ""
#| "By clicking \"Place order\" this payment will charge your credit card "
#| "account with a one time amount of %(total_price)s CHF"
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\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(total_price)s CHF/year"
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(vm_total_price)s CHF/month"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Jahr belastet."
#, python-format
msgid ""
"\n"
" By clicking \"Place order\" you agree to "
"our <a href=\"https://datacenterlight.ch/en-us/cms/terms-of-service/\">Terms "
"of Service</a> and this plan will charge your credit card account with "
"%(total_price)s CHF/month"
msgstr ""
"\n"
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Monat belastet."
#, python-format
msgid ""
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(total_price)s CHF"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF belastet."
#, python-format
msgid ""
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(vm_total_price)s CHF/"
"month"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_total_price)s CHF/Monat belastet"
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
"%(vm_total_price)s CHF pro Monat belastet"
msgid "Place order"
msgstr "Bestellen"
@ -499,10 +489,10 @@ msgid "Hold tight, we are processing your request"
msgstr "Bitte warten - wir verarbeiten Deine Anfrage gerade"
msgid "OK"
msgstr "Ok"
msgstr ""
msgid "Close"
msgstr "Schliessen"
msgstr ""
msgid "Some problem encountered. Please try again later."
msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal."
@ -514,7 +504,7 @@ msgid "Tech Stack"
msgstr "Tech Stack"
msgid "We are seriously open source."
msgstr "Wir sind vollends Open Source."
msgstr "Wir sind vollends opensource."
msgid ""
" Our full software stack is open source We don't use anything that isn't "
@ -584,16 +574,13 @@ msgid "Starting from only 15CHF per month. Try now."
msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
msgid "Actions speak louder than words. Let's do it, try our VM now."
msgstr "Taten sagen mehr als Worte Teste jetzt unsere VM!"
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgstr "Tagen sagen mehr als Worte Teste jetzt unsere VM!"
msgid "Invalid number of cores"
msgstr "Ungültige Anzahle CPU-Kerne"
msgid "Invalid calculator properties"
msgstr "Ungültige Berechnungseigenschaften"
msgstr ""
msgid "Invalid RAM size"
msgstr "Ungültige RAM-Grösse"
@ -604,24 +591,17 @@ msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
#, python-brace-format
msgid "{user} does not have permission to access the card"
msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
msgid "An error occurred. Details: {}"
msgstr "Ein Fehler ist aufgetreten. Details: {}"
msgid "Confirm Order"
msgstr "Bestellung Bestätigen"
#, fuzzy
#| msgid "Thank you!"
msgid "Thank you !"
msgstr "Vielen Dank!"
msgid "Your product will be provisioned as soon as we receive the payment."
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}"
msgid "Error."
msgstr ""
@ -632,30 +612,16 @@ msgstr ""
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
"Popup zur Bezahlseite weitergeleitet."
msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung."
msgid ""
"Your product will be provisioned as soon as we receive a payment "
"confirmation from Stripe. We will send you a confirmation email. You can "
"always contact us at support@datacenterlight.ch"
#, python-brace-format
msgid "An error occurred while associating the card. Details: {details}"
msgstr ""
msgid ""
"Your VM will be up and running in a few moments. We will send you a "
"confirmation email as soon as it is ready."
msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
msgid " This is a monthly recurring plan."
msgstr "Dies ist ein monatlich wiederkehrender Plan."
msgid " This is an yearly recurring plan."
msgstr "Dies ist ein jährlich wiederkehrender Plan."
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
msgid "Confirmation of your payment"
msgstr "Bestätigung deiner Zahlung"
msgstr ""
msgid " This is a monthly recurring plan."
msgstr ""
#, python-brace-format
msgid ""
@ -667,14 +633,6 @@ msgid ""
"Cheers,\n"
"Your Data Center Light team"
msgstr ""
"Hallo {name},\n"
"\n"
"vielen Dank für deine Bestellung!\n"
"Wir haben deine Bezahlung in Höhe von {amount:.2f} CHF erhalten. "
"{recurring}\n"
"\n"
"Grüsse\n"
"Dein Data Center Light Team"
msgid "Thank you for the payment."
msgstr "Danke für Deine Bestellung."
@ -683,49 +641,16 @@ 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 ""
"Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst "
"jederzeit unter info@ungleich.ch kontaktieren."
#, python-format
#~ 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 "
#~ "%(total_price)s CHF pro Monat belastet"
msgid "Thank you for the order."
msgstr "Danke für Deine Bestellung."
#, fuzzy, python-format
#~| msgid ""
#~| "By clicking \"Place order\" this payment will charge your credit card "
#~| "account with a one time amount of %(total_price)s CHF"
#~ 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-brace-format
#~ msgid "{user} does not have permission to access the card"
#~ msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
#~ msgid "An error occurred. Details: {}"
#~ msgstr "Ein Fehler ist aufgetreten. Details: {}"
#~ msgid "Price"
#~ msgstr "Preise"
#~ msgid "Total Amount"
#~ msgstr "Gesamtsumme"
#~ msgid "Amount"
#~ msgstr "Betrag"
#~ msgid "Subtotal"
#~ msgstr "Zwischensumme"
#~ msgid "VAT"
#~ msgstr "Mehrwertsteuer"
msgid ""
"Your VM will be up and running in a few moments. We will send you a "
"confirmation email as soon as it is ready."
msgstr ""
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
"auf sie zugreifen kannst."
#~ msgid ""
#~ "You are not making any payment yet. After submitting your card "
@ -735,6 +660,9 @@ msgstr ""
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
#~ "hast."
#~ msgid "Card Number"
#~ msgstr "Kreditkartennummer"
#~ msgid ""
#~ "You are not making any payment yet. After placing your order, you will be "
#~ "taken to the Submit Payment Page."
@ -786,6 +714,9 @@ msgstr ""
#~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie "
#~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld."
#~ msgid "Thank you!"
#~ msgstr "Vielen Dank!"
#~ msgid "Thank you for order! Our team will contact you via email"
#~ msgstr ""
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich "

View file

@ -1,41 +0,0 @@
import logging
from django.core.management.base import BaseCommand
from hosting.models import (
HostingOrder, VMDetail
)
from membership.models import CustomUser
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Dumps the email addresses of all customers who have a VM'''
def add_arguments(self, parser):
parser.add_argument('-a', '--all_registered', action='store_true',
help='All registered users')
def handle(self, *args, **options):
all_registered = options['all_registered']
all_customers_set = set()
if all_registered:
all_customers = CustomUser.objects.filter(
is_admin=False, validated=True
)
for customer in all_customers:
all_customers_set.add(customer.email)
else:
all_hosting_orders = HostingOrder.objects.filter()
running_vm_details = VMDetail.objects.filter(terminated_at=None)
running_vm_ids = [rvm.vm_id for rvm in running_vm_details]
for order in all_hosting_orders:
if order.vm_id in running_vm_ids:
all_customers_set.add(order.customer.user.email)
for cu in all_customers_set:
print(cu)
if all_registered:
print("All registered users = %s" % len(all_customers_set))
else:
print("Total active customers = %s" % len(all_customers_set))

View file

@ -1,65 +0,0 @@
from django.core.management.base import BaseCommand
from opennebula_api.models import OpenNebulaManager
from datacenterlight.models import VMTemplate
from membership.models import CustomUser
from django.conf import settings
from time import sleep
import datetime
import json
import logging
import os
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Checks all VM templates to find if they can be instantiated'''
def add_arguments(self, parser):
parser.add_argument('user_email', type=str)
def handle(self, *args, **options):
result_dict = {}
user_email = options['user_email'] if 'user_email' in options else ""
if user_email:
cu = CustomUser.objects.get(email=user_email)
specs = {'cpu': 1, 'memory': 1, 'disk_size': 10}
manager = OpenNebulaManager(email=user_email, password=cu.password)
pub_keys = [settings.TEST_MANAGE_SSH_KEY_PUBKEY]
PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
if not os.path.exists("%s/outputs" % PROJECT_PATH):
os.mkdir("%s/outputs" % PROJECT_PATH)
for vm_template in VMTemplate.objects.all():
vm_name = 'test-%s' % vm_template.name
vm_id = manager.create_vm(
template_id=vm_template.opennebula_vm_template_id,
specs=specs,
ssh_key='\n'.join(pub_keys),
vm_name=vm_name
)
if vm_id and vm_id > 0:
result_dict[vm_name] = "%s OK, created VM %s" % (
'%s %s %s' % (vm_template.opennebula_vm_template_id,
vm_template.name, vm_template.vm_type),
vm_id
)
self.stdout.write(self.style.SUCCESS(result_dict[vm_name]))
manager.delete_vm(vm_id)
else:
result_dict[vm_name] = '''Error creating VM %s, template_id
%s %s''' % (vm_name,
vm_template.opennebula_vm_template_id,
vm_template.vm_type)
self.stdout.write(self.style.ERROR(result_dict[vm_name]))
sleep(1)
date_str = datetime.datetime.strftime(
datetime.datetime.now(), '%Y%m%d%H%M%S'
)
with open("%s/outputs/check_vm_templates_%s.txt" %
(PROJECT_PATH, date_str),
'w',
encoding='utf-8') as f:
f.write(json.dumps(result_dict))
self.stdout.write(self.style.SUCCESS("Done"))

View file

@ -1,76 +0,0 @@
from django.core.management.base import BaseCommand
from datacenterlight.tasks import handle_metadata_and_emails
from opennebula_api.models import OpenNebulaManager
from membership.models import CustomUser
import logging
import json
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''Updates the DB after manual creation of VM'''
def add_arguments(self, parser):
parser.add_argument('vm_id', type=int)
parser.add_argument('order_id', type=int)
parser.add_argument('user', type=str)
parser.add_argument('specs', type=str)
parser.add_argument('template', type=str)
def handle(self, *args, **options):
vm_id = options['vm_id']
order_id = options['order_id']
user_str = options['user']
specs_str = options['specs']
template_str = options['template']
json_acceptable_string = user_str.replace("'", "\"")
user_dict = json.loads(json_acceptable_string)
json_acceptable_string = specs_str.replace("'", "\"")
specs = json.loads(json_acceptable_string)
json_acceptable_string = template_str.replace("'", "\"")
template = json.loads(json_acceptable_string)
if vm_id <= 0:
self.stdout.write(self.style.ERROR(
'vm_id can\'t be less than or 0. Given: %s' % vm_id))
return
if vm_id <= 0:
self.stdout.write(self.style.ERROR(
'order_id can\'t be less than or 0. Given: %s' % vm_id))
return
if specs_str is None or specs_str == "":
self.stdout.write(
self.style.ERROR('specs can\'t be empty or None'))
return
user = {
'name': user_dict['name'],
'email': user_dict['email'],
'username': user_dict['username'],
'pass': user_dict['pass'],
'request_scheme': user_dict['request_scheme'],
'request_host': user_dict['request_host'],
'language': user_dict['language'],
}
cu = CustomUser.objects.get(username=user.get('username'))
# Create OpenNebulaManager
self.stdout.write(
self.style.SUCCESS(
'Connecting using %s' % (cu.username)
)
)
manager = OpenNebulaManager(email=cu.username, password=cu.password)
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template)
self.stdout.write(
self.style.SUCCESS(
'Done handling metadata and emails for %s %s %s' % (
order_id,
vm_id,
str(user)
)
)
)

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-24 03:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0029_auto_20190420_1022'),
]
operations = [
migrations.AddField(
model_name='dclnavbarpluginmodel',
name='show_non_transparent_navbar_always',
field=models.BooleanField(default=False, help_text='Check this if you want to show non transparent navbar only.(Useful when we want to setup a simple page)'),
),
]

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-02-04 03:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datacenterlight', '0030_dclnavbarpluginmodel_show_non_transparent_navbar_always'),
]
operations = [
migrations.AddField(
model_name='vmpricing',
name='stripe_coupon_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -54,7 +54,6 @@ class VMPricing(models.Model):
discount_amount = models.DecimalField(
max_digits=6, decimal_places=2, default=0
)
stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
display_str = self.name + ' => ' + ' - '.join([

View file

@ -191,9 +191,3 @@ footer .dcl-link-separator::before {
font-weight: bold;
font-size: 14px;
}
@media(max-width:767px) {
.vspace-top {
margin-top: 35px;
}
}

View file

@ -532,7 +532,6 @@
.order-detail-container .total-price {
font-size: 18px;
line-height: 20px;
}
@media (max-width: 767px) {

View file

@ -77,18 +77,16 @@
}
function _navScroll() {
if (!window.non_transparent_navbar_always) {
if ($(window).scrollTop() > 10) {
$(".navbar").removeClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#777");
$(".dropdown-menu").removeClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#777");
} else {
$(".navbar").addClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#fff");
$(".dropdown-menu").addClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#fff");
}
if ($(window).scrollTop() > 10) {
$(".navbar").removeClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#777");
$(".dropdown-menu").removeClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#777");
} else {
$(".navbar").addClass("navbar-transparent");
$(".navbar-default .btn-link").css("color", "#fff");
$(".dropdown-menu").addClass("navbar-transparent");
$(".dropdown-menu > li > a").css("color", "#fff");
}
}
@ -225,8 +223,8 @@
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
(cardPricing['storage'].value * window.ssdUnitPrice) +
window.vmBasePrice - window.discountAmount;
(cardPricing['storage'].value * window.ssdUnitPrice) -
window.discountAmount;
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}

View file

@ -56,8 +56,13 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
"Running create_vm_task on {}".format(current_task.request.hostname))
vm_id = None
try:
final_price = (
specs.get('total_price') if 'total_price' in specs
else specs.get('price')
)
if 'pass' in user:
on_user = user.get('username')
on_user = user.get('email')
on_pass = user.get('pass')
logger.debug("Using user {user} to create VM".format(user=on_user))
vm_name = None
@ -87,8 +92,108 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
if vm_id is None:
raise Exception("Could not create VM")
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template)
# Update HostingOrder with the created vm_id
hosting_order = HostingOrder.objects.filter(id=order_id).first()
error_msg = None
try:
hosting_order.vm_id = vm_id
hosting_order.save()
logger.debug(
"Updated hosting_order {} with vm_id={}".format(
hosting_order.id, vm_id
)
)
except Exception as ex:
error_msg = (
"HostingOrder with id {order_id} not found. This means that "
"the hosting order was not created and/or it is/was not "
"associated with VM with id {vm_id}. Details {details}".format(
order_id=order_id, vm_id=vm_id, details=str(ex)
)
)
logger.error(error_msg)
stripe_utils = StripeUtils()
result = stripe_utils.set_subscription_metadata(
subscription_id=hosting_order.subscription_id,
metadata={"VM_ID": str(vm_id)}
)
if result.get('error') is not None:
emsg = "Could not update subscription metadata for {sub}".format(
sub=hosting_order.subscription_id
)
logger.error(emsg)
if error_msg:
error_msg += ". " + emsg
else:
error_msg = emsg
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
context = {
'name': user.get('name'),
'email': user.get('email'),
'cores': specs.get('cpu'),
'memory': specs.get('memory'),
'storage': specs.get('disk_size'),
'price': final_price,
'template': template.get('name'),
'vm_name': vm.get('name'),
'vm_id': vm['vm_id'],
'order_id': order_id
}
if error_msg:
context['errors'] = error_msg
if 'pricing_name' in specs:
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
))
email_data = {
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
email = EmailMessage(**email_data)
email.send()
if 'pass' in user:
lang = 'en-us'
if user.get('language') is not None:
logger.debug(
"Language is set to {}".format(user.get('language')))
lang = user.get('language')
translation.activate(lang)
# Send notification to the user as soon as VM has been booked
context = {
'base_url': "{0}://{1}".format(user.get('request_scheme'),
user.get('request_host')),
'order_url': reverse('hosting:orders',
kwargs={'pk': order_id}),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')},
'vm_name': vm.get('name')
}
email_data = {
'subject': context.get('page_header'),
'to': user.get('email'),
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if vm_id > 0:
get_or_create_vm_detail(custom_user, manager, vm_id)
except Exception as e:
logger.error(str(e))
try:
@ -110,127 +215,3 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
return
return vm_id
def handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
template):
"""
Handle's setting up of the metadata in Stripe and database and sending of
emails to the user after VM creation
:param order_id: the hosting order id
:param vm_id: the id of the vm created
:param manager: the OpenNebula Manager instance
:param user: the user's dict passed to the celery task
:param specs: the specification's dict passed to the celery task
:param template: the template dict passed to the celery task
:return:
"""
custom_user = CustomUser.objects.get(email=user.get('email'))
final_price = (
specs.get('total_price') if 'total_price' in specs
else specs.get('price')
)
# Update HostingOrder with the created vm_id
hosting_order = HostingOrder.objects.filter(id=order_id).first()
error_msg = None
try:
hosting_order.vm_id = vm_id
hosting_order.save()
logger.debug(
"Updated hosting_order {} with vm_id={}".format(
hosting_order.id, vm_id
)
)
except Exception as ex:
error_msg = (
"HostingOrder with id {order_id} not found. This means that "
"the hosting order was not created and/or it is/was not "
"associated with VM with id {vm_id}. Details {details}".format(
order_id=order_id, vm_id=vm_id, details=str(ex)
)
)
logger.error(error_msg)
stripe_utils = StripeUtils()
result = stripe_utils.set_subscription_metadata(
subscription_id=hosting_order.subscription_id,
metadata={"VM_ID": str(vm_id)}
)
if result.get('error') is not None:
emsg = "Could not update subscription metadata for {sub}".format(
sub=hosting_order.subscription_id
)
logger.error(emsg)
if error_msg:
error_msg += ". " + emsg
else:
error_msg = emsg
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
context = {
'name': user.get('name'),
'email': user.get('email'),
'cores': specs.get('cpu'),
'memory': specs.get('memory'),
'storage': specs.get('disk_size'),
'price': final_price,
'template': template.get('name'),
'vm_name': vm.get('name'),
'vm_id': vm['vm_id'],
'order_id': order_id
}
if error_msg:
context['errors'] = error_msg
if 'pricing_name' in specs:
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
name=specs['pricing_name']
))
email_data = {
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['dcl-orders@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in context.items()]),
'reply_to': [context['email']],
}
email = EmailMessage(**email_data)
email.send()
if 'pass' in user:
lang = 'en-us'
if user.get('language') is not None:
logger.debug(
"Language is set to {}".format(user.get('language')))
lang = user.get('language')
translation.activate(lang)
# Send notification to the user as soon as VM has been booked
context = {
'base_url': "{0}://{1}".format(user.get('request_scheme'),
user.get('request_host')),
'order_url': reverse('hosting:invoices'),
'page_header': _(
'Your New VM %(vm_name)s at Data Center Light') % {
'vm_name': vm.get('name')},
'vm_name': vm.get('name')
}
email_data = {
'subject': context.get('page_header'),
'to': user.get('email'),
'context': context,
'template_name': 'new_booked_vm',
'template_path': 'hosting/emails/',
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
}
email = BaseEmail(**email_data)
email.send()
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
if vm_id > 0:
get_or_create_vm_detail(custom_user, manager, vm_id)

View file

@ -1,5 +1,5 @@
<div class="price-calc-section">
<div class="card">
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing vm_base_price=vm_base_price %}
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
</div>
</div>

View file

@ -1,9 +1,7 @@
{% load static i18n custom_tags cms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% if instance.show_non_transparent_navbar_always %}
<script>window.non_transparent_navbar_always=true;</script>
{% endif %}
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always != True %}navbar-transparent{% endif %}">
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#dcl-topnav">

View file

@ -28,7 +28,7 @@
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
</p>
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
</p>
</td>
</tr>

View file

@ -3,7 +3,7 @@
{% trans "Welcome to Data Center Light!" %}
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
{{ base_url }}{% url 'hosting:create_virtual_machine' %}

View file

@ -9,13 +9,12 @@
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
window.discountAmount = {{vm_pricing.discount_amount|default:0}};
window.vmBasePrice = {{vm_base_price|default:0}};
window.minRam = {{min_ram}};
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
</script>
{% endif %}
<form id="order_form" method="POST" action="{% url 'datacenterlight:index' %}" data-toggle="validator" role="form">
<form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form">
{% csrf_token %}
<input type="hidden" name="pid" value="{{instance.id}}">
<div class="title">
@ -103,4 +102,4 @@
</div>
<input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input>
<input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input>
</form>
</form>

View file

@ -1,6 +1,7 @@
{% load staticfiles i18n custom_tags %}
{% get_current_language as LANGUAGE_CODE %}
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always is False %}navbar-transparent{% endif %}">
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">

View file

@ -13,15 +13,6 @@
<!-- Credit card form -->
<div class="dcl-order-container">
<div class="payment-container">
<div id='payment_error'>
{% for message in messages %}
{% if 'vat_error' in message.tags %}
<ul class="list-unstyled">
<li><p class="card-warning-content card-warning-error">An error occurred while validating VAT number: {{ message|safe }}</p></li>
</ul>
{% endif %}
{% endfor %}
</div>
<div class="dcl-payment-grid">
<div class="dcl-payment-box">
<div class="dcl-payment-section">

View file

@ -2,14 +2,6 @@
{% load staticfiles bootstrap3 i18n custom_tags humanize %}
{% block content %}
<script>
{% if payment_intent_secret %}
console.log("payment_intent_secret");
window.paymentIntentSecret = "{{payment_intent_secret}}";
{% else %}
console.log("No payment_intent_secret");
{% endif %}
</script>
<div id="order-detail{{order.pk}}" class="order-detail-container">
{% if messages %}
<div class="alert alert-warning">
@ -40,16 +32,6 @@
{{billing_address.cardholder_name}}<br>
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
{{billing_address.city}}, {{billing_address.country}}
{% if billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
{% if vm.vat_validation_status != "ch_vat" and vm.vat_validation_status != "not_needed" %}
{% if vm.vat_validation_status == "verified" %}
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
{% else %}
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
</p>
</address>
@ -66,121 +48,53 @@
<hr>
<div>
<h4>{% trans "Order summary" %}</h4>
<style>
@media screen and (max-width:400px){
.header-no-left-padding {
padding-left: 0 !important;
}
}
@media screen and (max-width:767px){
.cmf-ord-heading {
font-size: 11px;
}
.order-detail-container .order-details {
font-size: 13px;
}
}
@media screen and (max-width:367px){
.cmf-ord-heading {
font-size: 11px;
}
.order-detail-container .order-details {
font-size: 12px;
}
}
</style>
{% if generic_payment_details %}
<div class="row">
<div class="col-sm-9">
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ generic_payment_details.product_name }}
</p>
{% if generic_payment_details.description %}
<p>
<strong>{% trans "Description" %}: </strong>
<span class="pull-right">{{generic_payment_details.description}}</span>
</p>
{% endif %}
{% if generic_payment_details.recurring %}
<p>
<strong>{% trans "Recurring" %}: </strong>
<span class="pull-right">Yes</span>
</p>
{% endif %}
</div>
{% if generic_payment_details.exclude_vat_calculations %}
{% else %}
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-sm-6">
{% if generic_payment_details.vat_rate > 0 %}
<p>
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
<strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong>
<span>{% trans "Price" %}: </span>
<strong class="pull-right">CHF {{generic_payment_details.amount_before_vat|floatformat:2|intcomma}}</strong>
</p>
<p>
<span>{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%) : </span>
<strong class="pull-right">CHF {{generic_payment_details.vat_amount|floatformat:2|intcomma}}</strong>
</p>
<p>
<span>{% trans "Total Amount" %} : </span>
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
</p>
{% else %}
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
</p>
{% endif %}
{% if generic_payment_details.description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{generic_payment_details.description}}</strong>
</p>
{% endif %}
{% if generic_payment_details.recurring %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">Yes</strong>
</p>
{% endif %}
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span></span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)</strong></p>
</div>
</div>
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>Price</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right" >{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><strong>Total</strong></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><strong class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong></p>
</div>
</div>
</div>
{% endif %}
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
<strong class="total-price pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong>
</div>
</div>
{% else %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ request.session.template.name }}
</p>
<div class="row">
<div class="col-sm-9">
<div class="col-sm-6">
<p>
<span>{% trans "Cores" %}: </span>
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
@ -197,75 +111,38 @@
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<p>
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
{% 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 class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span></span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%)</strong></p>
</div>
</div>
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>Price</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right" >{{vm.price|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">{{vm.price_with_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
{% if vm.discount.amount > 0 %}
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><span>{{vm.discount.name}}</span></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><span class="pull-right">-{{vm.discount.amount|floatformat:2|intcomma}} CHF</span></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><span class="pull-right">-{{vm.discount.amount_with_vat|floatformat:2|intcomma}} CHF</span></p>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<div class="row">
<div class="col-md-4 col-sm-4 col-xs-4">
<p><strong>Total</strong></p>
</div>
<div class="col-md-3 col-sm-3 col-xs-4">
<p><strong class="pull-right">{{vm.price_after_discount|floatformat:2|intcomma}} CHF</strong></p>
</div>
<div class="col-md-5 col-sm-5 col-xs-4">
<p><strong class="pull-right">{{vm.price_after_discount_with_vat|floatformat:2|intcomma}} CHF</strong></p>
</div>
</div>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
<div class="col-sm-9">
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
<strong class="total-price pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
</div>
</div>
{% endif %}
</div>
@ -278,16 +155,15 @@
{% if generic_payment_details %}
{% if generic_payment_details.recurring %}
{% if generic_payment_details.recurring_interval == 'year' %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/year{% endblocktrans %}.</div>
<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/year{% endblocktrans %}.</div>
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}
By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/month{% endblocktrans %}.</div>
<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>
{% endif %}
{% else %}
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF{% endblocktrans %}.</div>
<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" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
{% endif %}
</div>
<div class="col-sm-4 order-confirm-btn text-right">
@ -330,14 +206,5 @@
<script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}';
var pm_id = '{{id_payment_method}}';
var error_url = '{{ error_msg.redirect }}';
var error_msg = '{{ error_msg.msg_body }}';
var error_title = '{{ error_msg.msg_title }}';
var success_msg = '{{ success_msg.msg_body }}';
var success_title = '{{ success_msg.msg_title }}';
var success_url = '{{ success_msg.redirect }}';
window.stripeKey = "{{stripe_key}}";
window.isSubscription = ("{{is_subscription}}" === 'true');
</script>
{%endblock%}
{%endblock%}

View file

@ -1,15 +1,6 @@
import datetime
import logging
from django import template
from django.core.urlresolvers import resolve, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import activate, get_language, ugettext_lazy as _
from hosting.models import GenericProduct, HostingOrder
from utils.hosting_utils import get_ip_addresses
logger = logging.getLogger(__name__)
from django.utils.translation import activate, get_language
register = template.Library()
@ -61,116 +52,3 @@ def escaped_line_break(value):
:return:
"""
return value.replace("\\n", "\n")
@register.filter('get_line_item_from_hosting_order_charge')
def get_line_item_from_hosting_order_charge(hosting_order_id):
"""
Returns ready-to-use "html" line item to be shown for a charge in the
invoice list page
:param hosting_order_id: the HostingOrder id
:return:
"""
try:
hosting_order = HostingOrder.objects.get(id = hosting_order_id)
if hosting_order.stripe_charge_id:
return mark_safe("""
<td class="xs-td-inline">{product_name}</td>
<td class="xs-td-inline">{created_at}</td>
<td class="xs-td-inline">{total}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{receipt_url}" target="_blank">{see_invoice_text}</a>
</td>
""".format(
product_name=hosting_order.generic_product.product_name.capitalize(),
created_at=hosting_order.created_at.strftime('%Y-%m-%d'),
total='%.2f' % (hosting_order.price),
receipt_url=reverse('hosting:orders',
kwargs={'pk': hosting_order.id}),
see_invoice_text=_("See Invoice")
))
else:
return ""
except Exception as ex:
logger.error("Error %s" % str(ex))
return ""
@register.filter('get_line_item_from_stripe_invoice')
def get_line_item_from_stripe_invoice(invoice):
"""
Returns ready-to-use "html" line item to be shown for an invoice in the
invoice list page
:param invoice: the stripe Invoice object
:return:
"""
start_date = 0
end_date = 0
is_first = True
vm_id = -1
plan_name = ""
for line_data in invoice["lines"]["data"]:
if is_first:
plan_name = line_data.plan.name if line_data.plan is not None else ""
start_date = line_data.period.start
end_date = line_data.period.end
is_first = False
if hasattr(line_data.metadata, "VM_ID"):
vm_id = line_data.metadata.VM_ID
else:
if line_data.period.start < start_date:
start_date = line_data.period.start
if line_data.period.end > end_date:
end_date = line_data.period.end
if hasattr(line_data.metadata, "VM_ID"):
vm_id = line_data.metadata.VM_ID
try:
vm_id = int(vm_id)
except ValueError as ve:
print(str(ve))
if invoice["lines"]["data"]:
return mark_safe("""
<td class="xs-td-inline">{vm_id}</td>
<td class="xs-td-inline">{ip_addresses}</td>
<td class="xs-td-inline">{period}</td>
<td class="xs-td-inline text-right dcl-text-right-padding">{total}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{stripe_invoice_url}" target="_blank">{see_invoice_text}</a>
</td>
""".format(
vm_id=vm_id if vm_id > 0 else "",
ip_addresses=mark_safe(get_ip_addresses(vm_id)) if vm_id > 0 else
mark_safe(get_product_name(plan_name)),
period=mark_safe("%s &mdash; %s" % (
datetime.datetime.fromtimestamp(start_date).strftime('%Y-%m-%d'),
datetime.datetime.fromtimestamp(end_date).strftime('%Y-%m-%d'))),
total='%.2f' % (invoice.total/100),
stripe_invoice_url=invoice.hosted_invoice_url,
see_invoice_text=_("See Invoice")
))
else:
return ""
def get_product_name(plan_name):
product_name = ""
if plan_name and plan_name.startswith("generic-"):
first_index_hyphen = plan_name.index("-") + 1
product_id = plan_name[first_index_hyphen:
(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen]
try:
product = GenericProduct.objects.get(id=product_id)
product_name = product.product_name
except GenericProduct.DoesNotExist as dne:
logger.error("Generic product id=%s does not exist" % product_id)
product_name = plan_name
except GenericProduct.MultipleObjectsReturned as mor:
logger.error("Multiple products with id=%s exist" % product_id)
product_name = "Unknown"
else:
logger.debug("Product name for plan %s does not exist" % plan_name)
return product_name

View file

@ -1,9 +1,7 @@
import datetime
import logging
import pyotp
import requests
import stripe
from django.conf import settings
from django.contrib.sites.models import Site
@ -11,19 +9,12 @@ from datacenterlight.tasks import create_vm_task
from hosting.models import HostingOrder, HostingBill, OrderDetail
from membership.models import StripeCustomer
from utils.forms import UserBillingAddressForm
from utils.models import BillingAddress, UserBillingAddress
from utils.stripe_utils import StripeUtils
from utils.models import BillingAddress
from .cms_models import CMSIntegration
from .models import VMPricing, VMTemplate
logger = logging.getLogger(__name__)
eu_countries = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk',
'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it',
'lv', 'lu', 'mt', 'nl', 'pl', 'pt', 'ro','sk', 'si', 'es',
'se', 'gb']
def get_cms_integration(name):
current_site = Site.objects.get_current()
try:
@ -38,14 +29,12 @@ def get_cms_integration(name):
def create_vm(billing_address_data, stripe_customer_id, specs,
stripe_subscription_obj, card_details_dict, request,
vm_template_id, template, user):
logger.debug("In create_vm")
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'],
vat_number=billing_address_data['vat_number'],
country=billing_address_data['country']
)
billing_address.save()
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
@ -103,6 +92,8 @@ def create_vm(billing_address_data, stripe_customer_id, specs,
create_vm_task.delay(vm_template_id, user, specs, template, order.id)
clear_all_session_vars(request)
def clear_all_session_vars(request):
if request.session is not None:
@ -110,9 +101,7 @@ def clear_all_session_vars(request):
'billing_address_data', 'card_id',
'token', 'customer', 'generic_payment_type',
'generic_payment_details', 'product_id',
'order_confirm_url', 'new_user_hosting_key_id',
'vat_validation_status', 'billing_address_id',
'id_payment_method']:
'order_confirm_url', 'new_user_hosting_key_id']:
if session_var in request.session:
del request.session[session_var]
@ -134,172 +123,3 @@ def check_otp(name, realm, token):
data=data
)
return response.status_code
def validate_vat_number(stripe_customer_id, billing_address_id,
is_user_ba=False):
if is_user_ba:
try:
billing_address = UserBillingAddress.objects.get(
id=billing_address_id)
except UserBillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug(
"UserBillingAddress does not exist for %s" % billing_address_id)
except UserBillingAddress.MultipleObjectsReturned as mor:
logger.debug(
"Multiple UserBillingAddress exist for %s" % billing_address_id)
billing_address = UserBillingAddress.objects.filter(
id=billing_address_id).order_by('-id').first()
else:
try:
billing_address = BillingAddress.objects.get(id=billing_address_id)
except BillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
except BillingAddress.MultipleObjectsReturned as mor:
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
billing_address = BillingAddress.objects.filter(id=billing_address_id).order_by('-id').first()
if billing_address is not None:
logger.debug("BillingAddress found: %s %s type=%s" % (
billing_address_id, str(billing_address), type(billing_address)))
if billing_address.country.lower().strip() not in eu_countries:
return {
"validated_on": "",
"status": "not_needed"
}
if billing_address.vat_number_validated_on:
logger.debug("billing_address verified on %s" %
billing_address.vat_number_validated_on)
return {
"validated_on": billing_address.vat_number_validated_on,
"status": "verified"
}
else:
logger.debug("billing_address not yet verified, "
"Checking if we already have a tax id")
if billing_address.stripe_tax_id:
logger.debug("We have a tax id %s" % billing_address.stripe_tax_id)
tax_id_obj = stripe.Customer.retrieve_tax_id(
stripe_customer_id,
billing_address.stripe_tax_id,
)
if tax_id_obj.verification.status == "verified":
logger.debug("Latest status on Stripe=%s. Updating" %
tax_id_obj.verification.status)
# update billing address
billing_address.vat_number_validated_on = datetime.datetime.now()
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
return {
"status": "verified",
"validated_on": billing_address.vat_number_validated_on
}
else:
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
logger.debug(
"Latest status on Stripe=%s" % str(tax_id_obj)
)
return {
"status": tax_id_obj.verification.status if tax_id_obj
else "unknown",
"validated_on": ""
}
else:
logger.debug("Creating a tax id")
logger.debug("Billing address = %s" % str(billing_address))
tax_id_obj = create_tax_id(
stripe_customer_id, billing_address_id,
"ch_vat" if billing_address.country.lower() == "ch" else "eu_vat",
is_user_ba=is_user_ba
)
logger.debug("tax_id_obj = %s" % str(tax_id_obj))
else:
logger.debug("invalid billing address")
return {
"status": "invalid billing address",
"validated_on": ""
}
if 'response_object' in tax_id_obj:
return tax_id_obj
return {
"status": tax_id_obj.verification.status,
"validated_on": datetime.datetime.now() if tax_id_obj.verification.status == "verified" else ""
}
def create_tax_id(stripe_customer_id, billing_address_id, type,
is_user_ba=False):
if is_user_ba:
try:
billing_address = UserBillingAddress.objects.get(
id=billing_address_id)
except UserBillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug(
"UserBillingAddress does not exist for %s" % billing_address_id)
except UserBillingAddress.MultipleObjectsReturned as mor:
logger.debug(
"Multiple UserBillingAddress exist for %s" % billing_address_id)
billing_address = UserBillingAddress.objects.filter(
id=billing_address_id).order_by('-id').first()
else:
try:
billing_address = BillingAddress.objects.get(id=billing_address_id)
except BillingAddress.DoesNotExist as dne:
billing_address = None
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
except BillingAddress.MultipleObjectsReturned as mor:
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
billing_address = BillingAddress.objects.filter(billing_address_id).order_by('-id').first()
tax_id_obj = None
if billing_address:
stripe_utils = StripeUtils()
tax_id_response = stripe_utils.get_or_create_tax_id_for_user(
stripe_customer_id,
vat_number=billing_address.vat_number,
type=type,
country=billing_address.country
)
tax_id_obj = tax_id_response.get('response_object')
if not tax_id_obj:
logger.debug("Received none in tax_id_obj")
return {
'paid': False,
'response_object': None,
'error': "No such address found" if 'error' not in tax_id_response else
tax_id_response["error"]
}
try:
stripe_customer = StripeCustomer.objects.get(stripe_id=stripe_customer_id)
billing_address_set = set()
logger.debug("Updating billing address")
for ho in stripe_customer.hostingorder_set.all():
if ho.billing_address.vat_number == billing_address.vat_number:
billing_address_set.add(ho.billing_address)
for b_address in billing_address_set:
b_address.stripe_tax_id = tax_id_obj.id
b_address.vat_validation_status = tax_id_obj.verification.status
b_address.save()
logger.debug("Updated billing_address %s" % str(b_address))
ub_addresses = stripe_customer.user.billing_addresses.filter(
vat_number=billing_address.vat_number)
for ub_address in ub_addresses:
ub_address.stripe_tax_id = tax_id_obj.id
ub_address.vat_validation_status = tax_id_obj.verification.status
ub_address.save()
logger.debug("Updated user_billing_address %s" % str(ub_address))
except StripeCustomer.DoesNotExist as dne:
logger.debug("StripeCustomer %s does not exist" % stripe_customer_id)
billing_address.stripe_tax_id = tax_id_obj.id
billing_address.vat_validation_status = tax_id_obj.verification.status
billing_address.save()
return tax_id_obj

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,6 @@ class MembershipBillingForm(BillingAddressForm):
'city': _('City'),
'postal_code': _('Postal Code'),
'country': _('Country'),
'vat_number': _('VAT Number'),
}

View file

@ -376,6 +376,8 @@ msgid ""
" digitalglarus.ch<br/>\n"
" hack4lgarus.ch<br/>\n"
" ipv6onlyhosting.com<br/>\n"
" ipv6onlyhosting.ch<br/>\n"
" ipv6onlyhosting.net<br/>\n"
" django-hosting.ch<br/>\n"
" rails-hosting.ch<br/>\n"
" node-hosting.ch<br/>\n"
@ -634,8 +636,8 @@ msgstr ""
"Internetangebot der ungleich glarus ag, welches unter den nachfolgenden "
"Domains erreichbar ist:<br/><br/>ungleich.ch<br/>datacenterlight.ch<br/"
">devuanhosting.com<br/>devuanhosting.ch<br/>digitalglarus.ch<br/>hack4lgarus."
"ch<br/>ipv6onlyhosting.com<br/>django-hosting.ch<br/>rails-hosting.ch"
"<br/>node-hosting.ch<br/>blog."
"ch<br/>ipv6onlyhosting.com<br/>ipv6onlyhosting.ch<br/>ipv6onlyhosting.net<br/"
">django-hosting.ch<br/>rails-hosting.ch<br/>node-hosting.ch<br/>blog."
"ungleich.ch<br/><br/>Der Datenschutzbeauftragte des Verantwortlichen ist:<br/"
"><br/>Sanghee Kim<br/>ungleich glarus ag<br/>Bahnhofstrasse 1<br/>8783 "
"Linthal (CH)<br/>E-Mail: <a href=\"mailto:sanghee.kim@ungleich.ch\">sanghee."
@ -836,4 +838,3 @@ msgstr ""
#~ msgid "index/?$"
#~ msgstr "index/?$"

View file

@ -52,7 +52,7 @@ PROJECT_DIR = os.path.abspath(
)
# load .env file
dotenv.load_dotenv("{0}/.env".format(PROJECT_DIR))
dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
from multisite import SiteID
@ -153,7 +153,6 @@ INSTALLED_APPS = (
'rest_framework',
'opennebula_api',
'django_celery_results',
'webhook',
)
MIDDLEWARE_CLASSES = (
@ -245,9 +244,8 @@ DATABASES = {
}
AUTHENTICATION_BACKENDS = (
'utils.backend.MyLDAPBackend',
'guardian.backends.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend',
)
# Internationalization
@ -631,6 +629,8 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
'datacenterlight.ch': 'UA-62285904-8',
'devuanhosting.ch': 'UA-62285904-9',
'devuanhosting.com': 'UA-62285904-9',
'ipv6onlyhosting.ch': 'UA-62285904-10',
'ipv6onlyhosting.net': 'UA-62285904-10',
'ipv6onlyhosting.com': 'UA-62285904-10',
'comic.ungleich.ch': 'UA-62285904-13',
'127.0.0.1:8000': 'localhost',
@ -703,7 +703,7 @@ if ENABLE_LOGGING:
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
'format': '%(asctime)s %(levelname)s %(name)s: %(message)s'
}
},
'handlers': handlers_dict,
@ -719,35 +719,7 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else
X_FRAME_OPTIONS_ALLOW_FROM_URI.strip()
))
WEBHOOK_SECRET = env('WEBHOOK_SECRET')
DEBUG = bool_env('DEBUG')
ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION')
# LDAP setup
LDAP_ADMIN_DN = env('LDAP_ADMIN_DN')
LDAP_ADMIN_PASSWORD = env('LDAP_ADMIN_PASSWORD')
AUTH_LDAP_SERVER = env('LDAPSERVER')
LDAP_CUSTOMER_DN = env('LDAP_CUSTOMER_DN')
LDAP_CUSTOMER_GROUP_ID = int(env('LDAP_CUSTOMER_GROUP_ID'))
LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH',
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file')
)
LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID'))
# Search union over OUs
AUTH_LDAP_START_TLS = bool(os.environ.get('LDAP_USE_TLS', False))
ENTIRE_SEARCH_BASE = env("ENTIRE_SEARCH_BASE")
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
READ_VM_REALM = env('READ_VM_REALM')
AUTH_NAME = env('AUTH_NAME')
@ -756,18 +728,6 @@ AUTH_REALM = env('AUTH_REALM')
OTP_SERVER = env('OTP_SERVER')
OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT')
FIRST_VM_ID_AFTER_EU_VAT = int_env('FIRST_VM_ID_AFTER_EU_VAT')
PRE_EU_VAT_RATE = float(env('PRE_EU_VAT_RATE'))
VM_BASE_PRICE = float(env('VM_BASE_PRICE'))
UPDATED_TEMPLATES_STR = env('UPDATED_TEMPLATES')
UPDATED_TEMPLATES_DICT = {}
if UPDATED_TEMPLATES_STR:
UPDATED_TEMPLATES_DICT = eval(UPDATED_TEMPLATES_STR)
MAX_TIME_TO_WAIT_FOR_VM_TERMINATE = int_env(
'MAX_TIME_TO_WAIT_FOR_VM_TERMINATE', 15)
if DEBUG:
from .local import * # flake8: noqa

View file

@ -28,7 +28,9 @@ ALLOWED_HOSTS = [
".devuanhosting.ch",
".devuanhosting.com",
".digitalezukunft.ch",
".ipv6onlyhosting.ch",
".ipv6onlyhosting.com",
".ipv6onlyhosting.net",
".digitalglarus.ch",
".hack4glarus.ch",
".xn--nglarus-n2a.ch"

View file

@ -11,7 +11,6 @@ from hosting.views import (
RailsHostingView, DjangoHostingView, NodeJSHostingView
)
from datacenterlight.views import PaymentOrderView
from webhook import views as webhook_views
from membership import urls as membership_urls
from ungleich_page.views import LandingView
from django.views.generic import RedirectView
@ -63,7 +62,6 @@ urlpatterns += i18n_patterns(
name='blog_list_view'),
url(r'^cms/', include('cms.urls')),
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
url(r'^webhooks/', webhook_views.handle_webhook),
url(r'^$', RedirectView.as_view(url='/cms') if REDIRECT_TO_CMS
else LandingView.as_view()),
url(r'^', include('ungleich_page.urls', namespace='ungleich_page')),

View file

@ -2,7 +2,6 @@ import datetime
import logging
import subprocess
import tempfile
import xml
from django import forms
from django.conf import settings
@ -208,7 +207,7 @@ class UserHostingKeyForm(forms.ModelForm):
logger.debug(
"Not a correct ssh format {error}".format(error=str(cpe)))
raise forms.ValidationError(KEY_ERROR_MESSAGE)
return xml.sax.saxutils.escape(openssh_pubkey_str)
return openssh_pubkey_str
def clean_name(self):
INVALID_NAME_MESSAGE = _("Comma not accepted in the name of the key")

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-07 10:19+0000\n"
"POT-Creation-Date: 2019-11-15 16:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -28,31 +28,31 @@ msgid "User does not exist"
msgstr "Der Benutzer existiert nicht"
msgid "Choose a product"
msgstr "Wähle ein Produkt"
msgstr ""
msgid "Amount in CHF"
msgstr "Betrag"
msgid "Recurring monthly"
msgstr "monatlich wiederkehrend"
msgstr ""
msgid "Amount field does not match"
msgstr "Betragsfeld stimmt nicht überein"
msgstr ""
msgid "Recurring field does not match"
msgstr "Betragsfeld stimmt nicht überein"
msgstr ""
msgid "Product name"
msgstr "Produkt"
msgid "Monthly subscription"
msgstr "Monatliches Abonnement"
msgstr ""
msgid "Yearly subscription"
msgstr "Jährliches Abonnement"
msgstr ""
msgid "One time payment"
msgstr "Einmalzahlung"
msgstr ""
msgid "Confirm Password"
msgstr "Passwort Bestätigung"
@ -76,7 +76,7 @@ msgid "Please input a proper SSH key"
msgstr "Bitte verwende einen gültigen SSH-Key"
msgid "Comma not accepted in the name of the key"
msgstr "Komma im Namen des Keys wird nicht akzeptiert"
msgstr ""
msgid "All Rights Reserved"
msgstr "Alle Rechte vorbehalten"
@ -211,9 +211,6 @@ msgstr "Bezahlbares VM Hosting in der Schweiz"
msgid "My Dashboard"
msgstr "Mein Dashboard"
msgid "Welcome"
msgstr ""
msgid "My VMs"
msgstr "Meine VMs"
@ -367,11 +364,6 @@ msgstr "Abgelehnt"
msgid "Billed to"
msgstr "Rechnungsadresse"
#, fuzzy
#| msgid "Card Number"
msgid "VAT Number"
msgstr "Kreditkartennummer"
msgid "Payment method"
msgstr "Bezahlmethode"
@ -399,9 +391,6 @@ msgstr "Festplattenkapazität"
msgid "Subtotal"
msgstr "Zwischensumme"
msgid "VAT for"
msgstr ""
msgid "VAT"
msgstr "Mehrwertsteuer"
@ -415,19 +404,19 @@ msgid "Amount"
msgstr "Betrag"
msgid "Description"
msgstr "Beschreibung"
msgstr ""
msgid "Recurring"
msgstr "wiederkehrend"
msgstr ""
msgid "of"
msgstr "von"
msgstr ""
msgid "each year"
msgstr "jedes Jahr"
msgstr ""
msgid "of every month"
msgstr "jeden Monat"
msgstr ""
msgid "BACK TO LIST"
msgstr "ZURÜCK ZUR LISTE"
@ -435,21 +424,17 @@ msgstr "ZURÜCK ZUR LISTE"
msgid "Some problem encountered. Please try again later."
msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal."
#, fuzzy
#| msgid "Description"
msgid "Subscriptions"
msgstr "Beschreibung"
#, fuzzy
#| msgid "One time payment"
msgid "One-time payments"
msgstr "Einmalzahlung"
msgid "VM ID"
msgstr ""
msgid "IP Address"
msgstr "IP-Adresse"
msgstr ""
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Page"
msgstr ""
msgid "Log in"
msgstr "Anmelden"
@ -495,13 +480,11 @@ msgstr "Bestellungsübersicht"
#, python-format
msgid ""
"By clicking \"Place order\" you agree to our <a href=\"https://"
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
"this plan will charge your credit card account with %(vm_price)s CHF/month."
"By clicking \"Place order\" this plan will charge your credit card account "
"with %(vm_price)s CHF/month"
msgstr ""
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren"
" <a href=\"https://"
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_price)s CHF/Monat belastet."
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF "
"pro Monat belastet"
msgid "Place order"
msgstr "Bestellen"
@ -513,7 +496,7 @@ msgid "Hold tight, we are processing your request"
msgstr "Bitte warten - wir bearbeiten Deine Anfrage gerade"
msgid "OK"
msgstr "Ok"
msgstr ""
msgid "Close"
msgstr "Schliessen"
@ -521,12 +504,6 @@ msgstr "Schliessen"
msgid "Order Nr."
msgstr "Bestellung Nr."
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Page"
msgstr "Seite"
msgid "Your Order"
msgstr "Deine Bestellung"
@ -595,19 +572,6 @@ msgstr "Absenden"
msgid "Password reset"
msgstr "Passwort zurücksetzen"
#, fuzzy
#| msgid "Key name"
msgid "My Username"
msgstr "Key-Name"
msgid "Your VAT number has been verified"
msgstr ""
msgid ""
"Your VAT number is under validation. VAT will be adjusted, once the "
"validation is complete."
msgstr ""
msgid "UPDATE"
msgstr "AKTUALISIEREN"
@ -809,15 +773,21 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden."
msgid "The reset password link is no longer valid."
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig."
msgid "Could not set a default card."
msgstr ""
msgid "Card deassociation successful"
msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben"
msgid "You are not permitted to do this operation"
msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
msgid "The selected card does not exist"
msgstr "Die ausgewählte Karte existiert nicht"
msgid "Billing address updated successfully"
msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert"
msgid "You seem to have already added this card"
msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
#, python-brace-format
msgid "An error occurred while associating the card. Details: {details}"
msgstr ""
@ -883,7 +853,6 @@ msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
msgid ""
"We could not find the requested VM. Please "
@ -903,8 +872,6 @@ msgid ""
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
"further information."
msgstr ""
"VM beendet wegen Zeitüberschreitung. Bitte kontaktiere "
"support@datacenterlight.ch für weitere Informationen."
#, python-format
msgid "Virtual Machine %(vm_name)s Cancelled"
@ -915,15 +882,6 @@ msgstr ""
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
"noch einmal."
#~ msgid "You are not permitted to do this operation"
#~ msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
#~ msgid "The selected card does not exist"
#~ msgstr "Die ausgewählte Karte existiert nicht"
#~ msgid "You seem to have already added this card"
#~ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
#, python-format
#~ msgid "This key exists already with the name \"%(name)s\""
#~ msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"

View file

@ -1,4 +1,3 @@
import datetime
import logging
from django.core.management.base import BaseCommand
@ -20,10 +19,6 @@ class Command(BaseCommand):
def handle(self, *args, **options):
try:
for email in options['customer_email']:
self.stdout.write(
self.style.SUCCESS(
"---------------------------------------------")
)
stripe_utils = StripeUtils()
user = CustomUser.objects.get(email=email)
if hasattr(user, 'stripecustomer'):
@ -44,9 +39,7 @@ class Command(BaseCommand):
)
if all_invoices_response['error'] is not None:
self.stdout.write(self.style.ERROR(all_invoices_response['error']))
user.import_stripe_bill_remark += "{}: {},".format(datetime.datetime.now(), all_invoices_response['error'])
user.save()
continue
exit(1)
all_invoices = all_invoices_response['response_object']
self.stdout.write(self.style.SUCCESS("Obtained {} invoices".format(len(all_invoices) if all_invoices is not None else 0)))
num_invoice_created = 0
@ -61,10 +54,6 @@ class Command(BaseCommand):
if MonthlyHostingBill.create(invoice) is not None:
num_invoice_created += 1
else:
user.import_stripe_bill_remark += "{}: Import failed - {},".format(
datetime.datetime.now(),
invoice['invoice_id'])
user.save()
logger.error("Did not import invoice for %s"
"" % str(invoice))
self.stdout.write(
@ -73,9 +62,5 @@ class Command(BaseCommand):
else:
self.stdout.write(self.style.SUCCESS(
'Customer email %s does not have a stripe customer.' % email))
user.import_stripe_bill_remark += "{}: No stripecustomer,".format(
datetime.datetime.now()
)
user.save()
except Exception as e:
print(" *** Error occurred. Details {}".format(str(e)))

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-01-05 04:29
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0058_genericproduct_product_subscription_interval'),
]
operations = [
migrations.CreateModel(
name='StripeTaxRate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tax_rate_id', models.CharField(max_length=100, unique=True)),
('jurisdiction', models.CharField(max_length=10)),
('inclusive', models.BooleanField(default=False)),
('display_name', models.CharField(max_length=100)),
('percentage', models.FloatField(default=0)),
('description', models.CharField(max_length=100)),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-06-30 19:12
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('hosting', '0059_stripetaxrate'),
]
operations = [
migrations.RunSQL(
sql=["update hosting_vatrates set stop_date = '2020-06-30' where territory_codes = 'DE' and rate = '0.19'"],
reverse_sql=[
"update hosting_vatrates set stop_date = null where stop_date = '2020-06-30' and territory_codes = 'DE' and rate = '0.19'"],
),
migrations.RunSQL(
sql=[
"insert into hosting_vatrates (start_date, stop_date, territory_codes, currency_code, rate, rate_type, description) values ('2020-07-01',null,'DE', 'EUR', '0.16', 'standard', 'Germany (member state) standard VAT rate - COVID 19 reduced rate')"],
reverse_sql=[
"delete from hosting_vatrates where description = 'Germany (member state) standard VAT rate - COVID 19 reduced rate' and start_date = '2020-07-01' and territory_codes = 'DE'" ],
),
migrations.RunSQL(
sql=[
"update hosting_stripetaxrate set description = 'VAT for DE pre-COVID-19' where description = 'VAT for DE'"],
reverse_sql=[
"update hosting_stripetaxrate set description = 'VAT for DE' where description = 'VAT for DE pre-COVID-19'"],
),
]

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-07-21 16:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0060_update_DE_vat_covid-19'),
]
operations = [
migrations.AddField(
model_name='genericproduct',
name='exclude_vat_calculations',
field=models.BooleanField(default=False, help_text='When checked VAT calculations are excluded for this product'),
),
]

View file

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-23 05:36
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0061_genericproduct_exclude_vat_calculations'),
]
operations = [
migrations.CreateModel(
name='IncompleteSubscriptions',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField()),
('subscription_id', models.CharField(max_length=100)),
('subscription_status', models.CharField(max_length=30)),
('name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254)),
('request', models.TextField()),
('stripe_api_cus_id', models.CharField(max_length=30)),
('card_details_response', models.TextField()),
('stripe_subscription_obj', models.TextField()),
('stripe_onetime_charge', models.TextField()),
('gp_details', models.TextField()),
('specs', models.TextField()),
('vm_template_id', models.PositiveIntegerField(default=0)),
('template', models.TextField()),
('billing_address_data', models.TextField()),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-23 06:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0062_incompletesubscriptions'),
]
operations = [
migrations.AlterField(
model_name='incompletesubscriptions',
name='completed_at',
field=models.DateTimeField(null=True),
),
]

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-31 10:13
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0063_auto_20201223_0612'),
]
operations = [
migrations.CreateModel(
name='IncompletePaymentIntents',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('completed_at', models.DateTimeField(null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('payment_intent_id', models.CharField(max_length=100)),
('request', models.TextField()),
('stripe_api_cus_id', models.CharField(max_length=30)),
('card_details_response', models.TextField()),
('stripe_subscription_id', models.TextField()),
('stripe_charge_id', models.TextField()),
('gp_details', models.TextField()),
('billing_address_data', models.TextField()),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2020-12-31 10:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0064_incompletepaymentintents'),
]
operations = [
migrations.AlterField(
model_name='incompletepaymentintents',
name='stripe_charge_id',
field=models.CharField(max_length=100, null=True),
),
migrations.AlterField(
model_name='incompletepaymentintents',
name='stripe_subscription_id',
field=models.CharField(max_length=100, null=True),
),
]

View file

@ -1,3 +1,4 @@
import decimal
import json
import logging
import os
@ -81,22 +82,15 @@ class GenericProduct(AssignPermissionsMixin, models.Model):
product_subscription_interval = models.CharField(
max_length=10, default="month",
help_text="Choose between `year` and `month`")
exclude_vat_calculations = models.BooleanField(
default=False,
help_text="When checked VAT calculations are excluded for this product"
)
def __str__(self):
return self.product_name
def get_actual_price(self, vat_rate=None):
if self.exclude_vat_calculations:
return round(float(self.product_price), 2)
else:
VAT = vat_rate if vat_rate is not None else self.product_vat
return round(
float(self.product_price) + float(self.product_price) * float(VAT), 2
)
VAT = vat_rate if vat_rate is not None else self.product_vat
return round(
float(self.product_price) + float(self.product_price) * float(VAT), 2
)
class HostingOrder(AssignPermissionsMixin, models.Model):
@ -169,12 +163,8 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
def set_stripe_charge(self, stripe_charge):
self.stripe_charge_id = stripe_charge.id
if stripe_charge.source is None:
self.last4 = stripe_charge.payment_method_details.card.last4
self.cc_brand = stripe_charge.payment_method_details.card.brand
else:
self.last4 = stripe_charge.source.last4
self.cc_brand = stripe_charge.source.brand
self.last4 = stripe_charge.source.last4
self.cc_brand = stripe_charge.source.brand
self.save()
def set_subscription_id(self, subscription_id, cc_details):
@ -355,10 +345,7 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
args['period_start']).replace(tzinfo=pytz.utc),
period_end=datetime.utcfromtimestamp(
args['period_end']).replace(tzinfo=pytz.utc),
billing_reason=(
args['billing_reason']
if args['billing_reason'] is not None else ''
),
billing_reason=args['billing_reason'],
discount=args['discount'],
total=args['total'],
lines_data_count=args['lines_data_count'],
@ -676,11 +663,7 @@ class UserCardDetail(AssignPermissionsMixin, models.Model):
stripe_utils = StripeUtils()
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
cu = cus_response['response_object']
if stripe_source_id.startswith("pm"):
# card is a payment method
cu.invoice_settings.default_payment_method = stripe_source_id
else:
cu.default_source = stripe_source_id
cu.default_source = stripe_source_id
cu.save()
UserCardDetail.save_default_card_local(
stripe_api_cus_id, stripe_source_id
@ -738,45 +721,4 @@ class VATRates(AssignPermissionsMixin, models.Model):
currency_code = models.CharField(max_length=10)
rate = models.FloatField()
rate_type = models.TextField(blank=True, default='')
description = models.TextField(blank=True, default='')
class StripeTaxRate(AssignPermissionsMixin, models.Model):
tax_rate_id = models.CharField(max_length=100, unique=True)
jurisdiction = models.CharField(max_length=10)
inclusive = models.BooleanField(default=False)
display_name = models.CharField(max_length=100)
percentage = models.FloatField(default=0)
description = models.CharField(max_length=100)
class IncompletePaymentIntents(AssignPermissionsMixin, models.Model):
completed_at = models.DateTimeField(null=True)
created_at = models.DateTimeField(auto_now_add=True)
payment_intent_id = models.CharField(max_length=100)
request = models.TextField()
stripe_api_cus_id = models.CharField(max_length=30)
card_details_response = models.TextField()
stripe_subscription_id = models.CharField(max_length=100, null=True)
stripe_charge_id = models.CharField(max_length=100, null=True)
gp_details = models.TextField()
billing_address_data = models.TextField()
class IncompleteSubscriptions(AssignPermissionsMixin, models.Model):
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True)
subscription_id = models.CharField(max_length=100)
subscription_status = models.CharField(max_length=30)
name = models.CharField(max_length=50)
email = models.EmailField()
request = models.TextField()
stripe_api_cus_id = models.CharField(max_length=30)
card_details_response = models.TextField()
stripe_subscription_obj = models.TextField()
stripe_onetime_charge = models.TextField()
gp_details = models.TextField()
specs = models.TextField()
vm_template_id = models.PositiveIntegerField(default=0)
template = models.TextField()
billing_address_data = models.TextField()
description = models.TextField(blank=True, default='')

View file

@ -23,6 +23,7 @@
.hosting-dashboard .dashboard-container-head {
color: #fff;
margin-bottom: 60px;
}
.hosting-dashboard-item {

View file

@ -2,10 +2,4 @@
.orders-container .table > tbody > tr > td {
vertical-align: middle;
}
@media screen and (min-width:767px){
.dcl-text-right {
padding-right: 20px;
}
}

View file

@ -248,9 +248,6 @@
.dashboard-title-thin {
font-size: 22px;
}
.dashboard-greetings-thin {
font-size: 16px;
}
}
.btn-vm-invoice {
@ -318,11 +315,6 @@
font-size: 32px;
}
.dashboard-greetings-thin {
font-weight: 300;
font-size: 24px;
}
.dashboard-title-thin .un-icon {
height: 34px;
margin-right: 5px;
@ -419,9 +411,6 @@
.dashboard-title-thin {
font-size: 22px;
}
.dashboard-greetings-thin {
font-size: 16px;
}
.dashboard-title-thin .un-icon {
height: 22px;
width: 22px;

View file

@ -266,8 +266,8 @@ $( document ).ready(function() {
}
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
(cardPricing['ram'].value * window.ramUnitPrice) +
(cardPricing['storage'].value * window.ssdUnitPrice) +
window.vmBasePrice - window.discountAmount;
(cardPricing['storage'].value * window.ssdUnitPrice) -
window.discountAmount;
total = parseFloat(total.toFixed(2));
$("#total").text(total);
}

View file

@ -84,72 +84,68 @@ $(document).ready(function () {
var hasCreditcard = window.hasCreditcard || false;
if (!hasCreditcard && window.stripeKey) {
var stripe = Stripe(window.stripeKey);
if (window.pm_id) {
} else {
var element_style = {
fonts: [{
family: 'lato-light',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
}, {
family: 'lato-regular',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
}
],
locale: window.current_lan
};
var elements = stripe.elements(element_style);
var credit_card_text_style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-light', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#777'
}
},
invalid: {
iconColor: '#eb4d5c',
color: '#eb4d5c',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-regular', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#eb4d5c',
fontWeight: 400
}
}
};
var enter_ccard_text = "Enter your credit card number";
if (typeof window.enter_your_card_text !== 'undefined') {
enter_ccard_text = window.enter_your_card_text;
var element_style = {
fonts: [{
family: 'lato-light',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
}, {
family: 'lato-regular',
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
}
var cardNumberElement = elements.create('cardNumber', {
style: credit_card_text_style,
placeholder: enter_ccard_text
});
cardNumberElement.mount('#card-number-element');
var cardExpiryElement = elements.create('cardExpiry', {
style: credit_card_text_style
});
cardExpiryElement.mount('#card-expiry-element');
var cardCvcElement = elements.create('cardCvc', {
style: credit_card_text_style
});
cardCvcElement.mount('#card-cvc-element');
cardNumberElement.on('change', function (event) {
if (event.brand) {
setBrandIcon(event.brand);
],
locale: window.current_lan
};
var elements = stripe.elements(element_style);
var credit_card_text_style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-light', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#777'
}
});
},
invalid: {
iconColor: '#eb4d5c',
color: '#eb4d5c',
lineHeight: '25px',
fontWeight: 300,
fontFamily: "'lato-regular', sans-serif",
fontSize: '14px',
'::placeholder': {
color: '#eb4d5c',
fontWeight: 400
}
}
};
var enter_ccard_text = "Enter your credit card number";
if (typeof window.enter_your_card_text !== 'undefined') {
enter_ccard_text = window.enter_your_card_text;
}
var cardNumberElement = elements.create('cardNumber', {
style: credit_card_text_style,
placeholder: enter_ccard_text
});
cardNumberElement.mount('#card-number-element');
var cardExpiryElement = elements.create('cardExpiry', {
style: credit_card_text_style
});
cardExpiryElement.mount('#card-expiry-element');
var cardCvcElement = elements.create('cardCvc', {
style: credit_card_text_style
});
cardCvcElement.mount('#card-cvc-element');
cardNumberElement.on('change', function (event) {
if (event.brand) {
setBrandIcon(event.brand);
}
});
}
var submit_form_btn = $('#payment_button_with_creditcard');
@ -167,7 +163,7 @@ $(document).ready(function () {
if (parts.length === 2) return parts.pop().split(";").shift();
}
function submitBillingForm(pmId) {
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() + '" />');
@ -178,40 +174,11 @@ $(document).ready(function () {
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.append('<input type="hidden" name="id_payment_method" value="' + pmId + '" />');
billing_form.submit();
}
var $form_new = $('#payment-form-new');
$form_new.submit(payWithPaymentIntent);
window.result = "";
window.card = "";
function payWithPaymentIntent(e) {
e.preventDefault();
function stripePMHandler(paymentMethod) {
// Insert the token ID into the form so it gets submitted to the server
console.log(paymentMethod);
$('#id_payment_method').val(paymentMethod.id);
submitBillingForm(paymentMethod.id);
}
stripe.createPaymentMethod({
type: 'card',
card: cardNumberElement,
})
.then(function(result) {
// Handle result.error or result.paymentMethod
window.result = result;
if(result.error) {
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
console.log("created paymentMethod " + result.paymentMethod.id);
stripePMHandler(result.paymentMethod);
}
});
window.card = cardNumberElement;
}
$form_new.submit(payWithStripe_new);
function payWithStripe_new(e) {
e.preventDefault();
@ -230,7 +197,7 @@ $(document).ready(function () {
} else {
var process_text = "Processing";
if (typeof window.processing_text !== 'undefined') {
process_text = window.processing_text;
process_text = window.processing_text
}
$form_new.find('[type=submit]').html(process_text + ' <i class="fa fa-spinner fa-pulse"></i>');

View file

@ -92,117 +92,50 @@ $(document).ready(function() {
});
var create_vm_form = $('#virtual_machine_create_form');
if (window.isSubscription) {
create_vm_form.submit(function () {
$('#btn-create-vm').prop('disabled', true);
$.ajax({
url: create_vm_form.attr('action'),
type: 'POST',
data: create_vm_form.serialize(),
init: function () {
ok_btn = $('#createvm-modal-done-btn');
close_btn = $('#createvm-modal-close-btn');
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
},
success: function (data) {
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
if (data.showSCA) {
console.log("Show SCA");
var stripe = Stripe(data.STRIPE_PUBLISHABLE_KEY);
stripe.confirmCardPayment(data.payment_intent_secret).then(function (result) {
if (result.error) {
// Display error.message in your UI.
modal_btn.attr('href', data.error.redirect).removeClass('hide');
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
$('#createvm-modal-title').text(data.error.msg_title);
$('#createvm-modal-body').html(data.error.msg_body);
} else {
// The payment has succeeded. Display a success message.
modal_btn.attr('href', data.success.redirect).removeClass('hide');
fa_icon.attr('class', 'checkmark');
$('#createvm-modal-title').text(data.success.msg_title);
$('#createvm-modal-body').html(data.success.msg_body);
}
});
$('#3Dsecure-modal').show();
} else {
$('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').html(data.msg_body);
if (data.redirect) {
modal_btn.attr('href', data.redirect).removeClass('hide');
} else {
modal_btn.attr('href', "");
}
if (data.status === true) {
fa_icon.attr('class', 'checkmark');
} else {
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
}
}
},
error: function (xmlhttprequest, textstatus, message) {
create_vm_form.submit(function () {
$('#btn-create-vm').prop('disabled', true);
$.ajax({
url: create_vm_form.attr('action'),
type: 'POST',
data: create_vm_form.serialize(),
init: function(){
ok_btn = $('#createvm-modal-done-btn');
close_btn = $('#createvm-modal-close-btn');
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
},
success: function (data) {
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
$('#createvm-modal-title').text(data.msg_title);
$('#createvm-modal-body').html(data.msg_body);
if (data.redirect) {
modal_btn.attr('href', data.redirect).removeClass('hide');
} else {
modal_btn.attr('href', "");
}
if (data.status === true) {
fa_icon.attr('class', 'checkmark');
} else {
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
}
},
error: function (xmlhttprequest, textstatus, message) {
fa_icon = $('.modal-icon > .fa');
fa_icon.attr('class', 'fa fa-close');
if (typeof (create_vm_error_message) !== 'undefined') {
if (typeof(create_vm_error_message) !== 'undefined') {
$('#createvm-modal-body').text(create_vm_error_message);
}
$('#btn-create-vm').prop('disabled', false);
$('#createvm-modal-close-btn').removeClass('hide');
}
});
return false;
}
});
} else {
create_vm_form.submit(placeOrderPaymentIntent);
function placeOrderPaymentIntent(e) {
e.preventDefault();
var stripe = Stripe(window.stripeKey);
stripe.confirmCardPayment(
window.paymentIntentSecret,
{
payment_method: window.pm_id
}
).then(function(result) {
window.result = result;
fa_icon = $('.modal-icon > .fa');
modal_btn = $('#createvm-modal-done-btn');
if (result.error) {
// Display error.message in your UI.
modal_btn.attr('href', error_url).removeClass('hide');
fa_icon.attr('class', 'fa fa-close');
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
$('#createvm-modal-title').text(error_title);
$('#createvm-modal-body').html(result.error.message + " " + error_msg);
} else {
// The payment has succeeded
// Display a success message
modal_btn.attr('href', success_url).removeClass('hide');
fa_icon.attr('class', 'checkmark');
$('#createvm-modal-title').text(success_title);
$('#createvm-modal-body').html(success_msg);
}
});
}
}
return false;
});
$('#createvm-modal').on('hidden.bs.modal', function () {
$(this).find('.modal-footer .btn').addClass('hide');
});
// Toggle subscription and one-time payments div
$('#li-one-time-charges').click(function() {
console.log("li-one-time-charges clicked");
$('#subscriptions').hide();
$('#one-time-charges').show();
});
$('#li-subscriptions').click(function() {
console.log("li-subscriptions clicked");
$('#one-time-charges').hide();
$('#subscriptions').show();
});
})
});
window.onload = function () {

View file

@ -7,9 +7,6 @@
<div class="dashboard-container-head">
<h1 class="dashboard-title-thin">{% trans "My Dashboard" %}</h1>
</div>
<div style="color:#fff; font-size: 18px; font-weight:300; padding: 0 8px; margin-top: 30px; margin-bottom: 30px;">
{% trans "Welcome" %} {{request.user.name}}
</div>
<div class="hosting-dashboard-content">
<a href="{% url 'hosting:create_virtual_machine' %}" class="hosting-dashboard-item">
<h2>{% trans "Create VM" %}</h2>
@ -29,7 +26,7 @@
<img class="svg-img" src="{% static 'hosting/img/key.svg' %}">
</div>
</a>
<a href="{% url 'hosting:invoices' %}" class="hosting-dashboard-item">
<a href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' %}{% endif %}" class="hosting-dashboard-item">
<h2>{% trans "My Bills" %}</h2>
<div class="hosting-dashboard-image">
<img class="svg-img" src="{% static 'hosting/img/billing.svg' %}">

View file

@ -1,11 +1,7 @@
{% if messages %}
<ul class="list-unstyled msg-list">
{% for message in messages %}
{% if message.tags and 'error' in message.tags %}
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
{% elif message.tags %}
<div class="alert alert-{{message.tags}}">{{ message|safe }}</div>
{% endif %}
<div class="alert {% if message.tags and message.tags == 'error' %} alert-danger {% else %} alert-{{message.tags}} {% endif %}">{{ message|safe }}</div>
{% endfor %}
</ul>
{% endif %}

View file

@ -26,7 +26,7 @@
</li>
<li class="dropdown highlights-dropdown">
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
<i class="fa fa-fw fa-user"></i>&nbsp;&nbsp;{{request.user.username}}&nbsp;<span class="fa fa-fw fa-caret-down"></span>
<i class="fa fa-fw fa-user"></i>&nbsp;&nbsp;{{request.user.name}}&nbsp;<span class="fa fa-fw fa-caret-down"></span>
</a>
<ul id="g-account-menu" class="dropdown-menu" role="menu">
<li><a href="{% url 'hosting:logout' %}">{% trans "Logout"%}</a></li>

View file

@ -70,9 +70,6 @@
{{invoice.order.billing_address.postal_code}}<br>
{{invoice.order.billing_address.city}},
{{invoice.order.billing_address.country}}
{% if invoice.order.billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{invoice.order.billing_address.vat_number}}
{% endif %}
{% endif %}
</p>
</address>
@ -150,12 +147,8 @@
CHF</strong>
</p>
<p>
{% if vm.after_eu_vat_intro %}
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
{% else %}
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
</small>
{% endif %}
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}

View file

@ -15,116 +15,47 @@
<div class="dashboard-subtitle"></div>
</div>
<ul class="nav nav-tabs">
<li class="active" id="li-subscriptions"><a href="#">{% trans "Subscriptions" %}</a></li>
<li id="li-one-time-charges"><a href="#">{% trans "One-time payments" %}</a></li>
</ul>
<div class="subscriptions" id="subscriptions">
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "VM ID" %}</th>
<th>{% trans "IP Address" %}/{% trans "Product" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Period" %}</th>
<th>{% trans "Amount" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for inv_data in invs %}
{% for invoice in invoices %}
<tr>
{{ inv_data | get_line_item_from_stripe_invoice }}
<td class="xs-td-inline" data-header="{% trans 'VM ID' %}">{{ invoice.order.vm_id }}</td>
<td class="xs-td-inline" data-header="{% trans 'IP Address' %}">{{ ips|get_value_from_dict:invoice.invoice_number|join:"<br/>" }}</td>
{% with period|get_value_from_dict:invoice.invoice_number as period_to_show %}
<td class="xs-td-inline" data-header="{% trans 'Period' %}">{{ period_to_show.period_start | date:'Y-m-d' }} &mdash; {{ period_to_show.period_end | date:'Y-m-d' }}</td>
{% endwith %}
<td class="xs-td-inline" data-header="{% trans 'Amount' %}">{{ invoice.total_in_chf|floatformat:2|intcomma }}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:invoices' invoice.invoice_number %}">{% trans 'See Invoice' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if invs.has_other_pages %}
<ul class="pagination">
{% if invs.has_previous %}
{% if user_email %}
<li><a href="?page={{ invs.previous_page_number }}&user_email={{user_email}}">&laquo;</a></li>
{% else %}
<li><a href="?page={{ invs.previous_page_number }}">&laquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&laquo;</span></li>
{% endif %}
{% for i in invs.paginator.page_range %}
{% if invs.number == i %}
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
{% else %}
{% if user_email %}
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
{% else %}
<li><a href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if invs.has_next %}
{% if user_email %}
<li><a href="?page={{ invs.next_page_number }}&user_email={{user_email}}">&raquo;</a></li>
{% else %}
<li><a href="?page={{ invs.next_page_number }}">&raquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&raquo;</span></li>
{% endif %}
</ul>
{% endif %}
</div>
<div id="one-time-charges" class="one-time-charges" style="display: none;">
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "Product" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Amount" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ho, stripe_charge_data in invs_charge %}
<tr>
{{ ho.id | get_line_item_from_hosting_order_charge }}
</tr>
{% endfor %}
</tbody>
</table>
{% if invs_charge.has_other_pages %}
<ul class="pagination">
{% if invs_charge.has_previous %}
{% if user_email %}
<li><a href="?page={{ invs_charge.previous_page_number }}&user_email={{user_email}}">&laquo;</a></li>
{% else %}
<li><a href="?page={{ invs_charge.previous_page_number }}">&laquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&laquo;</span></li>
{% endif %}
{% for i in invs_charge.paginator.page_range %}
{% if invs_charge.number == i %}
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
{% else %}
{% if user_email %}
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
{% else %}
<li><a href="?page={{ i }}">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if invs_charge.has_next %}
{% if user_email %}
<li><a href="?page={{ invs_charge.next_page_number }}&user_email={{user_email}}">&raquo;</a></li>
{% else %}
<li><a href="?page={{ invs_charge.next_page_number }}">&raquo;</a></li>
{% endif %}
{% else %}
<li class="disabled"><span>&raquo;</span></li>
{% endif %}
</ul>
{% endif %}
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
{% endif %}
<span class="page-current">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
{% endif %}
</span>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -62,17 +62,11 @@
{{user.name}}<br>
{{order.billing_address.street_address}}, {{order.billing_address.postal_code}}<br>
{{order.billing_address.city}}, {{order.billing_address.country}}
{% if order.billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{order.billing_address.vat_number}}
{% endif %}
{% else %}
{% with request.session.billing_address_data as billing_address %}
{{billing_address.cardholder_name}}<br>
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
{{billing_address.city}}, {{billing_address.country}}
{% if billing_address.vat_number %}
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
{% endif %}
{% endwith %}
{% endif %}
</p>
@ -148,12 +142,7 @@
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
</p>
<p>
{% if vm.after_eu_vat_intro %}
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
{% else %}
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
</small>
{% endif %}
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
</p>
{% endif %}
@ -218,7 +207,7 @@
{% csrf_token %}
<div class="row">
<div class="col-sm-8">
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_price }} CHF/month.{% endblocktrans %}.</div>
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.</div>
</div>
<div class="col-sm-4 order-confirm-btn text-right">
<button class="btn choice-btn" id="btn-create-vm" data-href="{% url 'hosting:order-confirmation' %}" data-toggle="modal" data-target="#createvm-modal">

View file

@ -15,10 +15,6 @@
<div class="settings-container">
<div class="row">
<div class="col-sm-5 col-md-6 billing dcl-billing">
<h3><b>{%trans "My Username"%}</b></h3>
<hr class="top-hr">
<p>{{request.user.username}}</p>
<br>
<h3>{%trans "Billing Address" %}</h3>
<hr>
<form role="form" id="billing-form" method="post" action="" novalidate>
@ -26,16 +22,6 @@
{% for field in form %}
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
{% endfor %}
{% if form.instance.vat_number %}
{% if form.instance.vat_validation_status != "ch_vat" and form.instance.vat_validation_status != "not_needed" %}
{% if form.instance.vat_validation_status == "verified" %}
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
{% elif form.instance.vat_validation_status == "pending" %}
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
{% endif %}
{% endif %}
{% endif %}
<div class="form-group text-right">
<button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
</div>

View file

@ -81,10 +81,12 @@
</td>
<td>
{% if user_key.private_key %}
<a class="btn btn-default" style='color: #717274;' href="{{ user_key.private_key.url }}" download="{{user_key.private_key.name}}">
<form action="{{ user_key.private_key.url }}">
<button style="color: #717274" type="submit" class="btn btn-default">
<span class="pc-only">{% trans "Download" %}</span>
<span class="mob-only"><i class="fa fa-download"></i></span>
</a>
</button>
</form>
{% endif %}
</td>
</tr>

View file

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

View file

@ -51,7 +51,7 @@ urlpatterns = [
name='choice_ssh_keys'),
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
name='delete_ssh_key'),
url(r'delete_card/(?P<pk>[\w\-]+)/$', SettingsView.as_view(),
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
name='delete_card'),
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
name='create_ssh_key'),

View file

@ -1,10 +1,8 @@
import logging
import uuid
from datetime import datetime
from urllib.parse import quote
from time import sleep
import stripe
from django import forms
from django.conf import settings
from django.contrib import messages
@ -12,9 +10,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse_lazy, reverse
from django.db.models import Q
from django.http import (
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
)
@ -41,10 +37,8 @@ from stored_messages.settings import stored_messages_settings
from datacenterlight.cms_models import DCLCalculatorPluginModel
from datacenterlight.models import VMTemplate, VMPricing
from datacenterlight.utils import (
create_vm, get_cms_integration, check_otp, validate_vat_number
)
from hosting.models import UserCardDetail, StripeTaxRate
from datacenterlight.utils import create_vm, get_cms_integration, check_otp
from hosting.models import UserCardDetail
from membership.models import CustomUser, StripeCustomer
from opennebula_api.models import OpenNebulaManager
from opennebula_api.serializers import (
@ -56,11 +50,7 @@ from utils.forms import (
ResendActivationEmailForm
)
from utils.hosting_utils import get_all_public_keys
from utils.hosting_utils import (
get_vm_price_with_vat, get_vm_price_for_given_vat, HostingUtils,
get_vat_rate_for_country
)
from utils.ldap_manager import LdapManager
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
from utils.mailer import BaseEmail
from utils.stripe_utils import StripeUtils
from utils.tasks import send_plain_email_task
@ -75,7 +65,7 @@ from .forms import (
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct, MonthlyHostingBill
GenericProduct, MonthlyHostingBill, HostingBillLineItem
)
logger = logging.getLogger(__name__)
@ -388,7 +378,7 @@ class PasswordResetConfirmView(HostingContextMixin,
user = CustomUser.objects.get(pk=uid)
opennebula_client = OpenNebulaManager(
email=user.username,
email=user.email,
password=user.password,
)
@ -401,30 +391,21 @@ class PasswordResetConfirmView(HostingContextMixin,
if user is not None and default_token_generator.check_token(user,
token):
if form.is_valid():
ldap_manager = LdapManager()
new_password = form.cleaned_data['new_password2']
user.set_password(new_password)
user.save()
messages.success(request, _('Password has been reset.'))
# Make sure the user have an ldap account already
user.create_ldap_account(new_password)
# Change opennebula password
opennebula_client.change_user_password(user.password)
# We are changing password in ldap before changing in database because
# ldap have more chances of failure than local database
if ldap_manager.change_password(user.username, new_password):
user.set_password(new_password)
user.save()
messages.success(request, _('Password has been reset.'))
# Change opennebula password
opennebula_client.change_user_password(user.password)
return self.form_valid(form)
messages.error(
request, _('Password reset has not been successful.'))
form.add_error(None,
_('Password reset has not been successful.'))
return self.form_invalid(form)
return self.form_valid(form)
else:
messages.error(
request, _('Password reset has not been successful.'))
form.add_error(None,
_('Password reset has not been successful.'))
return self.form_invalid(form)
else:
error_msg = _('The reset password link is no longer valid.')
@ -480,7 +461,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
def delete(self, request, *args, **kwargs):
owner = self.request.user
manager = OpenNebulaManager(
email=owner.username,
email=owner.email,
password=owner.password
)
pk = self.kwargs.get('pk')
@ -534,7 +515,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
ssh_key.private_key.save(filename, content)
owner = self.request.user
manager = OpenNebulaManager(
email=owner.username,
email=owner.email,
password=owner.password
)
keys = get_all_public_keys(request.user)
@ -565,11 +546,9 @@ class SettingsView(LoginRequiredMixin, FormView):
stripe_customer = None
if hasattr(user, 'stripecustomer'):
stripe_customer = user.stripecustomer
stripe_utils = StripeUtils()
cards_list_request = stripe_utils.get_available_payment_methods(
stripe_customer
cards_list = UserCardDetail.get_all_cards_list(
stripe_customer=stripe_customer
)
cards_list = cards_list_request.get('response_object')
context.update({
'cards_list': cards_list,
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
@ -580,155 +559,108 @@ class SettingsView(LoginRequiredMixin, FormView):
def post(self, request, *args, **kwargs):
if 'card' in request.POST and request.POST['card'] is not '':
card_id = escape(request.POST['card'])
user_card_detail = UserCardDetail.objects.get(id=card_id)
UserCardDetail.set_default_card(
stripe_api_cus_id=request.user.stripecustomer.stripe_id,
stripe_source_id=card_id
stripe_source_id=user_card_detail.card_id
)
stripe_utils = StripeUtils()
card_details = stripe_utils.get_cards_details_from_payment_method(
card_id
)
if not card_details.get('response_object'):
logger.debug("Could not find card %s in stripe" % card_id)
messages.add_message(request, messages.ERROR,
_("Could not set a default card."))
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
card_details_response = card_details['response_object']
msg = _(
("Your {brand} card ending in {last4} set as "
"default card").format(
brand=card_details_response['brand'],
last4=card_details_response['last4']
brand=user_card_detail.brand,
last4=user_card_detail.last4
)
)
messages.add_message(request, messages.SUCCESS, msg)
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
if 'delete_card' in request.POST:
card = self.kwargs.get('pk')
stripe_utils = StripeUtils()
stripe_utils.dissociate_customer_card(
request.user.stripecustomer.stripe_id,
card
)
msg = _("Card deassociation successful")
messages.add_message(request, messages.SUCCESS, msg)
try:
card = UserCardDetail.objects.get(pk=self.kwargs.get('pk'))
if (request.user.has_perm(self.permission_required[0], card)
and
request.user
.stripecustomer
.usercarddetail_set
.count() > 1):
if card.card_id is not None:
stripe_utils = StripeUtils()
stripe_utils.dissociate_customer_card(
request.user.stripecustomer.stripe_id,
card.card_id
)
if card.preferred:
UserCardDetail.set_default_card_from_stripe(
request.user.stripecustomer.stripe_id
)
card.delete()
msg = _("Card deassociation successful")
messages.add_message(request, messages.SUCCESS, msg)
else:
msg = _("You are not permitted to do this operation")
messages.add_message(request, messages.ERROR, msg)
except UserCardDetail.DoesNotExist:
msg = _("The selected card does not exist")
messages.add_message(request, messages.ERROR, msg)
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
form = self.get_form()
if form.is_valid():
if 'billing-form' in request.POST:
current_billing_address = self.request.user.billing_addresses.last()
billing_address_data = form.cleaned_data
billing_address_data.update({
'user': self.request.user.id
})
billing_address_user_form = UserBillingAddressForm(
instance=self.request.user.billing_addresses.order_by('-id').first(),
instance=self.request.user.billing_addresses.first(),
data=billing_address_data)
billing_address = billing_address_user_form.save()
billing_address.stripe_tax_id = ''
billing_address.vat_number_validated_on = None
billing_address.vat_validation_status = ''
billing_address.save()
vat_number = billing_address_user_form.cleaned_data.get(
'vat_number').strip()
logger.debug("Vat number = %s" % vat_number)
if vat_number:
try:
stripe_customer = request.user.stripecustomer
except StripeCustomer.DoesNotExist as dne:
logger.debug(
"User %s does not have a stripecustomer. "
"Creating one." % request.user.email)
stripe_customer = StripeCustomer.get_or_create(
email=request.user.email,
token=None)
request.user.stripecustomer = stripe_customer
request.user.save()
validate_result = validate_vat_number(
stripe_customer_id=request.user.stripecustomer.stripe_id,
billing_address_id=billing_address.id,
is_user_ba=True
)
logger.debug("validate_result = %s" % str(validate_result))
if 'error' in validate_result and validate_result['error']:
messages.add_message(
request, messages.ERROR,
"VAT Number validation error: %s" % validate_result["error"],
extra_tags='error'
)
billing_address = current_billing_address
if billing_address:
billing_address.save()
email_data = {
'subject': "%s updated VAT number to %s but failed" %
(request.user.email, vat_number),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
validate_result.items()]),
}
else:
email_data = {
'subject': "%s updated VAT number to %s" % (
request.user.email, vat_number
),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in
validate_result.items()]),
}
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
send_plain_email_task.delay(email_data)
else:
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
billing_address_user_form.save()
msg = _("Billing address updated successfully")
messages.add_message(request, messages.SUCCESS, msg)
else:
id_payment_method = request.POST.get('id_payment_method', None)
token = form.cleaned_data.get('token')
stripe_utils = StripeUtils()
card_details = stripe_utils.get_cards_details_from_payment_method(
id_payment_method
card_details = stripe_utils.get_cards_details_from_token(
token
)
if not card_details.get('response_object'):
form.add_error("__all__", card_details.get('error'))
return self.render_to_response(self.get_context_data())
stripe_customer = StripeCustomer.get_or_create(
email=request.user.email, id_payment_method=id_payment_method
email=request.user.email, token=token
)
card = card_details['response_object']
acc_result = stripe_utils.associate_customer_card(
request.user.stripecustomer.stripe_id,
id_payment_method,
set_as_default=True
)
if acc_result['response_object'] is None:
msg = _(
'An error occurred while associating the card.'
' Details: {details}'.format(
details=acc_result['error']
)
)
if UserCardDetail.get_user_card_details(stripe_customer, card):
msg = _('You seem to have already added this card')
messages.add_message(request, messages.ERROR, msg)
return self.render_to_response(self.get_context_data())
preferred = False
if stripe_customer.usercarddetail_set.count() == 0:
preferred = True
UserCardDetail.create(
stripe_customer=stripe_customer,
last4=card['last4'],
brand=card['brand'],
fingerprint=card['fingerprint'],
exp_month=card['exp_month'],
exp_year=card['exp_year'],
card_id=card['card_id'],
preferred=preferred
)
msg = _(
"Successfully associated the card with your account"
)
messages.add_message(request, messages.SUCCESS, msg)
else:
acc_result = stripe_utils.associate_customer_card(
request.user.stripecustomer.stripe_id, token
)
if acc_result['response_object'] is None:
msg = _(
'An error occurred while associating the card.'
' Details: {details}'.format(
details=acc_result['error']
)
)
messages.add_message(request, messages.ERROR, msg)
return self.render_to_response(self.get_context_data())
preferred = False
if stripe_customer.usercarddetail_set.count() == 0:
preferred = True
UserCardDetail.create(
stripe_customer=stripe_customer,
last4=card['last4'],
brand=card['brand'],
fingerprint=card['fingerprint'],
exp_month=card['exp_month'],
exp_year=card['exp_year'],
card_id=card['card_id'],
preferred=preferred
)
msg = _(
"Successfully associated the card with your account"
)
messages.add_message(request, messages.SUCCESS, msg)
return self.render_to_response(self.get_context_data())
else:
billing_address_data = form.cleaned_data
@ -741,7 +673,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
form_class = BillingAddressForm
def get_form_kwargs(self):
current_billing_address = self.request.user.billing_addresses.last()
current_billing_address = self.request.user.billing_addresses.first()
form_kwargs = super(PaymentVMView, self).get_form_kwargs()
if not current_billing_address:
return form_kwargs
@ -753,7 +685,6 @@ class PaymentVMView(LoginRequiredMixin, FormView):
'city': current_billing_address.city,
'postal_code': current_billing_address.postal_code,
'country': current_billing_address.country,
'vat_number': current_billing_address.vat_number
}
})
return form_kwargs
@ -914,66 +845,40 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
user_vat_country = obj.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
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'),
vat_rate= (
user_country_vat_rate * 100
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
if obj.vm_pricing else 'default')
)
context['vm']["after_eu_vat_intro"] = (
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
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:
try:
manager = OpenNebulaManager(
email=owner.username, password=owner.password
email=owner.email, password=owner.password
)
vm = manager.get_vm(obj.vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
user_vat_country = obj.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
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'),
vat_rate=(
user_country_vat_rate * 100
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
if obj.vm_pricing else 'default')
)
context['vm']["after_eu_vat_intro"] = (
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
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['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
except WrongIdError:
messages.error(
self.request,
@ -1011,27 +916,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
context['cc_exp_year'] = card_detail.exp_year
context['cc_exp_month'] = '{:02d}'.format(card_detail.exp_month)
context['site_url'] = reverse('hosting:create_virtual_machine')
vm_specs = self.request.session.get('specs')
user_vat_country = (
self.request.session.get('billing_address_data').get("country")
)
user_country_vat_rate = get_vat_rate_for_country(user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
cpu=vm_specs['cpu'],
memory=vm_specs['memory'],
ssd_size=vm_specs['disk_size'],
pricing_name=vm_specs['pricing_name'],
vat_rate=user_country_vat_rate * 100
)
vm_specs["price"] = price
vm_specs["vat"] = vat
vm_specs["vat_percent"] = vat_percent
vm_specs["vat_country"] = user_vat_country
vm_specs["discount"] = discount
vm_specs["total_price"] = round(price + vat - discount['amount'],
2)
vm_specs["after_eu_vat_intro"] = True
context['vm'] = vm_specs
context['vm'] = self.request.session.get('specs')
return context
@method_decorator(decorators)
@ -1069,13 +954,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
billing_address_data = request.session.get('billing_address_data')
vm_template_id = template.get('id', 1)
stripe_api_cus_id = request.user.stripecustomer.stripe_id
logger.debug("template=%s specs=%s stripe_customer_id=%s "
"billing_address_data=%s vm_template_id=%s "
"stripe_api_cus_id=%s" % (
template, specs, stripe_customer_id, billing_address_data,
vm_template_id, stripe_api_cus_id)
)
if 'id_payment_method' in self.request.session:
if 'token' in self.request.session:
card_details = stripe_utils.get_cards_details_from_token(
request.session['token']
)
@ -1092,7 +971,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
)
if not ucd:
acc_result = stripe_utils.associate_customer_card(
stripe_api_cus_id, request.session['id_payment_method'],
stripe_api_cus_id, request.session['token'],
set_as_default=True
)
if acc_result['response_object'] is None:
@ -1133,8 +1012,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
cpu = specs.get('cpu')
memory = specs.get('memory')
disk_size = specs.get('disk_size')
amount_to_be_charged = specs.get('price')
discount = specs.get('discount')
amount_to_be_charged = specs.get('total_price')
plan_name = StripeUtils.get_stripe_plan_name(
cpu=cpu,
memory=memory,
@ -1153,61 +1031,11 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
amount=amount_to_be_charged,
name=plan_name,
stripe_plan_id=stripe_plan_id)
# Create StripeTaxRate if applicable to the user
stripe_tax_rate = None
if specs["vat_percent"] > 0:
try:
stripe_tax_rate = StripeTaxRate.objects.get(
description="VAT for %s" % specs["vat_country"]
)
print("Stripe Tax Rate exists")
except StripeTaxRate.DoesNotExist as dne:
print("StripeTaxRate does not exist")
tax_rate_obj = stripe.TaxRate.create(
display_name="VAT",
description="VAT for %s" % specs["vat_country"],
jurisdiction=specs["vat_country"],
percentage=specs["vat_percent"] * 100,
inclusive=False,
)
stripe_tax_rate = StripeTaxRate.objects.create(
display_name=tax_rate_obj.display_name,
description=tax_rate_obj.description,
jurisdiction=tax_rate_obj.jurisdiction,
percentage=tax_rate_obj.percentage,
inclusive=False,
tax_rate_id=tax_rate_obj.id
)
logger.debug("Created StripeTaxRate %s" %
stripe_tax_rate.tax_rate_id)
subscription_result = stripe_utils.subscribe_customer_to_plan(
stripe_api_cus_id,
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
coupon=(discount['stripe_coupon_id']
if 'name' in discount and
discount['name'] is not None and
'ipv6' in discount['name'].lower() and
discount['stripe_coupon_id']
else ""),
tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [],
default_payment_method=request.session['id_payment_method']
)
[{"plan": stripe_plan.get(
'response_object').stripe_plan_id}])
stripe_subscription_obj = subscription_result.get('response_object')
latest_invoice = stripe.Invoice.retrieve(stripe_subscription_obj.latest_invoice)
ret = stripe.PaymentIntent.confirm(
latest_invoice.payment_intent
)
if ret.status == 'requires_source_action' or ret.status == 'requires_action':
pi = stripe.PaymentIntent.retrieve(
latest_invoice.payment_intent
)
context = {
'sid': stripe_subscription_obj.id,
'payment_intent_secret': pi.client_secret,
'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY,
'showSCA': True
}
return JsonResponse(context)
# Check if the subscription was approved and is active
if (stripe_subscription_obj is None or
stripe_subscription_obj.status != 'active'):
@ -1246,7 +1074,6 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
user = {
'name': self.request.user.name,
'email': self.request.user.email,
'username': self.request.user.username,
'pass': self.request.user.password,
'request_scheme': request.scheme,
'request_host': request.get_host(),
@ -1290,7 +1117,7 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
return super(OrdersHostingListView, self).get(request, *args, **kwargs)
class InvoiceListView(LoginRequiredMixin, TemplateView):
class InvoiceListView(LoginRequiredMixin, ListView):
template_name = "hosting/invoices.html"
login_url = reverse_lazy('hosting:login')
context_object_name = "invoices"
@ -1298,14 +1125,10 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
ordering = '-created'
def get_context_data(self, **kwargs):
page = self.request.GET.get('page', 1)
context = super(InvoiceListView, self).get_context_data(**kwargs)
invs_page = None
invs_page_charges = None
if ('user_email' in self.request.GET
and self.request.user.email == settings.ADMIN_EMAIL):
user_email = self.request.GET['user_email']
context['user_email'] = '%s' % quote(user_email)
logger.debug(
"user_email = {}".format(user_email)
)
@ -1314,67 +1137,54 @@ class InvoiceListView(LoginRequiredMixin, TemplateView):
except CustomUser.DoesNotExist as dne:
logger.debug("User does not exist")
cu = self.request.user
invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id,
count=100,
status='paid')
paginator = Paginator(invs.data, 10)
try:
invs_page = paginator.page(page)
except PageNotAnInteger:
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
hosting_orders = HostingOrder.objects.filter(
customer=cu.stripecustomer).filter(
Q(subscription_id=None) | Q(subscription_id='')
).order_by('-created_at')
stripe_chgs = []
for ho in hosting_orders:
stripe_chgs.append({ho.id: stripe.Charge.retrieve(ho.stripe_charge_id)})
paginator_charges = Paginator(stripe_chgs, 10)
try:
invs_page_charges = paginator_charges.page(page)
except PageNotAnInteger:
invs_page_charges = paginator_charges.page(1)
except EmptyPage:
invs_page_charges = paginator_charges.page(paginator_charges.num_pages)
mhbs = MonthlyHostingBill.objects.filter(customer__user=cu)
else:
mhbs = MonthlyHostingBill.objects.filter(
customer__user=self.request.user
)
ips_dict = {}
line_item_period_dict = {}
for mhb in mhbs:
try:
invs = stripe.Invoice.list(
customer=self.request.user.stripecustomer.stripe_id,
count=100
)
paginator = Paginator(invs.data, 10)
try:
invs_page = paginator.page(page)
except PageNotAnInteger:
invs_page = paginator.page(1)
except EmptyPage:
invs_page = paginator.page(paginator.num_pages)
hosting_orders = HostingOrder.objects.filter(
customer=self.request.user.stripecustomer).filter(
Q(subscription_id=None) | Q(subscription_id='')
).order_by('-created_at')
stripe_chgs = []
for ho in hosting_orders:
stripe_chgs.append(
{ho: stripe.Charge.retrieve(ho.stripe_charge_id)})
paginator_charges = Paginator(stripe_chgs, 10)
try:
invs_page_charges = paginator_charges.page(page)
except PageNotAnInteger:
invs_page_charges = paginator_charges.page(1)
except EmptyPage:
invs_page_charges = paginator_charges.page(
paginator_charges.num_pages)
except Exception as ex:
logger.error(str(ex))
invs_page = None
context["invs"] = invs_page
context["invs_charge"] = invs_page_charges
vm_detail = VMDetail.objects.get(vm_id=mhb.order.vm_id)
ips_dict[mhb.invoice_number] = [vm_detail.ipv6, vm_detail.ipv4]
all_line_items = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb)
for line_item in all_line_items:
if line_item.get_item_detail_str() != "":
line_item_period_dict[mhb.invoice_number] = {
"period_start": line_item.period_start,
"period_end": line_item.period_end
}
break
except VMDetail.DoesNotExist as dne:
ips_dict[mhb.invoice_number] = ['--']
logger.debug("VMDetail for {} doesn't exist".format(
mhb.order.vm_id
))
context['ips'] = ips_dict
context['period'] = line_item_period_dict
return context
def get_queryset(self):
user = self.request.user
if ('user_email' in self.request.GET
and self.request.user.email == settings.ADMIN_EMAIL):
user_email = self.request.GET['user_email']
logger.debug(
"user_email = {}".format(user_email)
)
try:
cu = CustomUser.objects.get(email=user_email)
except CustomUser.DoesNotExist as dne:
logger.debug("User does not exist")
cu = self.request.user
self.queryset = MonthlyHostingBill.objects.filter(customer__user=cu)
else:
self.queryset = MonthlyHostingBill.objects.filter(
customer__user=self.request.user
)
return super(InvoiceListView, self).get_queryset()
@method_decorator(decorators)
def get(self, request, *args, **kwargs):
return super(InvoiceListView, self).get(request, *args, **kwargs)
@ -1423,67 +1233,41 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
context['vm'] = vm_detail.__dict__
context['vm']['name'] = '{}-{}'.format(
context['vm']['configuration'], context['vm']['vm_id'])
user_vat_country = obj.order.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
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.order.vm_pricing.name
if obj.order.vm_pricing else 'default'),
vat_rate=(
user_country_vat_rate * 100
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
if obj.order.vm_pricing else 'default')
)
context['vm']["after_eu_vat_intro"] = (
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
)
context['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
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 VMDetail.DoesNotExist:
# fallback to get it from the infrastructure
try:
manager = OpenNebulaManager(
email=self.request.user.username,
email=self.request.user.email,
password=self.request.user.password
)
vm = manager.get_vm(vm_id)
context['vm'] = VirtualMachineSerializer(vm).data
user_vat_country = obj.order.billing_address.country
user_country_vat_rate = get_vat_rate_for_country(
user_vat_country)
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
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.order.vm_pricing.name
if obj.order.vm_pricing else 'default'),
vat_rate=(
user_country_vat_rate * 100
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else settings.PRE_EU_VAT_RATE
)
if obj.order.vm_pricing else 'default')
)
context['vm']["after_eu_vat_intro"] = (
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
else False
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['vm']["price"] = price
context['vm']["vat"] = vat
context['vm']["vat_percent"] = vat_percent
context['vm']["vat_country"] = user_vat_country
context['vm']["discount"] = discount
context['vm']["total_price"] = round(
price + vat - discount['amount'], 2)
except TypeError:
logger.error("Type error. Probably we "
"came from a generic product. "
@ -1536,7 +1320,7 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
try:
queryset = manager.get_vms()
@ -1697,7 +1481,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
owner = self.request.user
vm = None
manager = OpenNebulaManager(
email=owner.username,
email=owner.email,
password=owner.password
)
vm_id = self.kwargs.get('pk')
@ -1741,28 +1525,13 @@ class VirtualMachineView(LoginRequiredMixin, View):
context = None
try:
serializer = VirtualMachineSerializer(vm)
hosting_order = HostingOrder.objects.get(
vm_id=serializer.data['vm_id']
)
inv_url = None
if hosting_order.subscription_id:
stripe_obj = stripe.Invoice.list(
subscription=hosting_order.subscription_id,
count=1
)
inv_url = stripe_obj.data[0].hosted_invoice_url
elif hosting_order.stripe_charge_id:
stripe_obj = stripe.Charge.retrieve(
hosting_order.stripe_charge_id
)
inv_url = stripe_obj.receipt_url
context = {
'virtual_machine': serializer.data,
'order': hosting_order,
'keys': UserHostingKey.objects.filter(user=request.user),
'inv_url': inv_url
'order': HostingOrder.objects.get(
vm_id=serializer.data['vm_id']
),
'keys': UserHostingKey.objects.filter(user=request.user)
}
except Exception as ex:
logger.debug("Exception generated {}".format(str(ex)))
messages.error(self.request,
@ -1781,7 +1550,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
vm = self.get_object()
manager = OpenNebulaManager(
email=owner.username,
email=owner.email,
password=owner.password
)
try:
@ -1794,7 +1563,6 @@ class VirtualMachineView(LoginRequiredMixin, View):
# Cancel Stripe subscription
stripe_utils = StripeUtils()
hosting_order = None
stripe_subscription_obj = None
try:
hosting_order = HostingOrder.objects.get(
vm_id=vm.id
@ -1809,7 +1577,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
error_msg = result.get('error')
logger.error(
'Error canceling subscription for {user} and vm id '
'{vm_id}'.format(user=owner.username, vm_id=vm.id)
'{vm_id}'.format(user=owner.email, vm_id=vm.id)
)
logger.error(error_msg)
admin_email_body['stripe_error_msg'] = error_msg
@ -1831,7 +1599,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
response['text'] = str(_('Error terminating VM')) + str(vm.id)
else:
for t in range(settings.MAX_TIME_TO_WAIT_FOR_VM_TERMINATE):
for t in range(15):
try:
manager.get_vm(vm.id)
except WrongIdError:
@ -1854,10 +1622,6 @@ class VirtualMachineView(LoginRequiredMixin, View):
)
break
else:
logger.debug(
'Sleeping 2 seconds for terminate action on VM %s' %
vm.id
)
sleep(2)
if not response['status']:
response['text'] = str(_("VM terminate action timed out. "
@ -1885,33 +1649,19 @@ class VirtualMachineView(LoginRequiredMixin, View):
email.send()
admin_email_body.update(response)
admin_email_body["customer_email"] = owner.email
admin_email_body["customer_username"] = owner.username
admin_email_body["VM_ID"] = vm.id
admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if
hosting_order is not None
else "unknown")
content = ""
total_amount = 0
if stripe_subscription_obj:
for line_item in stripe_subscription_obj["items"]["data"]:
total_amount += (line_item["quantity"] *
line_item.plan["amount"])
content += " %s => %s x %s => %s\n" % (
line_item.plan["name"], line_item["quantity"],
line_item.plan["amount"]/100,
(line_item["quantity"] * line_item.plan["amount"])/100
)
admin_email_body["subscription_amount"] = total_amount/100
admin_email_body["subscription_detail"] = content
admin_msg_sub = "VM and Subscription for VM {} and user: {}, {}".format(
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
vm.id,
owner.email, owner.username
owner.email
)
email_to_admin_data = {
'subject': ("Deleted " if response['status']
else "ERROR deleting ") + admin_msg_sub,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': ['dcl-orders@ungleich.ch'],
'to': ['info@ungleich.ch'],
'body': "\n".join(
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
}
@ -1951,7 +1701,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin,
context = super(DetailView, self).get_context_data(**kwargs)
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# Get vms
queryset = manager.get_vms()

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-11-28 07:19
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('membership', '0009_deleteduser'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='import_stripe_bill_remark',
field=models.TextField(default='', help_text='Indicates any issues while importing stripe bills'),
),
]

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-18 10:50
from __future__ import unicode_literals
from django.db import migrations, models
import membership.models
class Migration(migrations.Migration):
dependencies = [
('membership', '0010_customuser_import_stripe_bill_remark'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='in_ldap',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='username',
field=models.CharField(max_length=60, null=True, unique=True),
),
migrations.AlterField(
model_name='customuser',
name='name',
field=models.CharField(max_length=50, validators=[membership.models.validate_name]),
),
]

View file

@ -1,8 +1,5 @@
import logging
import random
import unicodedata
from datetime import datetime
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
@ -10,17 +7,13 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.validators import RegexValidator
from django.db import models, IntegrityError
from django.db import models
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from utils.mailer import BaseEmail
from utils.mailer import DigitalGlarusRegistrationMailer
from utils.stripe_utils import StripeUtils
from utils.ldap_manager import LdapManager
logger = logging.getLogger(__name__)
REGISTRATION_MESSAGE = {'subject': "Validation mail",
'message': 'Please validate Your account under this link '
@ -49,7 +42,6 @@ class MyUserManager(BaseUserManager):
user.is_admin = False
user.set_password(password)
user.save(using=self._db)
user.create_ldap_account(password)
return user
def create_superuser(self, email, name, password):
@ -71,54 +63,13 @@ def get_validation_slug():
return make_password(None)
def get_first_and_last_name(full_name):
first_name, *last_name = full_name.split(" ")
last_name = " ".join(last_name)
return first_name, last_name
def assign_username(user):
if not user.username:
ldap_manager = LdapManager()
# Try to come up with a username
first_name, last_name = get_first_and_last_name(user.name)
user.username = unicodedata.normalize('NFKD', first_name + last_name)
user.username = "".join([char for char in user.username if char.isalnum()]).lower()
exist = True
while exist:
# Check if it exists
exist, entries = ldap_manager.check_user_exists(user.username)
if exist:
# If username exists in ldap, come up with a new user name and check it again
user.username = user.username + str(random.randint(0, 2 ** 10))
else:
# If username does not exists in ldap, try to save it in database
try:
user.save()
except IntegrityError:
# If username exists in database then come up with a new username
user.username = user.username + str(random.randint(0, 2 ** 10))
exist = True
def validate_name(value):
valid_chars = [char for char in value if (char.isalpha() or char == "-" or char == " ")]
if len(valid_chars) < len(value):
raise ValidationError(
_('%(value)s is not a valid name. A valid name can only include letters, spaces or -'),
params={'value': value},
)
class CustomUser(AbstractBaseUser, PermissionsMixin):
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
site = models.ForeignKey(Site, default=1)
name = models.CharField(max_length=50, validators=[validate_name])
name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
username = models.CharField(max_length=60, unique=True, null=True)
validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0)
in_ldap = models.BooleanField(default=False)
# By default, we initialize the validation_slug with appropriate value
# This is required for User(page) admin
validation_slug = models.CharField(
@ -131,10 +82,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
help_text=_(
'Designates whether the user can log into this admin site.'),
)
import_stripe_bill_remark = models.TextField(
default="",
help_text="Indicates any issues while importing stripe bills"
)
objects = MyUserManager()
@ -213,38 +160,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
# The user is identified by their email address
return self.email
def create_ldap_account(self, password):
# create ldap account for user if it does not exists already.
if self.in_ldap:
return
assign_username(self)
ldap_manager = LdapManager()
try:
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.username)
except Exception:
logger.exception("Exception occur while searching for user in LDAP")
else:
if not user_exists_in_ldap:
# IF no ldap account
first_name, last_name = get_first_and_last_name(self.name)
if not last_name:
last_name = first_name
ldap_manager.create_user(self.username, password=password,
firstname=first_name, lastname=last_name,
email=self.email)
else:
# User exists already in LDAP, but with a dummy credential
# We are here implies that the user has successfully
# authenticated against Django db, and a corresponding user
# exists in LDAP.
# We just update the LDAP credentials once again, assuming it
# was set to a dummy value while migrating users from Django to
# LDAP
ldap_manager.change_password(self.username, password)
self.in_ldap = True
self.save()
def __str__(self): # __unicode__ on Python 2
return self.email
@ -277,7 +192,7 @@ class StripeCustomer(models.Model):
return "%s - %s" % (self.stripe_id, self.user.email)
@classmethod
def create_stripe_api_customer(cls, email=None, id_payment_method=None,
def create_stripe_api_customer(cls, email=None, token=None,
customer_name=None):
"""
This method creates a Stripe API customer with the given
@ -288,8 +203,7 @@ class StripeCustomer(models.Model):
stripe user.
"""
stripe_utils = StripeUtils()
stripe_data = stripe_utils.create_customer(
id_payment_method, email, customer_name)
stripe_data = stripe_utils.create_customer(token, email, customer_name)
if stripe_data.get('response_object'):
stripe_cus_id = stripe_data.get('response_object').get('id')
return stripe_cus_id
@ -297,7 +211,7 @@ class StripeCustomer(models.Model):
return None
@classmethod
def get_or_create(cls, email=None, token=None, id_payment_method=None):
def get_or_create(cls, email=None, token=None):
"""
Check if there is a registered stripe customer with that email
or create a new one

View file

@ -154,8 +154,6 @@ class OpenNebulaManager():
protocol=settings.OPENNEBULA_PROTOCOL)
)
raise ConnectionRefusedError
except Exception as ex:
logger.error(str(ex))
def _get_user_pool(self):
try:
@ -429,12 +427,8 @@ class OpenNebulaManager():
template_id = int(template_id)
try:
template_pool = self._get_template_pool()
if template_id in settings.UPDATED_TEMPLATES_DICT.keys():
template_id = settings.UPDATED_TEMPLATES_DICT[template_id]
return template_pool.get_by_id(template_id)
except Exception as ex:
logger.debug("Template Id we are looking for : %s" % template_id)
logger.error(str(ex))
except:
raise ConnectionRefusedError
def create_template(self, name, cores, memory, disk_size, core_price,
@ -491,15 +485,9 @@ class OpenNebulaManager():
)
def change_user_password(self, passwd_hash):
if type(self.opennebula_user) == int:
logger.debug("opennebula_user is int and has value = %s" %
self.opennebula_user)
else:
logger.debug("opennebula_user is object and corresponding id is %s"
% self.opennebula_user.id)
self.oneadmin_client.call(
oca.User.METHODS['passwd'],
self.opennebula_user if type(self.opennebula_user) == int else self.opennebula_user.id,
self.opennebula_user.id,
passwd_hash
)

View file

@ -86,7 +86,7 @@ class VirtualMachineSerializer(serializers.Serializer):
}
try:
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password,
)
opennebula_id = manager.create_vm(template_id=template_id,

View file

@ -19,7 +19,7 @@ class VmCreateView(generics.ListCreateAPIView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -42,7 +42,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_queryset(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -54,7 +54,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def get_object(self):
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable
@ -66,7 +66,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
def perform_destroy(self, instance):
owner = self.request.user
manager = OpenNebulaManager(email=owner.username,
manager = OpenNebulaManager(email=owner.email,
password=owner.password)
# We may have ConnectionRefusedError if we don't have a
# connection to OpenNebula. For now, we raise ServiceUnavailable

View file

@ -1,21 +0,0 @@
#!/bin/sh
# Nico Schottelius, 2021-12-17
current=$(git describe --dirty)
last_tag=$(git describe --tags --abbrev=0)
registry=harbor.ungleich.svc.p10.k8s.ooo/ungleich-public
image_url=$registry/dynamicweb:${current}
if echo $current | grep -q -e 'dirty$'; then
echo Refusing to release a dirty tree build
exit 1
fi
if [ "$current" != "$last_tag" ]; then
echo "Last tag ($last_tag) is not current version ($current)"
echo "Only release proper versions"
exit 1
fi
docker tag dynamicweb:${current} ${image_url}
docker push ${image_url}

View file

@ -1,2 +1 @@
base-devel
libmemcached

View file

@ -23,9 +23,9 @@ django-classy-tags==0.7.2
django-cms==3.2.5
django-compressor==2.0
django-debug-toolbar==1.4
python-dotenv==0.10.3
django-dotenv==1.4.1
django-extensions==1.6.7
django-filer==2.1.2
django-filer==1.2.0
django-filter==0.13.0
django-formtools==1.0
django-guardian==1.4.4
@ -63,7 +63,6 @@ djangocms-text-ckeditor==2.9.3
djangocms-video==1.0.0
easy-thumbnails==2.3
html5lib==0.9999999
ldap3==2.6.1
lxml==3.6.0
model-mommy==1.2.6
phonenumbers==7.4.0
@ -79,11 +78,11 @@ requests==2.10.0
rjsmin==1.0.12
six==1.10.0
sqlparse==0.1.19
stripe==2.41.0
stripe==1.33.0
wheel==0.29.0
django-admin-honeypot==1.0.0
coverage==4.3.4
git+https://github.com/ungleich/python-oca.git#egg=oca
git+https://github.com/ungleich/python-oca.git#egg=python-oca
djangorestframework==3.6.3
flake8==3.3.0
python-memcached==1.58

View file

@ -134,6 +134,8 @@
digitalglarus.ch<br/>
hack4lgarus.ch<br/>
ipv6onlyhosting.com<br/>
ipv6onlyhosting.ch<br/>
ipv6onlyhosting.net<br/>
django-hosting.ch<br/>
rails-hosting.ch<br/>
node-hosting.ch<br/>

View file

@ -1,13 +0,0 @@
import logging
from django.contrib.auth.backends import ModelBackend
logger = logging.getLogger(__name__)
class MyLDAPBackend(ModelBackend):
def authenticate(self, username=None, password=None, **kwargs):
user = super().authenticate(username, password, **kwargs)
if user:
user.create_ldap_account(password)
return user

View file

@ -241,6 +241,7 @@ COUNTRIES = (
('ZM', _('Zambia')),
('ZR', _('Zaire')),
('ZW', _('Zimbabwe')),
('ZZ', _('Unknown or unspecified country')),
)

View file

@ -124,14 +124,13 @@ class BillingAddressForm(forms.ModelForm):
class Meta:
model = BillingAddress
fields = ['cardholder_name', 'street_address',
'city', 'postal_code', 'country', 'vat_number']
'city', 'postal_code', 'country']
labels = {
'cardholder_name': _('Cardholder Name'),
'street_address': _('Street Address'),
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'VAT Number': _('VAT Number')
}
@ -143,7 +142,7 @@ class BillingAddressFormSignup(BillingAddressForm):
class Meta:
model = BillingAddress
fields = ['name', 'email', 'cardholder_name', 'street_address',
'city', 'postal_code', 'country', 'vat_number']
'city', 'postal_code', 'country']
labels = {
'name': 'Name',
'email': _('Email'),
@ -152,7 +151,6 @@ class BillingAddressFormSignup(BillingAddressForm):
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'vat_number': _('VAT Number')
}
def clean_email(self):
@ -175,14 +173,13 @@ class UserBillingAddressForm(forms.ModelForm):
class Meta:
model = UserBillingAddress
fields = ['cardholder_name', 'street_address',
'city', 'postal_code', 'country', 'user', 'vat_number']
'city', 'postal_code', 'country', 'user']
labels = {
'cardholder_name': _('Cardholder Name'),
'street_address': _('Street Building'),
'city': _('City'),
'postal_code': _('Postal Code'),
'Country': _('Country'),
'vat_number': _('VAT Number'),
}

View file

@ -1,10 +1,7 @@
import decimal
import logging
import math
import subprocess
from django.conf import settings
from oca.pool import WrongIdError
from datacenterlight.models import VMPricing
@ -81,55 +78,12 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(disk_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE))
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
return round(float(price), 2)
def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
pricing_name='default', vat_rate=0):
try:
pricing = VMPricing.objects.get(name=pricing_name)
except Exception as ex:
logger.error(
"Error getting VMPricing object for {pricing_name}."
"Details: {details}".format(
pricing_name=pricing_name, details=str(ex)
)
)
return None
price = (
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE)
)
discount_name = pricing.discount_name
discount_amount = round(float(pricing.discount_amount), 2)
vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01)
vat_percent = vat_rate
cents = decimal.Decimal('.01')
price = price.quantize(cents, decimal.ROUND_HALF_UP)
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01))
discount_amount_with_vat = discount_amount_with_vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': discount_name,
'amount': discount_amount,
'amount_with_vat': round(float(discount_amount_with_vat), 2),
'stripe_coupon_id': pricing.stripe_coupon_id
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
pricing_name='default'):
"""
@ -159,8 +113,7 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
(decimal.Decimal(memory) * pricing.ram_unit_price) +
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
decimal.Decimal(settings.VM_BASE_PRICE)
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
)
if pricing.vat_inclusive:
vat = decimal.Decimal(0)
@ -174,8 +127,7 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
discount = {
'name': pricing.discount_name,
'amount': round(float(pricing.discount_amount), 2),
'stripe_coupon_id': pricing.stripe_coupon_id
'amount': round(float(pricing.discount_amount), 2)
}
return (round(float(price), 2), round(float(vat), 2),
round(float(vat_percent), 2), discount)
@ -212,16 +164,6 @@ def get_vat_rate_for_country(country):
return 0
def get_ip_addresses(vm_id):
try:
vm_detail = VMDetail.objects.get(vm_id=vm_id)
return "%s <br/>%s" % (vm_detail.ipv6, vm_detail.ipv4)
except VMDetail.DoesNotExist as dne:
logger.error(str(dne))
logger.error("VMDetail for %s does not exist" % vm_id)
return "--"
class HostingUtils:
@staticmethod
def clear_items_from_list(from_list, items_list):

View file

@ -1,282 +0,0 @@
import base64
import hashlib
import random
import ldap3
import logging
import unicodedata
from django.conf import settings
logger = logging.getLogger(__name__)
class LdapManager:
__instance = None
def __new__(cls):
if LdapManager.__instance is None:
LdapManager.__instance = object.__new__(cls)
return LdapManager.__instance
def __init__(self):
"""
Initialize the LDAP subsystem.
"""
self.rng = random.SystemRandom()
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER)
def get_admin_conn(self):
"""
Return a bound :class:`ldap3.Connection` instance which has write
permissions on the dn in which the user accounts reside.
"""
conn = self.get_conn(user=settings.LDAP_ADMIN_DN,
password=settings.LDAP_ADMIN_PASSWORD,
raise_exceptions=True)
conn.bind()
return conn
def get_conn(self, **kwargs):
"""
Return an unbound :class:`ldap3.Connection` which talks to the configured
LDAP server.
The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and
can be used to set *user*, *password* and other useful arguments.
"""
return ldap3.Connection(self.server, **kwargs)
def _ssha_password(self, password):
"""
Apply the SSHA password hashing scheme to the given *password*.
*password* must be a :class:`bytes` object, containing the utf-8
encoded password.
Return a :class:`bytes` object containing ``ascii``-compatible data
which can be used as LDAP value, e.g. after armoring it once more using
base64 or decoding it to unicode from ``ascii``.
"""
SALT_BYTES = 15
sha1 = hashlib.sha1()
salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little")
sha1.update(password)
sha1.update(salt)
digest = sha1.digest()
passwd = b"{SSHA}" + base64.b64encode(digest + salt)
return passwd
def create_user(self, user, password, firstname, lastname, email):
conn = self.get_admin_conn()
uidNumber = self._get_max_uid() + 1
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
user_exists = True
while user_exists:
user_exists, _ = self.check_user_exists(
"",
'(&(objectClass=inetOrgPerson)(objectClass=posixAccount)'
'(objectClass=top)(uidNumber={uidNumber}))'.format(
uidNumber=uidNumber
)
)
if user_exists:
logger.debug(
"{uid} exists. Trying next.".format(uid=uidNumber)
)
uidNumber += 1
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
self._set_max_uid(uidNumber)
try:
uid = user
conn.add("uid={uid},{customer_dn}".format(
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
),
["inetOrgPerson", "posixAccount", "ldapPublickey"],
{
"uid": [uid],
"sn": [lastname.encode("utf-8")],
"givenName": [firstname.encode("utf-8")],
"cn": [uid],
"displayName": ["{} {}".format(firstname, lastname).encode("utf-8")],
"uidNumber": [str(uidNumber)],
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
"loginShell": ["/bin/bash"],
"homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))],
"mail": email.encode("utf-8"),
"userPassword": [self._ssha_password(
password.encode("utf-8")
)]
}
)
logger.debug('Created user %s %s' % (user.encode('utf-8'),
uidNumber))
except Exception as ex:
logger.debug('Could not create user %s' % user.encode('utf-8'))
logger.error("Exception: " + str(ex))
raise Exception(ex)
finally:
conn.unbind()
def change_password(self, uid, new_password):
"""
Changes the password of the user identified by user_dn
:param uid: str The uid that identifies the user
:param new_password: str The new password string
:return: True if password was changed successfully False otherwise
"""
conn = self.get_admin_conn()
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
)
return_val = False
if user_exists:
try:
return_val = conn.modify(
entries[0].entry_dn,
{
"userpassword": (
ldap3.MODIFY_REPLACE,
[self._ssha_password(new_password.encode("utf-8"))]
)
}
)
except Exception as ex:
logger.error("Exception: " + str(ex))
else:
logger.error("User {} not found".format(uid))
conn.unbind()
return return_val
def change_user_details(self, uid, details):
"""
Updates the user details as per given values in kwargs of the user
identified by user_dn.
Assumes that all attributes passed in kwargs are valid.
:param uid: str The uid that identifies the user
:param details: dict A dictionary containing the new values
:return: True if user details were updated successfully False otherwise
"""
conn = self.get_admin_conn()
# Make sure the user exists first to change his/her details
user_exists, entries = self.check_user_exists(
uid=uid,
search_base=settings.ENTIRE_SEARCH_BASE
)
return_val = False
if user_exists:
details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for
k, v in details.items()}
try:
return_val = conn.modify(entries[0].entry_dn, details_dict)
msg = "success"
except Exception as ex:
msg = str(ex)
logger.error("Exception: " + msg)
finally:
conn.unbind()
else:
msg = "User {} not found".format(uid)
logger.error(msg)
conn.unbind()
return return_val, msg
def check_user_exists(self, uid, search_filter="", attributes=None,
search_base=settings.LDAP_CUSTOMER_DN, search_attr="uid"):
"""
Check if the user with the given uid exists in the customer group.
:param uid: str representing the user
:param search_filter: str representing the filter condition to find
users. If its empty, the search finds the user with
the given uid.
:param attributes: list A list of str representing all the attributes
to be obtained in the result entries
:param search_base: str
:return: tuple (bool, [ldap3.abstract.entry.Entry ..])
A bool indicating if the user exists
A list of all entries obtained in the search
"""
conn = self.get_admin_conn()
entries = []
try:
result = conn.search(
search_base=search_base,
search_filter=search_filter if len(search_filter) > 0 else
'(uid={uid})'.format(uid=uid),
attributes=attributes
)
entries = conn.entries
finally:
conn.unbind()
return result, entries
def delete_user(self, uid):
"""
Deletes the user with the given uid from ldap
:param uid: str representing the user
:return: True if the delete was successful False otherwise
"""
conn = self.get_admin_conn()
try:
return_val = conn.delete(
("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid),
)
msg = "success"
except Exception as ex:
msg = str(ex)
logger.error("Exception: " + msg)
return_val = False
finally:
conn.unbind()
return return_val, msg
def _set_max_uid(self, max_uid):
"""
a utility function to save max_uid value to a file
:param max_uid: an integer representing the max uid
:return:
"""
with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler:
handler.write(str(max_uid))
def _get_max_uid(self):
"""
A utility function to read the max uid value that was previously set
:return: An integer representing the max uid value that was previously
set
"""
try:
with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler:
try:
return_value = int(handler.read())
except ValueError as ve:
logger.error(
"Error reading int value from {}. {}"
"Returning default value {} instead".format(
settings.LDAP_MAX_UID_FILE_PATH,
str(ve),
settings.LDAP_DEFAULT_START_UID
)
)
return_value = settings.LDAP_DEFAULT_START_UID
return return_value
except FileNotFoundError as fnfe:
logger.error("File not found : " + str(fnfe))
return_value = settings.LDAP_DEFAULT_START_UID
logger.error("So, returning UID={}".format(return_value))
return return_value

File diff suppressed because one or more lines are too long

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-12-26 14:02
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('utils', '0007_auto_20191226_0610'),
]
operations = [
migrations.AddField(
model_name='billingaddress',
name='vat_validation_status',
field=models.CharField(blank=True, default='', max_length=25),
),
migrations.AddField(
model_name='userbillingaddress',
name='vat_validation_status',
field=models.CharField(blank=True, default='', max_length=25),
),
]

View file

@ -13,11 +13,6 @@ class BaseBillingAddress(models.Model):
city = models.CharField(max_length=50)
postal_code = models.CharField(max_length=50)
country = CountryField()
vat_number = models.CharField(max_length=100, default="", blank=True)
stripe_tax_id = models.CharField(max_length=100, default="", blank=True)
vat_number_validated_on = models.DateTimeField(blank=True, null=True)
vat_validation_status = models.CharField(max_length=25, default="",
blank=True)
class Meta:
abstract = True
@ -25,18 +20,7 @@ class BaseBillingAddress(models.Model):
class BillingAddress(BaseBillingAddress):
def __str__(self):
if self.vat_number:
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country, self.vat_number,
self.stripe_tax_id, self.vat_number_validated_on,
self.vat_validation_status
)
else:
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
return self.street_address
class UserBillingAddress(BaseBillingAddress):
@ -44,18 +28,7 @@ class UserBillingAddress(BaseBillingAddress):
current = models.BooleanField(default=True)
def __str__(self):
if self.vat_number:
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country, self.vat_number,
self.stripe_tax_id, self.vat_number_validated_on,
self.vat_validation_status
)
else:
return "%s, %s, %s, %s, %s" % (
self.cardholder_name, self.street_address, self.city,
self.postal_code, self.country
)
return self.street_address
def to_dict(self):
return {
@ -64,7 +37,6 @@ class UserBillingAddress(BaseBillingAddress):
'City': self.city,
'Postal Code': self.postal_code,
'Country': self.country,
'VAT Number': self.vat_number
}

View file

@ -1,9 +1,7 @@
import logging
import re
import stripe
from django.conf import settings
from datacenterlight.models import StripePlan
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
@ -34,33 +32,32 @@ def handleStripeError(f):
logger.error(str(e))
return response
except stripe.error.RateLimitError as e:
logger.error(str(e))
response.update(
{'error': "Too many requests made to the API too quickly"})
return response
except stripe.error.InvalidRequestError as e:
logger.error(str(e))
response.update({'error': str(e._message)})
response.update({'error': "Invalid parameters"})
return response
except stripe.error.AuthenticationError as e:
# Authentication with Stripe's API failed
# (maybe you changed API keys recently)
logger.error(str(e))
response.update({'error': str(e)})
response.update({'error': common_message})
return response
except stripe.error.APIConnectionError as e:
logger.error(str(e))
response.update({'error': str(e)})
response.update({'error': common_message})
return response
except stripe.error.StripeError as e:
# maybe send email
logger.error(str(e))
response.update({'error': str(e)})
response.update({'error': common_message})
return response
except Exception as e:
# maybe send email
logger.error(str(e))
response.update({'error': str(e)})
response.update({'error': common_message})
return response
return handleProblems
@ -70,7 +67,7 @@ class StripeUtils(object):
CURRENCY = 'chf'
INTERVAL = 'month'
SUCCEEDED_STATUS = 'succeeded'
RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists'
STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
STRIPE_NO_SUCH_PLAN = 'No such plan'
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
@ -83,31 +80,20 @@ class StripeUtils(object):
customer.save()
@handleStripeError
def associate_customer_card(self, stripe_customer_id, id_payment_method,
def associate_customer_card(self, stripe_customer_id, token,
set_as_default=False):
customer = stripe.Customer.retrieve(stripe_customer_id)
stripe.PaymentMethod.attach(
id_payment_method,
customer=stripe_customer_id,
)
card = customer.sources.create(source=token)
if set_as_default:
customer.invoice_settings.default_payment_method = id_payment_method
customer.default_source = card.id
customer.save()
return True
@handleStripeError
def dissociate_customer_card(self, stripe_customer_id, card_id):
customer = stripe.Customer.retrieve(stripe_customer_id)
if card_id.startswith("pm"):
logger.debug("PaymentMethod %s detached %s" % (card_id,
stripe_customer_id))
pm = stripe.PaymentMethod.retrieve(card_id)
stripe.PaymentMethod.detach(card_id)
pm.delete()
else:
logger.debug("card %s detached %s" % (card_id, stripe_customer_id))
card = customer.sources.retrieve(card_id)
card.delete()
card = customer.sources.retrieve(card_id)
card.delete()
@handleStripeError
def update_customer_card(self, customer_id, token):
@ -199,24 +185,6 @@ class StripeUtils(object):
}
return card_details
@handleStripeError
def get_cards_details_from_payment_method(self, payment_method_id):
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
# payment_method does not always seem to have a card with id
# if that is the case, fallback to payment_method_id for card_id
card_id = payment_method_id
if hasattr(payment_method.card, 'id'):
card_id = payment_method.card.id
card_details = {
'last4': payment_method.card.last4,
'brand': payment_method.card.brand,
'exp_month': payment_method.card.exp_month,
'exp_year': payment_method.card.exp_year,
'fingerprint': payment_method.card.fingerprint,
'card_id': card_id
}
return card_details
def check_customer(self, stripe_cus_api_id, user, token):
try:
customer = stripe.Customer.retrieve(stripe_cus_api_id)
@ -236,11 +204,11 @@ class StripeUtils(object):
return customer
@handleStripeError
def create_customer(self, id_payment_method, email, name=None):
def create_customer(self, token, email, name=None):
if name is None or name.strip() == "":
name = email
customer = self.stripe.Customer.create(
payment_method=id_payment_method,
source=token,
description=name,
email=email
)
@ -297,17 +265,11 @@ class StripeUtils(object):
stripe_plan_db_obj = StripePlan.objects.create(
stripe_plan_id=stripe_plan_id)
except stripe.error.InvalidRequestError as e:
logger.error(str(e))
logger.error("error_code = %s" % str(e.__dict__))
if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code:
if self.STRIPE_PLAN_ALREADY_EXISTS in str(e):
logger.debug(
self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
stripe_plan_db_obj, c = StripePlan.objects.get_or_create(
stripe_plan_db_obj = StripePlan.objects.create(
stripe_plan_id=stripe_plan_id)
if c:
logger.debug("Created stripe plan %s" % stripe_plan_id)
else:
logger.debug("Plan %s exists already" % stripe_plan_id)
return stripe_plan_db_obj
@handleStripeError
@ -335,15 +297,10 @@ class StripeUtils(object):
return return_value
@handleStripeError
def subscribe_customer_to_plan(self, customer, plans, trial_end=None,
coupon="", tax_rates=list(),
default_payment_method=""):
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
"""
Subscribes the given customer to the list of given plans
:param default_payment_method:
:param tax_rates:
:param coupon:
:param customer: The stripe customer identifier
:param plans: A list of stripe plans.
:param trial_end: An integer representing when the Stripe subscription
@ -357,17 +314,10 @@ class StripeUtils(object):
]
:return: The subscription StripeObject
"""
logger.debug("Subscribing %s to plan %s : coupon = %s" % (
customer, str(plans), str(coupon)
))
subscription_result = self.stripe.Subscription.create(
customer=customer, items=plans, trial_end=trial_end,
coupon=coupon,
default_tax_rates=tax_rates,
payment_behavior='allow_incomplete',
default_payment_method=default_payment_method
customer=customer, items=plans, trial_end=trial_end
)
logger.debug("Done subscribing")
return subscription_result
@handleStripeError
@ -398,7 +348,7 @@ class StripeUtils(object):
@staticmethod
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
price=None, excl_vat=True):
price=None):
"""
Returns the Stripe plan id string of the form
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
@ -425,16 +375,13 @@ class StripeUtils(object):
plan=dcl_plan_string
)
if price is not None:
stripe_plan_id_string = '{}-{}chf'.format(
stripe_plan_id_string_with_price = '{}-{}chf'.format(
stripe_plan_id_string,
round(price, 2)
)
if excl_vat:
stripe_plan_id_string = '{}-{}'.format(
stripe_plan_id_string,
"excl_vat"
)
return stripe_plan_id_string
return stripe_plan_id_string_with_price
else:
return stripe_plan_id_string
@staticmethod
def get_vm_config_from_stripe_id(stripe_id):
@ -463,27 +410,18 @@ class StripeUtils(object):
@staticmethod
def get_stripe_plan_name(cpu, memory, disk_size, price, excl_vat=True):
def get_stripe_plan_name(cpu, memory, disk_size, price):
"""
Returns the Stripe plan name
:return:
"""
if excl_vat:
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF Excl. VAT".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
else:
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
"{price} CHF".format(
cpu=cpu,
memory=memory,
disk_size=disk_size,
price=round(price, 2)
)
@handleStripeError
def set_subscription_meta_data(self, subscription_id, meta_data):
@ -496,78 +434,3 @@ class StripeUtils(object):
subscription = stripe.Subscription.retrieve(subscription_id)
subscription.metadata = meta_data
subscription.save()
@handleStripeError
def get_or_create_tax_id_for_user(self, stripe_customer_id, vat_number,
type="eu_vat", country=""):
tax_ids_list = stripe.Customer.list_tax_ids(
stripe_customer_id,
limit=100,
)
for tax_id_obj in tax_ids_list.data:
if self.compare_vat_numbers(tax_id_obj.value, vat_number):
logger.debug("tax id obj exists already")
return tax_id_obj
else:
logger.debug(
"{val1} is not equal to {val2} or {con1} not same as "
"{con2}".format(val1=tax_id_obj.value, val2=vat_number,
con1=tax_id_obj.country.lower(),
con2=country.lower().strip()))
logger.debug(
"tax id obj does not exist for {val}. Creating a new one".format(
val=vat_number
))
tax_id_obj = stripe.Customer.create_tax_id(
stripe_customer_id,
type=type,
value=vat_number,
)
return tax_id_obj
@handleStripeError
def get_payment_intent(self, amount, customer):
""" Create a stripe PaymentIntent of the given amount and return it
:param amount: the amount of payment_intent
:return:
"""
payment_intent_obj = stripe.PaymentIntent.create(
amount=amount,
currency='chf',
customer=customer,
setup_future_usage='off_session'
)
return payment_intent_obj
@handleStripeError
def get_available_payment_methods(self, customer):
""" Retrieves all payment methods of the given customer
:param customer: StripeCustomer object
:return: a list of available payment methods
"""
return_list = []
if customer is None:
return return_list
cu = stripe.Customer.retrieve(customer.stripe_id)
pms = stripe.PaymentMethod.list(
customer=customer.stripe_id,
type="card",
)
default_source = None
if cu.default_source:
default_source = cu.default_source
else:
default_source = cu.invoice_settings.default_payment_method
for pm in pms.data:
return_list.append({
'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id,
'exp_year': pm.card.exp_year,
'exp_month': '{:02d}'.format(pm.card.exp_month),
'preferred': pm.id == default_source
})
return return_list
def compare_vat_numbers(self, vat1, vat2):
_vat1 = vat1.replace(" ", "").replace(".", "").replace("-","")
_vat2 = vat2.replace(" ", "").replace(".", "").replace("-","")
return True if _vat1 == _vat2 else False

View file

@ -228,7 +228,7 @@ class SSHKeyCreateView(FormView):
if self.request.user.is_authenticated():
owner = self.request.user
manager = OpenNebulaManager(
email=owner.username,
email=owner.email,
password=owner.password
)
keys_to_save = get_all_public_keys(self.request.user)

View file

@ -316,9 +316,3 @@ IM",GBP,0.1,standard,
2019-11-15,,FR,EUR,0.2,standard,France standard VAT (added manually)
2019-11-15,,GR,EUR,0.24,standard,Greece standard VAT (added manually)
2019-11-15,,GB,EUR,0.2,standard,UK standard VAT (added manually)
2019-12-17,,AD,EUR,0.045,standard,Andorra standard VAT (added manually)
2019-12-17,,TK,EUR,0.18,standard,Turkey standard VAT (added manually)
2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually)
2019-12-17,,FX,EUR,0.20,standard,France metropolitan standard VAT (added manually)
2020-01-04,,CY,EUR,0.19,standard,Cyprus standard VAT (added manually)
2019-01-04,,LI,EUR,0.077,standard,Liechtenstein standard VAT (added manually)

1 start_date stop_date territory_codes currency_code rate rate_type description
316
317
318

View file

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class WebhookConfig(AppConfig):
name = 'webhook'

View file

@ -1,81 +0,0 @@
import logging
import stripe
from django.core.management.base import BaseCommand
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '''creates webhook with the supplied arguments and returns the
webhook secret
'''
def add_arguments(self, parser):
parser.add_argument(
'--webhook_endpoint',
help="The url of the webhook endpoint that accepts the events "
"from stripe",
dest="webhook_endpoint",
required=False
)
parser.add_argument('--events_csv', dest="events_csv", required=False)
parser.add_argument('--webhook_id', dest="webhook_id", required=False)
parser.add_argument('--create', dest='create', action='store_true')
parser.add_argument('--list', dest='list', action='store_true')
parser.add_argument('--delete', dest='delete', action='store_true')
def handle(self, *args, **options):
wep_exists = False
if options['list']:
logger.debug("Listing webhooks")
we_list = stripe.WebhookEndpoint.list(limit=100)
for wep in we_list.data:
msg = wep.id + " -- " + ",".join(wep.enabled_events)
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
elif options['delete']:
logger.debug("Deleting webhook")
if 'webhook_id' in options:
stripe.WebhookEndpoint.delete(options['webhook_id'])
msg = "Deleted " + options['webhook_id']
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
else:
msg = "Supply webhook_id to delete a webhook"
logger.debug(msg)
self.stdout.write(
self.style.SUCCESS(msg)
)
exit(0)
elif options['create']:
logger.debug("Creating webhook")
try:
we_list = stripe.WebhookEndpoint.list(limit=100)
for wep in we_list.data:
if set(wep.enabled_events) == set(options['events_csv'].split(",")):
if wep.url == options['webhook_endpoint']:
logger.debug("We have this webhook already")
wep_exists = True
break
if wep_exists is False:
logger.debug(
"No webhook exists for {} at {}. Creatting a new endpoint "
"now".format(
options['webhook_endpoint'], options['events_csv']
)
)
wep = stripe.WebhookEndpoint.create(
url=options['webhook_endpoint'],
enabled_events=options['events_csv'].split(",")
)
self.stdout.write(
self.style.SUCCESS('Creation successful. '
'webhook_secret = %s' % wep.secret)
)
except Exception as e:
print(" *** Error occurred. Details {}".format(str(e)))

View file

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,318 +0,0 @@
import datetime
import logging
import json
import stripe
# Create your views here.
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from datacenterlight.views import do_provisioning, do_provisioning_generic
from membership.models import StripeCustomer
from hosting.models import IncompleteSubscriptions, IncompletePaymentIntents
from utils.models import BillingAddress, UserBillingAddress
from utils.tasks import send_plain_email_task
logger = logging.getLogger(__name__)
# Things to do for webhooks feature
# 1. Uninstall old version and install a more recent version of stripe
# ```
# source venv/bin/activate
# ./manage.py shell
# pip uninstall stripe
# pip install stripe==2.24.1
# ```
# 2. Create tax id updated webhook
# ```
# ./manage.py webhook --create \
# --webhook_endpoint https://datacenterlight.ch/en-us/webhooks/ \
# --events_csv customer.tax_id.updated
# ```
#
# 3. From the secret obtained in 2, setup an environment variable
# ```
# WEBHOOK_SECRET='whsec......'
# ```
@require_POST
@csrf_exempt
def handle_webhook(request):
payload = request.body
event = None
try:
if 'HTTP_STRIPE_SIGNATURE' in request.META:
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
else:
logger.error("No HTTP_STRIPE_SIGNATURE header")
# Invalid payload
return HttpResponse(status=400)
event = stripe.Webhook.construct_event(
payload, sig_header, settings.WEBHOOK_SECRET
)
except ValueError as e:
# Invalid payload
err_msg = "FAILURE handle_invoice_webhook: Invalid payload details"
err_body = "Details %s" % str(e)
return handle_error(err_msg, err_body)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
err_msg = "FAILURE handle_invoice_webhook: SignatureVerificationError"
err_body = "Details %s" % str(e)
return handle_error(err_msg, err_body)
# Do something with event
logger.debug("Passed signature verification")
if event.type == "customer.tax_id.updated":
logger.debug("Webhook Event: customer.tax_id.updated")
tax_id_obj = event.data.object
logger.debug("Tax_id %s is %s" % (tax_id_obj.id,
tax_id_obj.verification.status))
stripe_customer = None
try:
stripe_customer = StripeCustomer.objects.get(stripe_id=tax_id_obj.customer)
except StripeCustomer.DoesNotExist as dne:
logger.debug(
"StripeCustomer %s does not exist" % tax_id_obj.customer)
if tax_id_obj.verification.status == "verified":
b_addresses = BillingAddress.objects.filter(stripe_tax_id=tax_id_obj.id)
for b_address in b_addresses:
b_address.vat_validation_status = tax_id_obj.verification.status
b_address.vat_number_validated_on = datetime.datetime.now()
b_address.save()
ub_addresses = UserBillingAddress.objects.filter(stripe_tax_id=tax_id_obj.id)
for ub_address in ub_addresses:
ub_address.vat_validation_status = tax_id_obj.verification.status
ub_address.vat_number_validated_on = datetime.datetime.now()
ub_address.save()
email_data = {
'subject': "The VAT %s associated with %s was verified" %
(tax_id_obj.value, stripe_customer.user.email if stripe_customer else "unknown"),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "The following objects were modified:\n".join(
'\n'.join([str(b_address) for b_address in b_addresses])
).join(
'\n'.join([str(ub_address) for ub_address in ub_addresses])
),
}
else:
logger.debug("Tax_id %s is %s" % (tax_id_obj.id,
tax_id_obj.verification.status))
email_data = {
'subject': "The VAT %s associated with %s was %s" %
(tax_id_obj.value, stripe_customer.user.email if stripe_customer else "unknown", tax_id_obj.verification.status),
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(tax_id_obj),
}
send_plain_email_task.delay(email_data)
elif event.type == 'invoice.paid':
#More info: https://stripe.com/docs/billing/migration/strong-customer-authentication#scenario-1-handling-fulfillment
invoice_obj = event.data.object
logger.debug("Webhook Event: invoice.paid")
logger.debug("invoice_obj %s " % str(invoice_obj))
logger.debug("invoice_obj.paid = %s %s" % (invoice_obj.paid, type(invoice_obj.paid)))
logger.debug("invoice_obj.billing_reason = %s %s" % (invoice_obj.billing_reason, type(invoice_obj.billing_reason)))
# We should check for billing_reason == "subscription_create" but we
# check for "subscription_update"
# because we are using older api.
# See https://stripe.com/docs/upgrades?since=2015-07-13
# The billing_reason attribute of the invoice object now can take the
# value of subscription_create, indicating that it is the first
# invoice of a subscription. For older API versions,
# billing_reason=subscription_create is represented as
# subscription_update.
if (invoice_obj.paid and
invoice_obj.billing_reason == "subscription_update"):
logger.debug("""invoice_obj.paid and
invoice_obj.billing_reason == subscription_update""")
logger.debug("Start provisioning")
try:
logger.debug("Looking for subscription %s" %
invoice_obj.subscription)
stripe_subscription_obj = stripe.Subscription.retrieve(
invoice_obj.subscription)
try:
incomplete_sub = IncompleteSubscriptions.objects.get(
subscription_id=invoice_obj.subscription)
request = ""
soc = ""
card_details_response = ""
gp_details = ""
template = ""
specs = ""
billing_address_data = ""
if incomplete_sub.request:
request = json.loads(incomplete_sub.request)
if incomplete_sub.specs:
specs = json.loads(incomplete_sub.specs)
if incomplete_sub.stripe_onetime_charge:
soc = json.loads(incomplete_sub.stripe_onetime_charge)
if incomplete_sub.gp_details:
gp_details = json.loads(incomplete_sub.gp_details)
if incomplete_sub.card_details_response:
card_details_response = json.loads(
incomplete_sub.card_details_response)
if incomplete_sub.template:
template = json.loads(
incomplete_sub.template)
if incomplete_sub.billing_address_data:
billing_address_data = json.loads(
incomplete_sub.billing_address_data)
logger.debug("*******")
logger.debug(str(incomplete_sub))
logger.debug("*******")
logger.debug("1*******")
logger.debug(request)
logger.debug("2*******")
logger.debug(card_details_response)
logger.debug("3*******")
logger.debug(soc)
logger.debug("4*******")
logger.debug(gp_details)
logger.debug("5*******")
logger.debug(template)
logger.debug("6*******")
do_provisioning(
request=request,
stripe_api_cus_id=incomplete_sub.stripe_api_cus_id,
card_details_response=card_details_response,
stripe_subscription_obj=stripe_subscription_obj,
stripe_onetime_charge=soc,
gp_details=gp_details,
specs=specs,
vm_template_id=incomplete_sub.vm_template_id,
template=template,
billing_address_data=billing_address_data,
real_request=None
)
except IncompleteSubscriptions.DoesNotExist as ex:
logger.error(str(ex))
except IncompleteSubscriptions.MultipleObjectsReturned as ex:
logger.error(str(ex))
email_data = {
'subject': "IncompleteSubscriptions error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
except Exception as ex:
logger.error(str(ex))
email_data = {
'subject': "invoice.paid Webhook error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
elif event.type == 'invoice.payment_failed':
invoice_obj = event.data.object
logger.debug("Webhook Event: invoice.payment_failed")
logger.debug("invoice_obj %s " % str(invoice_obj))
if (invoice_obj.payment_failed and
invoice_obj.billing_reason == "subscription_update"):
logger.debug("Payment failed, inform the users")
elif event.type == 'payment_intent.succeeded':
payment_intent_obj = event.data.object
logger.debug("Webhook Event: payment_intent.succeeded")
logger.debug("payment_intent_obj %s " % str(payment_intent_obj))
try:
logger.debug("Looking for IncompletePaymentIntents %s " %
payment_intent_obj.id)
incomplete_pm = IncompletePaymentIntents.objects.get(
payment_intent_id=payment_intent_obj.id)
logger.debug("incomplete_pm = %s" % str(incomplete_pm.__dict__))
request = ""
soc = ""
card_details_response = ""
gp_details = ""
template = ""
billing_address_data = ""
if incomplete_pm.request:
request = json.loads(incomplete_pm.request)
logger.debug("request = %s" % str(request))
if incomplete_pm.stripe_charge_id:
soc = incomplete_pm.stripe_charge_id
logger.debug("stripe_onetime_charge = %s" % str(soc))
if incomplete_pm.gp_details:
gp_details = json.loads(incomplete_pm.gp_details)
logger.debug("gp_details = %s" % str(gp_details))
if incomplete_pm.card_details_response:
card_details_response = json.loads(
incomplete_pm.card_details_response)
logger.debug("card_details_response = %s" % str(card_details_response))
if incomplete_pm.billing_address_data:
billing_address_data = json.loads(
incomplete_pm.billing_address_data)
logger.debug("billing_address_data = %s" % str(billing_address_data))
logger.debug("1*******")
logger.debug(request)
logger.debug("2*******")
logger.debug(card_details_response)
logger.debug("3*******")
logger.debug(soc)
logger.debug("4*******")
logger.debug(gp_details)
logger.debug("5*******")
logger.debug(template)
logger.debug("6*******")
logger.debug(billing_address_data)
incomplete_pm.completed_at = datetime.datetime.now()
charges = ""
if len(payment_intent_obj.charges.data) > 0:
for d in payment_intent_obj.charges.data:
if charges == "":
charges = "%s" % d.id
else:
charges = "%s,%s" % (charges, d.id)
logger.debug("Charge ids = %s" % charges)
incomplete_pm.stripe_charge_id=charges
do_provisioning_generic(
request=request,
stripe_api_cus_id=incomplete_pm.stripe_api_cus_id,
card_details_response=card_details_response,
stripe_subscription_id=None,
stripe_charge_id=charges,
gp_details=gp_details,
billing_address_data=billing_address_data
)
incomplete_pm.save()
except IncompletePaymentIntents.DoesNotExist as ex:
logger.error(str(ex))
except (IncompletePaymentIntents.MultipleObjectsReturned,
Exception) as ex:
logger.error(str(ex))
email_data = {
'subject': "IncompletePaymentIntents error",
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
'body': "Response = %s" % str(ex),
}
send_plain_email_task.delay(email_data)
else:
logger.error("Unhandled event : " + event.type)
return HttpResponse(status=200)
def handle_error(error_msg, error_body):
logger.error("%s -- %s" % (error_msg, error_body))
email_to_admin_data = {
'subject': error_msg,
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
'to': [settings.ADMIN_EMAIL],
'body': error_body,
}
send_plain_email_task.delay(email_to_admin_data)
return HttpResponse(status=400)