Compare commits
27 Commits
Author | SHA1 | Date |
---|---|---|
Nico Schottelius | ecc9e6f734 | |
Nico Schottelius | 20c7c86703 | |
Nico Schottelius | 8959bc6ad5 | |
Nico Schottelius | 0cd8a3a787 | |
Nico Schottelius | bbc7625550 | |
Nico Schottelius | fe4e200dc0 | |
Nico Schottelius | e03cdf214a | |
Nico Schottelius | 50fd9e1f37 | |
Nico Schottelius | 2e74661702 | |
Nico Schottelius | c26ff253de | |
Nico Schottelius | 9623a77907 | |
Nico Schottelius | c435639241 | |
Nico Schottelius | 992c7c551e | |
Nico Schottelius | 58883765d7 | |
Nico Schottelius | 8d8c4d660c | |
Nico Schottelius | c32499199a | |
Nico Schottelius | c6bacab35a | |
Nico Schottelius | 1aead50170 | |
Nico Schottelius | d8a7964fed | |
Nico Schottelius | 077c665c53 | |
Nico Schottelius | f7274fe967 | |
Nico Schottelius | 1c7d81762d | |
Nico Schottelius | 18f9a3848a | |
Nico Schottelius | 9211894b23 | |
Nico Schottelius | b8b15704a3 | |
Nico Schottelius | ab412cb877 | |
Nico Schottelius | 7b83efe995 |
|
@ -60,6 +60,21 @@ python manage.py migrate
|
|||
python manage.py bootstrap-user --username nicocustomer
|
||||
#+END_SRC
|
||||
|
||||
** Initialise the database
|
||||
While it is not strictly required to add default values to the
|
||||
database, it might significantly reduce the starting time with
|
||||
uncloud.
|
||||
|
||||
To add the default database values run:
|
||||
|
||||
#+BEGIN_SRC shell
|
||||
# Add local objects
|
||||
python manage.py db-add-defaults
|
||||
|
||||
# Import VAT rates
|
||||
python manage.py import-vat-rates
|
||||
#+END_SRC
|
||||
|
||||
* Testing / CLI Access
|
||||
Access via the commandline (CLI) can be done using curl or
|
||||
httpie. In our examples we will use httpie.
|
||||
|
@ -84,6 +99,14 @@ python manage.py migrate
|
|||
* URLs
|
||||
- api/ - the rest API
|
||||
* uncloud Products
|
||||
** Product features
|
||||
- Dependencies on other products
|
||||
- Minimum parameters (min cpu, min ram, etc).
|
||||
- Can also realise the dcl vm
|
||||
- dualstack vm = VM + IPv4 + SSD
|
||||
- Need to have a non-misguiding name for the "bare VM"
|
||||
- Should support network boot (?)
|
||||
|
||||
** VPN
|
||||
*** How to add a new VPN Host
|
||||
**** Install wireguard to the host
|
||||
|
@ -95,8 +118,8 @@ python manage.py migrate
|
|||
**** Add it to DNS as vpn-XXX.ungleich.ch
|
||||
**** Route a /40 network to its IPv6 address
|
||||
**** Install wireguard on it
|
||||
**** TODO Enable wireguard on boot
|
||||
**** TODO Create a new VPNPool on uncloud with
|
||||
**** TODO [#C] Enable wireguard on boot
|
||||
**** TODO [#C] Create a new VPNPool on uncloud with
|
||||
***** the network address (selecting from our existing pool)
|
||||
***** the network size (/...)
|
||||
***** the vpn host that provides the network (selecting the created VM)
|
||||
|
@ -210,3 +233,95 @@ VPNNetworks can be managed by all authenticated users.
|
|||
|
||||
*** Decision
|
||||
We use integers, because they are easy.
|
||||
|
||||
** Milestones :uncloud:
|
||||
*** 1.1 (cleanup 1)
|
||||
**** TODO Unify ValidationError, FieldError - define proper Exception
|
||||
- What do we use for model errors
|
||||
*** 1.0 (initial release)
|
||||
**** TODO Initial Generic product support
|
||||
- Product
|
||||
***** TODO Recurring product support
|
||||
****** TODO Support replacing orders for updates
|
||||
****** DONE [#A] Finish split of bill creation
|
||||
CLOSED: [2020-09-11 Fri 23:19]
|
||||
****** TODO Test the new functions in the Order class
|
||||
****** Define the correct order replacement logic
|
||||
Assumption:
|
||||
- recurringperiods are 30days
|
||||
******* Case 1: downgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to downgrade after 15 days to 5 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days
|
||||
- order 2: 5 CHF starting 30days + 1s
|
||||
- Sum of the two orders is 15 CHF
|
||||
- Question is
|
||||
- when is the VM shutdown?
|
||||
- a) instantly
|
||||
- b) at the end of the cycle
|
||||
- best solution
|
||||
- user can choose between a ... b any time
|
||||
******* Duration
|
||||
- You cannot cancel the duration
|
||||
- You can upgrade and with that cancel the duration
|
||||
- The idea of a duration is that you commit for it
|
||||
- If you want to commit lower (daily basis for instance) you
|
||||
have higher per period prices
|
||||
******* Case X
|
||||
- User has VM with 2 Core / 2 GB RAM
|
||||
- User modifies with to 1 core / 3 GB RAM
|
||||
- We treat it as down/upgrade independent of the modifications
|
||||
|
||||
******* Case 2: upgrading after 1 day
|
||||
- committed for 30 days
|
||||
- upgrade after 1 day
|
||||
- so first order will be charged for 1/30ths
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Order 1 : 1 VM with 2 Core / 2 GB / 10 SSD -- 10 CHF
|
||||
- 30days period, stopped after 15, so quantity is 0.5 = 5 CHF
|
||||
- Order 2 : 1 VM with 2 Core / 6 GB / 10 SSD -- 20 CHF
|
||||
- after 15 days
|
||||
- VM is upgraded instantly
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +15days = 0.5 units = 5 CHF
|
||||
- order 2: 20 CHF starting 15days + 1s ... +30 days after
|
||||
the 15 days -> 45 days = 1 unit = 20 CHF
|
||||
- Total on bill: 25 CHF
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days = 1 units = 10 CHF
|
||||
|
||||
- order 2: 20 CHF starting 15days + 1s = 1 unit = 20 CHF
|
||||
- Total on bill: 30 CHF
|
||||
|
||||
|
||||
****** TODO Note: ending date not set if replaced by default (implicit!)
|
||||
- Should the new order modify the old order on save()?
|
||||
****** DONE Fix totally wrong bill dates in our test case
|
||||
CLOSED: [2020-09-09 Wed 01:00]
|
||||
- 2020 used instead of 2019
|
||||
- Was due to existing test data ...
|
||||
***** TODO Bill logic is still wrong
|
||||
- Bill starting_date is the date of the first order
|
||||
- However first encountered order does not have to be the
|
||||
earliest in the bill!
|
||||
- Bills should not have a duration
|
||||
- Bills should only have a (unique) issue date
|
||||
- We charge based on bill_records
|
||||
- Last time charged issue date of the bill OR earliest date
|
||||
after that
|
||||
- Every bill generation checks all (relevant) orders
|
||||
- add a flag "not_for_billing" or "closed"
|
||||
- query on that flag
|
||||
- verify it every time
|
||||
|
||||
|
||||
***** TODO Generating bill for admins/staff
|
||||
-
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('opennebula', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vm',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('opennebula', '0005_remove_vm_orders'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vm',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vm',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vm',
|
||||
name='status',
|
||||
),
|
||||
]
|
|
@ -10,7 +10,7 @@ storage_class_mapping = {
|
|||
'hdd': 'hdd'
|
||||
}
|
||||
|
||||
class VM(Product):
|
||||
class VM(models.Model):
|
||||
vmid = models.IntegerField(primary_key=True)
|
||||
data = models.JSONField()
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
from rest_framework import viewsets, permissions
|
||||
|
||||
from .models import VM
|
||||
from .serializers import OpenNebulaVMSerializer
|
||||
#from .models import VM
|
||||
# from .serializers import OpenNebulaVMSerializer
|
||||
|
||||
class VMViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = OpenNebulaVMSerializer
|
||||
# class VMViewSet(viewsets.ModelViewSet):
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
# serializer_class = OpenNebulaVMSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
obj = VM.objects.all()
|
||||
else:
|
||||
obj = VM.objects.filter(owner=self.request.user)
|
||||
# def get_queryset(self):
|
||||
# if self.request.user.is_superuser:
|
||||
# obj = VM.objects.all()
|
||||
# else:
|
||||
# obj = VM.objects.filter(owner=self.request.user)
|
||||
|
||||
return obj
|
||||
# return obj
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# http://xml.coverpages.org/country3166.html
|
||||
COUNTRIES = (
|
||||
('AD', _('Andorra')),
|
||||
('AE', _('United Arab Emirates')),
|
||||
('AF', _('Afghanistan')),
|
||||
('AG', _('Antigua & Barbuda')),
|
||||
('AI', _('Anguilla')),
|
||||
('AL', _('Albania')),
|
||||
('AM', _('Armenia')),
|
||||
('AN', _('Netherlands Antilles')),
|
||||
('AO', _('Angola')),
|
||||
('AQ', _('Antarctica')),
|
||||
('AR', _('Argentina')),
|
||||
('AS', _('American Samoa')),
|
||||
('AT', _('Austria')),
|
||||
('AU', _('Australia')),
|
||||
('AW', _('Aruba')),
|
||||
('AZ', _('Azerbaijan')),
|
||||
('BA', _('Bosnia and Herzegovina')),
|
||||
('BB', _('Barbados')),
|
||||
('BD', _('Bangladesh')),
|
||||
('BE', _('Belgium')),
|
||||
('BF', _('Burkina Faso')),
|
||||
('BG', _('Bulgaria')),
|
||||
('BH', _('Bahrain')),
|
||||
('BI', _('Burundi')),
|
||||
('BJ', _('Benin')),
|
||||
('BM', _('Bermuda')),
|
||||
('BN', _('Brunei Darussalam')),
|
||||
('BO', _('Bolivia')),
|
||||
('BR', _('Brazil')),
|
||||
('BS', _('Bahama')),
|
||||
('BT', _('Bhutan')),
|
||||
('BV', _('Bouvet Island')),
|
||||
('BW', _('Botswana')),
|
||||
('BY', _('Belarus')),
|
||||
('BZ', _('Belize')),
|
||||
('CA', _('Canada')),
|
||||
('CC', _('Cocos (Keeling) Islands')),
|
||||
('CF', _('Central African Republic')),
|
||||
('CG', _('Congo')),
|
||||
('CH', _('Switzerland')),
|
||||
('CI', _('Ivory Coast')),
|
||||
('CK', _('Cook Iislands')),
|
||||
('CL', _('Chile')),
|
||||
('CM', _('Cameroon')),
|
||||
('CN', _('China')),
|
||||
('CO', _('Colombia')),
|
||||
('CR', _('Costa Rica')),
|
||||
('CU', _('Cuba')),
|
||||
('CV', _('Cape Verde')),
|
||||
('CX', _('Christmas Island')),
|
||||
('CY', _('Cyprus')),
|
||||
('CZ', _('Czech Republic')),
|
||||
('DE', _('Germany')),
|
||||
('DJ', _('Djibouti')),
|
||||
('DK', _('Denmark')),
|
||||
('DM', _('Dominica')),
|
||||
('DO', _('Dominican Republic')),
|
||||
('DZ', _('Algeria')),
|
||||
('EC', _('Ecuador')),
|
||||
('EE', _('Estonia')),
|
||||
('EG', _('Egypt')),
|
||||
('EH', _('Western Sahara')),
|
||||
('ER', _('Eritrea')),
|
||||
('ES', _('Spain')),
|
||||
('ET', _('Ethiopia')),
|
||||
('FI', _('Finland')),
|
||||
('FJ', _('Fiji')),
|
||||
('FK', _('Falkland Islands (Malvinas)')),
|
||||
('FM', _('Micronesia')),
|
||||
('FO', _('Faroe Islands')),
|
||||
('FR', _('France')),
|
||||
('FX', _('France, Metropolitan')),
|
||||
('GA', _('Gabon')),
|
||||
('GB', _('United Kingdom (Great Britain)')),
|
||||
('GD', _('Grenada')),
|
||||
('GE', _('Georgia')),
|
||||
('GF', _('French Guiana')),
|
||||
('GH', _('Ghana')),
|
||||
('GI', _('Gibraltar')),
|
||||
('GL', _('Greenland')),
|
||||
('GM', _('Gambia')),
|
||||
('GN', _('Guinea')),
|
||||
('GP', _('Guadeloupe')),
|
||||
('GQ', _('Equatorial Guinea')),
|
||||
('GR', _('Greece')),
|
||||
('GS', _('South Georgia and the South Sandwich Islands')),
|
||||
('GT', _('Guatemala')),
|
||||
('GU', _('Guam')),
|
||||
('GW', _('Guinea-Bissau')),
|
||||
('GY', _('Guyana')),
|
||||
('HK', _('Hong Kong')),
|
||||
('HM', _('Heard & McDonald Islands')),
|
||||
('HN', _('Honduras')),
|
||||
('HR', _('Croatia')),
|
||||
('HT', _('Haiti')),
|
||||
('HU', _('Hungary')),
|
||||
('ID', _('Indonesia')),
|
||||
('IE', _('Ireland')),
|
||||
('IL', _('Israel')),
|
||||
('IN', _('India')),
|
||||
('IO', _('British Indian Ocean Territory')),
|
||||
('IQ', _('Iraq')),
|
||||
('IR', _('Islamic Republic of Iran')),
|
||||
('IS', _('Iceland')),
|
||||
('IT', _('Italy')),
|
||||
('JM', _('Jamaica')),
|
||||
('JO', _('Jordan')),
|
||||
('JP', _('Japan')),
|
||||
('KE', _('Kenya')),
|
||||
('KG', _('Kyrgyzstan')),
|
||||
('KH', _('Cambodia')),
|
||||
('KI', _('Kiribati')),
|
||||
('KM', _('Comoros')),
|
||||
('KN', _('St. Kitts and Nevis')),
|
||||
('KP', _('Korea, Democratic People\'s Republic of')),
|
||||
('KR', _('Korea, Republic of')),
|
||||
('KW', _('Kuwait')),
|
||||
('KY', _('Cayman Islands')),
|
||||
('KZ', _('Kazakhstan')),
|
||||
('LA', _('Lao People\'s Democratic Republic')),
|
||||
('LB', _('Lebanon')),
|
||||
('LC', _('Saint Lucia')),
|
||||
('LI', _('Liechtenstein')),
|
||||
('LK', _('Sri Lanka')),
|
||||
('LR', _('Liberia')),
|
||||
('LS', _('Lesotho')),
|
||||
('LT', _('Lithuania')),
|
||||
('LU', _('Luxembourg')),
|
||||
('LV', _('Latvia')),
|
||||
('LY', _('Libyan Arab Jamahiriya')),
|
||||
('MA', _('Morocco')),
|
||||
('MC', _('Monaco')),
|
||||
('MD', _('Moldova, Republic of')),
|
||||
('MG', _('Madagascar')),
|
||||
('MH', _('Marshall Islands')),
|
||||
('ML', _('Mali')),
|
||||
('MN', _('Mongolia')),
|
||||
('MM', _('Myanmar')),
|
||||
('MO', _('Macau')),
|
||||
('MP', _('Northern Mariana Islands')),
|
||||
('MQ', _('Martinique')),
|
||||
('MR', _('Mauritania')),
|
||||
('MS', _('Monserrat')),
|
||||
('MT', _('Malta')),
|
||||
('MU', _('Mauritius')),
|
||||
('MV', _('Maldives')),
|
||||
('MW', _('Malawi')),
|
||||
('MX', _('Mexico')),
|
||||
('MY', _('Malaysia')),
|
||||
('MZ', _('Mozambique')),
|
||||
('NA', _('Namibia')),
|
||||
('NC', _('New Caledonia')),
|
||||
('NE', _('Niger')),
|
||||
('NF', _('Norfolk Island')),
|
||||
('NG', _('Nigeria')),
|
||||
('NI', _('Nicaragua')),
|
||||
('NL', _('Netherlands')),
|
||||
('NO', _('Norway')),
|
||||
('NP', _('Nepal')),
|
||||
('NR', _('Nauru')),
|
||||
('NU', _('Niue')),
|
||||
('NZ', _('New Zealand')),
|
||||
('OM', _('Oman')),
|
||||
('PA', _('Panama')),
|
||||
('PE', _('Peru')),
|
||||
('PF', _('French Polynesia')),
|
||||
('PG', _('Papua New Guinea')),
|
||||
('PH', _('Philippines')),
|
||||
('PK', _('Pakistan')),
|
||||
('PL', _('Poland')),
|
||||
('PM', _('St. Pierre & Miquelon')),
|
||||
('PN', _('Pitcairn')),
|
||||
('PR', _('Puerto Rico')),
|
||||
('PT', _('Portugal')),
|
||||
('PW', _('Palau')),
|
||||
('PY', _('Paraguay')),
|
||||
('QA', _('Qatar')),
|
||||
('RE', _('Reunion')),
|
||||
('RO', _('Romania')),
|
||||
('RU', _('Russian Federation')),
|
||||
('RW', _('Rwanda')),
|
||||
('SA', _('Saudi Arabia')),
|
||||
('SB', _('Solomon Islands')),
|
||||
('SC', _('Seychelles')),
|
||||
('SD', _('Sudan')),
|
||||
('SE', _('Sweden')),
|
||||
('SG', _('Singapore')),
|
||||
('SH', _('St. Helena')),
|
||||
('SI', _('Slovenia')),
|
||||
('SJ', _('Svalbard & Jan Mayen Islands')),
|
||||
('SK', _('Slovakia')),
|
||||
('SL', _('Sierra Leone')),
|
||||
('SM', _('San Marino')),
|
||||
('SN', _('Senegal')),
|
||||
('SO', _('Somalia')),
|
||||
('SR', _('Suriname')),
|
||||
('ST', _('Sao Tome & Principe')),
|
||||
('SV', _('El Salvador')),
|
||||
('SY', _('Syrian Arab Republic')),
|
||||
('SZ', _('Swaziland')),
|
||||
('TC', _('Turks & Caicos Islands')),
|
||||
('TD', _('Chad')),
|
||||
('TF', _('French Southern Territories')),
|
||||
('TG', _('Togo')),
|
||||
('TH', _('Thailand')),
|
||||
('TJ', _('Tajikistan')),
|
||||
('TK', _('Tokelau')),
|
||||
('TM', _('Turkmenistan')),
|
||||
('TN', _('Tunisia')),
|
||||
('TO', _('Tonga')),
|
||||
('TP', _('East Timor')),
|
||||
('TR', _('Turkey')),
|
||||
('TT', _('Trinidad & Tobago')),
|
||||
('TV', _('Tuvalu')),
|
||||
('TW', _('Taiwan, Province of China')),
|
||||
('TZ', _('Tanzania, United Republic of')),
|
||||
('UA', _('Ukraine')),
|
||||
('UG', _('Uganda')),
|
||||
('UM', _('United States Minor Outlying Islands')),
|
||||
('US', _('United States of America')),
|
||||
('UY', _('Uruguay')),
|
||||
('UZ', _('Uzbekistan')),
|
||||
('VA', _('Vatican City State (Holy See)')),
|
||||
('VC', _('St. Vincent & the Grenadines')),
|
||||
('VE', _('Venezuela')),
|
||||
('VG', _('British Virgin Islands')),
|
||||
('VI', _('United States Virgin Islands')),
|
||||
('VN', _('Viet Nam')),
|
||||
('VU', _('Vanuatu')),
|
||||
('WF', _('Wallis & Futuna Islands')),
|
||||
('WS', _('Samoa')),
|
||||
('YE', _('Yemen')),
|
||||
('YT', _('Mayotte')),
|
||||
('YU', _('Yugoslavia')),
|
||||
('ZA', _('South Africa')),
|
||||
('ZM', _('Zambia')),
|
||||
('ZR', _('Zaire')),
|
||||
('ZW', _('Zimbabwe')),
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import UncloudProvider, UncloudNetwork
|
||||
|
||||
for m in [ UncloudProvider, UncloudNetwork ]:
|
||||
admin.site.register(m)
|
|
@ -0,0 +1,43 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from uncloud_pay.models import BillingAddress, RecurringPeriod, Product
|
||||
from uncloud.models import UncloudProvider, UncloudNetwork
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add standard uncloud values'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
pass
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Order matters, objects can be dependent on each other
|
||||
|
||||
admin_username="uncloud-admin"
|
||||
pw_length = 32
|
||||
|
||||
# Only set password if the user did not exist before
|
||||
try:
|
||||
admin_user = get_user_model().objects.get(username=settings.UNCLOUD_ADMIN_NAME)
|
||||
except ObjectDoesNotExist:
|
||||
random_password = ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(pw_length))
|
||||
|
||||
admin_user = get_user_model().objects.create_user(username=settings.UNCLOUD_ADMIN_NAME, password=random_password)
|
||||
admin_user.is_superuser=True
|
||||
admin_user.is_staff=True
|
||||
admin_user.save()
|
||||
|
||||
print(f"Created admin user '{admin_username}' with password '{random_password}'")
|
||||
|
||||
BillingAddress.populate_db_defaults()
|
||||
RecurringPeriod.populate_db_defaults()
|
||||
Product.populate_db_defaults()
|
||||
|
||||
UncloudNetwork.populate_db_defaults()
|
||||
UncloudProvider.populate_db_defaults()
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 19:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UncloudProvider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('starting_date', models.DateField()),
|
||||
('ending_date', models.DateField(blank=True)),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('address', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 20:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='uncloudprovider',
|
||||
name='ending_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 20:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0010_auto_20201011_2009'),
|
||||
('uncloud', '0002_auto_20201011_2001'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='uncloudprovider',
|
||||
name='billing_network',
|
||||
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='uncloudproviderbill', to='uncloud_net.uncloudnetwork'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='uncloudprovider',
|
||||
name='referral_network',
|
||||
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='uncloudproviderreferral', to='uncloud_net.uncloudnetwork'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-10-12 17:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0010_auto_20201011_2009'),
|
||||
('uncloud', '0004_auto_20201011_2031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='uncloudprovider',
|
||||
name='coupon_network',
|
||||
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='uncloudprovidercoupon', to='uncloud_net.uncloudnetwork'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.1 on 2020-10-25 19:31
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud', '0005_uncloudprovider_coupon_network'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UncloudNetwork',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('network_address', models.GenericIPAddressField(unique=True)),
|
||||
('network_mask', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])),
|
||||
('description', models.CharField(max_length=256)),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='uncloudprovider',
|
||||
name='billing_network',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uncloudproviderbill', to='uncloud.uncloudnetwork'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='uncloudprovider',
|
||||
name='coupon_network',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uncloudprovidercoupon', to='uncloud.uncloudnetwork'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='uncloudprovider',
|
||||
name='referral_network',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uncloudproviderreferral', to='uncloud.uncloudnetwork'),
|
||||
),
|
||||
]
|
||||
2
|
|
@ -1,7 +1,10 @@
|
|||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
|
||||
from django.db.models import JSONField, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from uncloud import COUNTRIES
|
||||
|
||||
class UncloudModel(models.Model):
|
||||
"""
|
||||
|
@ -34,3 +37,127 @@ class UncloudStatus(models.TextChoices):
|
|||
DELETED = 'DELETED', _('Deleted') # Resource has been deleted
|
||||
DISABLED = 'DISABLED', _('Disabled') # Is usable, but cannot be used for new things
|
||||
UNUSABLE = 'UNUSABLE', _('Unusable'), # Has some kind of error
|
||||
|
||||
|
||||
|
||||
###
|
||||
# General address handling
|
||||
class CountryField(models.CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('choices', COUNTRIES)
|
||||
kwargs.setdefault('default', 'CH')
|
||||
kwargs.setdefault('max_length', 2)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
|
||||
class UncloudAddress(models.Model):
|
||||
full_name = models.CharField(max_length=256)
|
||||
organization = models.CharField(max_length=256, blank=True, null=True)
|
||||
street = models.CharField(max_length=256)
|
||||
city = models.CharField(max_length=256)
|
||||
postal_code = models.CharField(max_length=64)
|
||||
country = CountryField(blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
###
|
||||
# UncloudNetworks are used as identifiers - such they are a base of uncloud
|
||||
|
||||
class UncloudNetwork(models.Model):
|
||||
"""
|
||||
Storing IP networks
|
||||
"""
|
||||
|
||||
network_address = models.GenericIPAddressField(null=False, unique=True)
|
||||
network_mask = models.IntegerField(null=False,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(128)]
|
||||
)
|
||||
|
||||
description = models.CharField(max_length=256)
|
||||
|
||||
@classmethod
|
||||
def populate_db_defaults(cls):
|
||||
for net, desc in [
|
||||
( "2a0a:e5c0:11::", "uncloud Billing" ),
|
||||
( "2a0a:e5c0:11:1::", "uncloud Referral" ),
|
||||
( "2a0a:e5c0:11:2::", "uncloud Coupon" )
|
||||
]:
|
||||
obj, created = cls.objects.get_or_create(network_address=net,
|
||||
defaults= {
|
||||
'network_mask': 64,
|
||||
'description': desc
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not ':' in self.network_address and self.network_mask > 32:
|
||||
raise FieldError("Mask cannot exceed 32 for IPv4")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.network_address}/{self.network_mask} {self.description}"
|
||||
|
||||
###
|
||||
# Who is running / providing this instance of uncloud?
|
||||
|
||||
class UncloudProvider(UncloudAddress):
|
||||
"""
|
||||
A class resembling who is running this uncloud instance.
|
||||
This might change over time so we allow starting/ending dates
|
||||
|
||||
This also defines the taxation rules.
|
||||
|
||||
starting/ending date define from when to when this is valid. This way
|
||||
we can model address changes and have it correct in the bills.
|
||||
"""
|
||||
|
||||
# Meta:
|
||||
# FIXMe: only allow non overlapping time frames -- how to define this as a constraint?
|
||||
starting_date = models.DateField()
|
||||
ending_date = models.DateField(blank=True, null=True)
|
||||
|
||||
billing_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderbill", on_delete=models.CASCADE)
|
||||
referral_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderreferral", on_delete=models.CASCADE)
|
||||
coupon_network = models.ForeignKey(UncloudNetwork, related_name="uncloudprovidercoupon", on_delete=models.CASCADE)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_provider(cls, when=None):
|
||||
"""
|
||||
Find active provide at a certain time - if there was any
|
||||
"""
|
||||
|
||||
if not when:
|
||||
when = timezone.now()
|
||||
|
||||
|
||||
return cls.objects.get(Q(starting_date__gte=when, ending_date__lte=when) |
|
||||
Q(starting_date__gte=when, ending_date__isnull=True))
|
||||
|
||||
|
||||
@classmethod
|
||||
def populate_db_defaults(cls):
|
||||
obj, created = cls.objects.get_or_create(full_name="ungleich glarus ag",
|
||||
street="Bahnhofstrasse 1",
|
||||
postal_code="8783",
|
||||
city="Linthal",
|
||||
country="CH",
|
||||
starting_date=timezone.now(),
|
||||
billing_network=UncloudNetwork.objects.get(description="uncloud Billing"),
|
||||
referral_network=UncloudNetwork.objects.get(description="uncloud Referral"),
|
||||
coupon_network=UncloudNetwork.objects.get(description="uncloud Coupon")
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.full_name} {self.country}"
|
||||
|
|
|
@ -19,8 +19,6 @@ from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
|
|||
|
||||
LOGGING = {}
|
||||
|
||||
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -185,6 +183,8 @@ ALLOWED_HOSTS = []
|
|||
# required for hardcopy / pdf rendering: https://github.com/loftylabs/django-hardcopy
|
||||
CHROME_PATH = '/usr/bin/chromium-browser'
|
||||
|
||||
# Username that is created by default and owns the configuration objects
|
||||
UNCLOUD_ADMIN_NAME = "uncloud-admin"
|
||||
|
||||
# Overwrite settings with local settings, if existing
|
||||
try:
|
||||
|
|
|
@ -12,7 +12,7 @@ from django.conf.urls.static import static
|
|||
from rest_framework import routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
from opennebula import views as oneviews
|
||||
#from opennebula import views as oneviews
|
||||
from uncloud_auth import views as authviews
|
||||
from uncloud_net import views as netviews
|
||||
from uncloud_pay import views as payviews
|
||||
|
@ -60,7 +60,7 @@ router.register(r'v1/admin/order', payviews.AdminOrderViewSet, basename='admin/o
|
|||
router.register(r'v1/admin/vmhost', vmviews.VMHostViewSet)
|
||||
router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet)
|
||||
router.register(r'v1/admin/vpnpool', netviews.VPNPoolViewSet)
|
||||
router.register(r'v1/admin/opennebula', oneviews.VMViewSet, basename='opennebula')
|
||||
#router.register(r'v1/admin/opennebula', oneviews.VMViewSet, basename='opennebula')
|
||||
|
||||
# User/Account
|
||||
router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from .models import ReverseDNSEntry
|
||||
|
||||
# Register your models here.
|
||||
|
||||
for m in [ ReverseDNSEntry ]:
|
||||
admin.site.register(m)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetwork',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0005_remove_vpnnetwork_orders'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetwork',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetwork',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetwork',
|
||||
name='status',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 19:20
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0006_auto_20200928_1858'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UncloudNetwork',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('network_address', models.GenericIPAddressField()),
|
||||
('network_mask', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0007_uncloudnetwork'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='uncloudnetwork',
|
||||
name='network_address',
|
||||
field=models.GenericIPAddressField(unique=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0008_auto_20201011_1924'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='uncloudnetwork',
|
||||
name='description',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 20:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0009_uncloudnetwork_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetworkreservation',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vpnpool',
|
||||
name='extra_data',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.1 on 2020-10-25 19:31
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('uncloud', '0006_auto_20201025_1931'),
|
||||
('uncloud_net', '0010_auto_20201011_2009'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReverseDNSEntry',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip_address', models.GenericIPAddressField(unique=True)),
|
||||
('name', models.CharField(max_length=253)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UncloudNetwork',
|
||||
),
|
||||
]
|
|
@ -4,16 +4,14 @@ import ipaddress
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
|
||||
|
||||
from uncloud_pay.models import Product, RecurringPeriod
|
||||
from uncloud.models import UncloudModel, UncloudStatus
|
||||
|
||||
from uncloud_pay.models import Order
|
||||
|
||||
class MACAdress(models.Model):
|
||||
default_prefix = 0x420000000000
|
||||
|
||||
class VPNPool(UncloudModel):
|
||||
class VPNPool(models.Model):
|
||||
"""
|
||||
Network address pools from which VPNs can be created
|
||||
"""
|
||||
|
@ -145,7 +143,7 @@ AllowedIPs = {peer_network}
|
|||
pass
|
||||
|
||||
|
||||
class VPNNetworkReservation(UncloudModel):
|
||||
class VPNNetworkReservation(models.Model):
|
||||
"""
|
||||
This class tracks the used VPN networks. It will be deleted, when the product is cancelled.
|
||||
"""
|
||||
|
@ -163,7 +161,7 @@ class VPNNetworkReservation(UncloudModel):
|
|||
)
|
||||
|
||||
|
||||
class VPNNetwork(Product):
|
||||
class VPNNetwork(models.Model):
|
||||
"""
|
||||
A selected network. Used for tracking reservations / used networks
|
||||
"""
|
||||
|
@ -173,7 +171,7 @@ class VPNNetwork(Product):
|
|||
|
||||
wireguard_public_key = models.CharField(max_length=48)
|
||||
|
||||
default_recurring_period = RecurringPeriod.PER_365D
|
||||
# default_recurring_period = RecurringPeriod.PER_365D
|
||||
|
||||
@property
|
||||
def recurring_price(self):
|
||||
|
@ -185,3 +183,42 @@ class VPNNetwork(Product):
|
|||
self.network.save()
|
||||
super().save(*args, **kwargs)
|
||||
print("deleted {}".format(self))
|
||||
|
||||
|
||||
class ReverseDNSEntry(models.Model):
|
||||
"""
|
||||
A reverse DNS entry
|
||||
"""
|
||||
owner = models.ForeignKey(get_user_model(),
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
ip_address = models.GenericIPAddressField(null=False, unique=True)
|
||||
|
||||
name = models.CharField(max_length=253, null=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Product.objects.filter(config__parameters__contains='reverse_dns_network')
|
||||
# FIXME: check if order is still active / not replaced
|
||||
|
||||
allowed = False
|
||||
|
||||
for order in Order.objects.filter(config__parameters__reverse_dns_network__isnull=False,
|
||||
owner=self.owner):
|
||||
network = order.config['parameters']['reverse_dns_network']
|
||||
|
||||
net = ipaddress.ip_network(network)
|
||||
addr = ipaddress.ip_address(self.ip_address)
|
||||
|
||||
if addr in net:
|
||||
allowed = True
|
||||
break
|
||||
|
||||
|
||||
if not allowed:
|
||||
raise ValidationError(f"User {self.owner} does not have the right to create reverse DNS entry for {self.ip_address}")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ip_address} - {self.name}"
|
||||
|
|
|
@ -3,13 +3,20 @@ from rest_framework.test import APIRequestFactory, force_authenticate
|
|||
|
||||
from rest_framework.reverse import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, FieldError
|
||||
|
||||
from .views import *
|
||||
from .models import *
|
||||
|
||||
from uncloud_pay.models import BillingAddress, Order
|
||||
|
||||
|
||||
class UncloudNetworkTests(TestCase):
|
||||
def test_invalid_IPv4_network(self):
|
||||
with self.assertRaises(FieldError):
|
||||
UncloudNetwork.objects.create(network_address="192.168.1.0",
|
||||
network_mask=33)
|
||||
|
||||
class VPNTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user('django-test-user', 'noreply@ungleich.ch')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import decimal
|
||||
|
||||
# Define DecimalField properties, used to represent amounts of money.
|
||||
|
@ -6,245 +6,3 @@ AMOUNT_MAX_DIGITS=10
|
|||
AMOUNT_DECIMALS=2
|
||||
|
||||
decimal.getcontext().prec = AMOUNT_DECIMALS
|
||||
|
||||
# http://xml.coverpages.org/country3166.html
|
||||
COUNTRIES = (
|
||||
('AD', _('Andorra')),
|
||||
('AE', _('United Arab Emirates')),
|
||||
('AF', _('Afghanistan')),
|
||||
('AG', _('Antigua & Barbuda')),
|
||||
('AI', _('Anguilla')),
|
||||
('AL', _('Albania')),
|
||||
('AM', _('Armenia')),
|
||||
('AN', _('Netherlands Antilles')),
|
||||
('AO', _('Angola')),
|
||||
('AQ', _('Antarctica')),
|
||||
('AR', _('Argentina')),
|
||||
('AS', _('American Samoa')),
|
||||
('AT', _('Austria')),
|
||||
('AU', _('Australia')),
|
||||
('AW', _('Aruba')),
|
||||
('AZ', _('Azerbaijan')),
|
||||
('BA', _('Bosnia and Herzegovina')),
|
||||
('BB', _('Barbados')),
|
||||
('BD', _('Bangladesh')),
|
||||
('BE', _('Belgium')),
|
||||
('BF', _('Burkina Faso')),
|
||||
('BG', _('Bulgaria')),
|
||||
('BH', _('Bahrain')),
|
||||
('BI', _('Burundi')),
|
||||
('BJ', _('Benin')),
|
||||
('BM', _('Bermuda')),
|
||||
('BN', _('Brunei Darussalam')),
|
||||
('BO', _('Bolivia')),
|
||||
('BR', _('Brazil')),
|
||||
('BS', _('Bahama')),
|
||||
('BT', _('Bhutan')),
|
||||
('BV', _('Bouvet Island')),
|
||||
('BW', _('Botswana')),
|
||||
('BY', _('Belarus')),
|
||||
('BZ', _('Belize')),
|
||||
('CA', _('Canada')),
|
||||
('CC', _('Cocos (Keeling) Islands')),
|
||||
('CF', _('Central African Republic')),
|
||||
('CG', _('Congo')),
|
||||
('CH', _('Switzerland')),
|
||||
('CI', _('Ivory Coast')),
|
||||
('CK', _('Cook Iislands')),
|
||||
('CL', _('Chile')),
|
||||
('CM', _('Cameroon')),
|
||||
('CN', _('China')),
|
||||
('CO', _('Colombia')),
|
||||
('CR', _('Costa Rica')),
|
||||
('CU', _('Cuba')),
|
||||
('CV', _('Cape Verde')),
|
||||
('CX', _('Christmas Island')),
|
||||
('CY', _('Cyprus')),
|
||||
('CZ', _('Czech Republic')),
|
||||
('DE', _('Germany')),
|
||||
('DJ', _('Djibouti')),
|
||||
('DK', _('Denmark')),
|
||||
('DM', _('Dominica')),
|
||||
('DO', _('Dominican Republic')),
|
||||
('DZ', _('Algeria')),
|
||||
('EC', _('Ecuador')),
|
||||
('EE', _('Estonia')),
|
||||
('EG', _('Egypt')),
|
||||
('EH', _('Western Sahara')),
|
||||
('ER', _('Eritrea')),
|
||||
('ES', _('Spain')),
|
||||
('ET', _('Ethiopia')),
|
||||
('FI', _('Finland')),
|
||||
('FJ', _('Fiji')),
|
||||
('FK', _('Falkland Islands (Malvinas)')),
|
||||
('FM', _('Micronesia')),
|
||||
('FO', _('Faroe Islands')),
|
||||
('FR', _('France')),
|
||||
('FX', _('France, Metropolitan')),
|
||||
('GA', _('Gabon')),
|
||||
('GB', _('United Kingdom (Great Britain)')),
|
||||
('GD', _('Grenada')),
|
||||
('GE', _('Georgia')),
|
||||
('GF', _('French Guiana')),
|
||||
('GH', _('Ghana')),
|
||||
('GI', _('Gibraltar')),
|
||||
('GL', _('Greenland')),
|
||||
('GM', _('Gambia')),
|
||||
('GN', _('Guinea')),
|
||||
('GP', _('Guadeloupe')),
|
||||
('GQ', _('Equatorial Guinea')),
|
||||
('GR', _('Greece')),
|
||||
('GS', _('South Georgia and the South Sandwich Islands')),
|
||||
('GT', _('Guatemala')),
|
||||
('GU', _('Guam')),
|
||||
('GW', _('Guinea-Bissau')),
|
||||
('GY', _('Guyana')),
|
||||
('HK', _('Hong Kong')),
|
||||
('HM', _('Heard & McDonald Islands')),
|
||||
('HN', _('Honduras')),
|
||||
('HR', _('Croatia')),
|
||||
('HT', _('Haiti')),
|
||||
('HU', _('Hungary')),
|
||||
('ID', _('Indonesia')),
|
||||
('IE', _('Ireland')),
|
||||
('IL', _('Israel')),
|
||||
('IN', _('India')),
|
||||
('IO', _('British Indian Ocean Territory')),
|
||||
('IQ', _('Iraq')),
|
||||
('IR', _('Islamic Republic of Iran')),
|
||||
('IS', _('Iceland')),
|
||||
('IT', _('Italy')),
|
||||
('JM', _('Jamaica')),
|
||||
('JO', _('Jordan')),
|
||||
('JP', _('Japan')),
|
||||
('KE', _('Kenya')),
|
||||
('KG', _('Kyrgyzstan')),
|
||||
('KH', _('Cambodia')),
|
||||
('KI', _('Kiribati')),
|
||||
('KM', _('Comoros')),
|
||||
('KN', _('St. Kitts and Nevis')),
|
||||
('KP', _('Korea, Democratic People\'s Republic of')),
|
||||
('KR', _('Korea, Republic of')),
|
||||
('KW', _('Kuwait')),
|
||||
('KY', _('Cayman Islands')),
|
||||
('KZ', _('Kazakhstan')),
|
||||
('LA', _('Lao People\'s Democratic Republic')),
|
||||
('LB', _('Lebanon')),
|
||||
('LC', _('Saint Lucia')),
|
||||
('LI', _('Liechtenstein')),
|
||||
('LK', _('Sri Lanka')),
|
||||
('LR', _('Liberia')),
|
||||
('LS', _('Lesotho')),
|
||||
('LT', _('Lithuania')),
|
||||
('LU', _('Luxembourg')),
|
||||
('LV', _('Latvia')),
|
||||
('LY', _('Libyan Arab Jamahiriya')),
|
||||
('MA', _('Morocco')),
|
||||
('MC', _('Monaco')),
|
||||
('MD', _('Moldova, Republic of')),
|
||||
('MG', _('Madagascar')),
|
||||
('MH', _('Marshall Islands')),
|
||||
('ML', _('Mali')),
|
||||
('MN', _('Mongolia')),
|
||||
('MM', _('Myanmar')),
|
||||
('MO', _('Macau')),
|
||||
('MP', _('Northern Mariana Islands')),
|
||||
('MQ', _('Martinique')),
|
||||
('MR', _('Mauritania')),
|
||||
('MS', _('Monserrat')),
|
||||
('MT', _('Malta')),
|
||||
('MU', _('Mauritius')),
|
||||
('MV', _('Maldives')),
|
||||
('MW', _('Malawi')),
|
||||
('MX', _('Mexico')),
|
||||
('MY', _('Malaysia')),
|
||||
('MZ', _('Mozambique')),
|
||||
('NA', _('Namibia')),
|
||||
('NC', _('New Caledonia')),
|
||||
('NE', _('Niger')),
|
||||
('NF', _('Norfolk Island')),
|
||||
('NG', _('Nigeria')),
|
||||
('NI', _('Nicaragua')),
|
||||
('NL', _('Netherlands')),
|
||||
('NO', _('Norway')),
|
||||
('NP', _('Nepal')),
|
||||
('NR', _('Nauru')),
|
||||
('NU', _('Niue')),
|
||||
('NZ', _('New Zealand')),
|
||||
('OM', _('Oman')),
|
||||
('PA', _('Panama')),
|
||||
('PE', _('Peru')),
|
||||
('PF', _('French Polynesia')),
|
||||
('PG', _('Papua New Guinea')),
|
||||
('PH', _('Philippines')),
|
||||
('PK', _('Pakistan')),
|
||||
('PL', _('Poland')),
|
||||
('PM', _('St. Pierre & Miquelon')),
|
||||
('PN', _('Pitcairn')),
|
||||
('PR', _('Puerto Rico')),
|
||||
('PT', _('Portugal')),
|
||||
('PW', _('Palau')),
|
||||
('PY', _('Paraguay')),
|
||||
('QA', _('Qatar')),
|
||||
('RE', _('Reunion')),
|
||||
('RO', _('Romania')),
|
||||
('RU', _('Russian Federation')),
|
||||
('RW', _('Rwanda')),
|
||||
('SA', _('Saudi Arabia')),
|
||||
('SB', _('Solomon Islands')),
|
||||
('SC', _('Seychelles')),
|
||||
('SD', _('Sudan')),
|
||||
('SE', _('Sweden')),
|
||||
('SG', _('Singapore')),
|
||||
('SH', _('St. Helena')),
|
||||
('SI', _('Slovenia')),
|
||||
('SJ', _('Svalbard & Jan Mayen Islands')),
|
||||
('SK', _('Slovakia')),
|
||||
('SL', _('Sierra Leone')),
|
||||
('SM', _('San Marino')),
|
||||
('SN', _('Senegal')),
|
||||
('SO', _('Somalia')),
|
||||
('SR', _('Suriname')),
|
||||
('ST', _('Sao Tome & Principe')),
|
||||
('SV', _('El Salvador')),
|
||||
('SY', _('Syrian Arab Republic')),
|
||||
('SZ', _('Swaziland')),
|
||||
('TC', _('Turks & Caicos Islands')),
|
||||
('TD', _('Chad')),
|
||||
('TF', _('French Southern Territories')),
|
||||
('TG', _('Togo')),
|
||||
('TH', _('Thailand')),
|
||||
('TJ', _('Tajikistan')),
|
||||
('TK', _('Tokelau')),
|
||||
('TM', _('Turkmenistan')),
|
||||
('TN', _('Tunisia')),
|
||||
('TO', _('Tonga')),
|
||||
('TP', _('East Timor')),
|
||||
('TR', _('Turkey')),
|
||||
('TT', _('Trinidad & Tobago')),
|
||||
('TV', _('Tuvalu')),
|
||||
('TW', _('Taiwan, Province of China')),
|
||||
('TZ', _('Tanzania, United Republic of')),
|
||||
('UA', _('Ukraine')),
|
||||
('UG', _('Uganda')),
|
||||
('UM', _('United States Minor Outlying Islands')),
|
||||
('US', _('United States of America')),
|
||||
('UY', _('Uruguay')),
|
||||
('UZ', _('Uzbekistan')),
|
||||
('VA', _('Vatican City State (Holy See)')),
|
||||
('VC', _('St. Vincent & the Grenadines')),
|
||||
('VE', _('Venezuela')),
|
||||
('VG', _('British Virgin Islands')),
|
||||
('VI', _('United States Virgin Islands')),
|
||||
('VN', _('Viet Nam')),
|
||||
('VU', _('Vanuatu')),
|
||||
('WF', _('Wallis & Futuna Islands')),
|
||||
('WS', _('Samoa')),
|
||||
('YE', _('Yemen')),
|
||||
('YT', _('Mayotte')),
|
||||
('YU', _('Yugoslavia')),
|
||||
('ZA', _('South Africa')),
|
||||
('ZM', _('Zambia')),
|
||||
('ZR', _('Zaire')),
|
||||
('ZW', _('Zimbabwe')),
|
||||
)
|
||||
|
|
|
@ -11,19 +11,17 @@ from django.http import FileResponse
|
|||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress
|
||||
from uncloud_pay.models import *
|
||||
|
||||
|
||||
class BillRecordInline(admin.TabularInline):
|
||||
# model = Bill.bill_records.through
|
||||
model = BillRecord
|
||||
|
||||
# AT some point in the future: expose REPLACED and orders that depend on us
|
||||
# class OrderInline(admin.TabularInline):
|
||||
# model = Order
|
||||
# fk_name = "replaces"
|
||||
# class OrderAdmin(admin.ModelAdmin):
|
||||
# inlines = [ OrderInline ]
|
||||
class RecurringPeriodInline(admin.TabularInline):
|
||||
model = ProductToRecurringPeriod
|
||||
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
inlines = [ RecurringPeriodInline ]
|
||||
|
||||
class BillAdmin(admin.ModelAdmin):
|
||||
inlines = [ BillRecordInline ]
|
||||
|
@ -87,9 +85,8 @@ class BillAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(Bill, BillAdmin)
|
||||
admin.site.register(Order)
|
||||
admin.site.register(BillRecord)
|
||||
admin.site.register(BillingAddress)
|
||||
admin.site.register(ProductToRecurringPeriod)
|
||||
admin.site.register(Product, ProductAdmin)
|
||||
|
||||
|
||||
#admin.site.register(Order, OrderAdmin)
|
||||
for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate ]:
|
||||
admin.site.register(m)
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Customer tests
|
||||
customer-*.py
|
|
@ -1,44 +1,35 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from uncloud_pay.models import VATRate
|
||||
import csv
|
||||
|
||||
import urllib
|
||||
import csv
|
||||
import sys
|
||||
import io
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv'''
|
||||
vat_url = "https://raw.githubusercontent.com/ungleich/vat-rates/main/vat_rates.csv"
|
||||
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('csv_file', nargs='+', type=str)
|
||||
parser.add_argument('--vat-url', default=self.vat_url)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
for c_file in options['csv_file']:
|
||||
print("c_file = %s" % c_file)
|
||||
with open(c_file, mode='r') as csv_file:
|
||||
csv_reader = csv.DictReader(csv_file)
|
||||
line_count = 0
|
||||
for row in csv_reader:
|
||||
if line_count == 0:
|
||||
line_count += 1
|
||||
obj, created = VATRate.objects.get_or_create(
|
||||
start_date=row["start_date"],
|
||||
stop_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
||||
territory_codes=row["territory_codes"],
|
||||
currency_code=row["currency_code"],
|
||||
rate=row["rate"],
|
||||
rate_type=row["rate_type"],
|
||||
description=row["description"]
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'%s. %s - %s - %s - %s' % (
|
||||
line_count,
|
||||
obj.start_date,
|
||||
obj.stop_date,
|
||||
obj.territory_codes,
|
||||
obj.rate
|
||||
)
|
||||
))
|
||||
line_count+=1
|
||||
vat_url = options['vat_url']
|
||||
url_open = urllib.request.urlopen(vat_url)
|
||||
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
||||
# map to fileio using stringIO
|
||||
csv_file = io.StringIO(url_open.read().decode('utf-8'))
|
||||
reader = csv.DictReader(csv_file)
|
||||
|
||||
for row in reader:
|
||||
# print(row)
|
||||
obj, created = VATRate.objects.get_or_create(
|
||||
starting_date=row["start_date"],
|
||||
ending_date=row["stop_date"] if row["stop_date"] != "" else None,
|
||||
territory_codes=row["territory_codes"],
|
||||
currency_code=row["currency_code"],
|
||||
rate=row["rate"],
|
||||
rate_type=row["rate_type"],
|
||||
description=row["description"]
|
||||
)
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.1 on 2020-08-25 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0013_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='ot_price',
|
||||
field=models.IntegerField(default=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='rc_price',
|
||||
field=models.IntegerField(default=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='ot_price',
|
||||
field=models.IntegerField(default=5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='rc_price',
|
||||
field=models.IntegerField(default=10),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0014_auto_20200825_1915'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,65 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:58
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('uncloud_pay', '0015_auto_20200928_1844'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='status',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('extra_data', models.JSONField(blank=True, editable=False, null=True)),
|
||||
('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)),
|
||||
('config', models.JSONField()),
|
||||
('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0016_auto_20200928_1858'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='config',
|
||||
field=models.JSONField(default={}),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0017_order_config'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='product',
|
||||
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0018_order_product'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='owner',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0019_remove_product_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='status',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='description',
|
||||
field=models.CharField(default='', max_length=1024),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='name',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0020_auto_20200928_1915'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='default_currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='default_recurring_period',
|
||||
field=models.IntegerField(choices=[(31536000, 'Per 365 days'), (2592000, 'Per 30 days'), (604800, 'Per Week'), (86400, 'Per Day'), (3600, 'Per Hour'), (60, 'Per Minute'), (1, 'Per Second'), (0, 'Onetime')], default=2592000),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0021_auto_20200928_1932'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='product',
|
||||
old_name='default_currency',
|
||||
new_name='currency',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0022_auto_20200928_1932'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='order',
|
||||
old_name='price',
|
||||
new_name='one_time_price',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 19:45
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0023_auto_20200928_1944'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='recurring_price',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 20:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0024_auto_20200928_1945'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='billrecord',
|
||||
name='is_recurring_record',
|
||||
field=models.BooleanField(default=False),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 20:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0025_billrecord_is_recurring_record'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='should_be_billed',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.1 on 2020-10-06 13:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0026_order_should_be_billed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RecurringPeriod',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('duration_seconds', models.IntegerField(unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SampleOneTimeProduct',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SampleRecurringProduct',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='SampleRecurringProductOneTimeFee',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='recurring_period',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='default_recurring_period',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 3.1 on 2020-10-06 15:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0027_auto_20201006_1319'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='default_recurring_period',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductToRecurringPeriod',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_default', models.BooleanField(default=False)),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product')),
|
||||
('recurring_period', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='recurring_periods',
|
||||
field=models.ManyToManyField(through='uncloud_pay.ProductToRecurringPeriod', to='uncloud_pay.RecurringPeriod'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='producttorecurringperiod',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(is_default=True), fields=('recurring_period', 'product'), name='one_default_recurring_period_per_product'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1 on 2020-10-06 15:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0028_auto_20201006_1529'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='name',
|
||||
field=models.CharField(max_length=256, unique=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-10-06 16:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0029_auto_20201006_1540'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='producttorecurringperiod',
|
||||
name='one_default_recurring_period_per_product',
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='producttorecurringperiod',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(is_default=True), fields=('product',), name='one_default_recurring_period_per_product'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-10-06 16:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0030_auto_20201006_1640'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='producttorecurringperiod',
|
||||
constraint=models.UniqueConstraint(fields=('product', 'recurring_period'), name='recurring_period_once_per_product'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 15:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0031_auto_20201006_1655'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UncloudProvider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('valid_from', models.DateField()),
|
||||
('valid_to', models.DateField(blank=True)),
|
||||
('billing_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.billingaddress')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 20:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0032_uncloudprovider'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UncloudProvider',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.1 on 2020-10-11 20:31
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0033_auto_20201011_2003'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='city',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='country',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='postal_code',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='billingaddress',
|
||||
name='street',
|
||||
),
|
||||
]
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -82,7 +82,7 @@ class BillRecordSerializer(serializers.Serializer):
|
|||
description = serializers.CharField()
|
||||
one_time_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
recurring_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
recurring_period = serializers.ChoiceField(choices=RecurringPeriod.choices)
|
||||
# recurring_period = serializers.ChoiceField()
|
||||
recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
vat_rate = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
vat_amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
Icons, fonts, etc. are INLINED. This is rather ugly, but as the PDF
|
||||
generation is based on a local snapshot of the HTML file, URLs are
|
||||
screwed if they are not absolute.
|
||||
screwed if they are not absolute to the *local* filesystem.
|
||||
|
||||
As this document is used ONLY for bills and ONLY for downloading, I
|
||||
decided that this is an acceptable uglyness.
|
||||
|
@ -36,7 +36,6 @@
|
|||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
font-size: 14px;
|
||||
width: 600px;
|
||||
margin: auto;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 15px;
|
||||
|
@ -696,9 +695,9 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Units</th>
|
||||
<th>Price/Unit</th>
|
||||
<th class="tr">Total price</tH>
|
||||
<th>Units</th>
|
||||
<th>Total price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -708,8 +707,8 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
- {{ record.ending_date|date:"c" }}
|
||||
{{ record.order }}
|
||||
</td>
|
||||
<td>{{ record.price|floatformat:2 }}</td>
|
||||
<td>{{ record.quantity|floatformat:2 }}</td>
|
||||
<td>{{ record.order.price|floatformat:2 }}</td>
|
||||
<td>{{ record.sum|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -0,0 +1,721 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from datetime import datetime, date, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import *
|
||||
from uncloud_service.models import GenericServiceProduct
|
||||
|
||||
class OrderTestCase(TestCase):
|
||||
"""
|
||||
The heart of ordering products
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create(
|
||||
username='random_user',
|
||||
email='jane.random@domain.tld')
|
||||
|
||||
self.ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=True)
|
||||
|
||||
def test_create_one_time_product(self):
|
||||
"""
|
||||
One time payment products cannot be updated - can they?
|
||||
"""
|
||||
|
||||
p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||
|
||||
self.assertEqual(p.one_time_price, 5)
|
||||
self.assertEqual(p.recurring_price, 0)
|
||||
|
||||
|
||||
# class ProductTestCase(TestCase):
|
||||
# """
|
||||
# Test products and products <-> order interaction
|
||||
# """
|
||||
|
||||
# def setUp(self):
|
||||
# self.user = get_user_model().objects.create(
|
||||
# username='random_user',
|
||||
# email='jane.random@domain.tld')
|
||||
|
||||
# self.ba = BillingAddress.objects.create(
|
||||
# owner=self.user,
|
||||
# organization = 'Test org',
|
||||
# street="unknown",
|
||||
# city="unknown",
|
||||
# postal_code="somewhere else",
|
||||
# active=True)
|
||||
|
||||
# def test_create_one_time_product(self):
|
||||
# """
|
||||
# One time payment products cannot be updated - can they?
|
||||
# """
|
||||
|
||||
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||
|
||||
# self.assertEqual(p.one_time_price, 5)
|
||||
# self.assertEqual(p.recurring_price, 0)
|
||||
|
||||
# def test_create_product_without_active_billing_address(self):
|
||||
# """
|
||||
# Fail to create a product without an active billing address
|
||||
# """
|
||||
|
||||
# self.ba.active = False
|
||||
# self.ba.save()
|
||||
|
||||
# with self.assertRaises(ValidationError):
|
||||
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||
|
||||
# def test_create_product_without_billing_address(self):
|
||||
# """
|
||||
# Fail to create a product without a billing address
|
||||
# """
|
||||
|
||||
# user2 = get_user_model().objects.create(
|
||||
# username='random_user2',
|
||||
# email='jane.randomly@domain.tld')
|
||||
|
||||
# with self.assertRaises(ValidationError):
|
||||
# p = SampleOneTimeProduct.objects.create(owner=user2)
|
||||
|
||||
|
||||
# def test_create_order_creates_correct_order_count(self):
|
||||
# """
|
||||
# Ensure creating orders from product only creates 1 order
|
||||
# """
|
||||
|
||||
# # One order
|
||||
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
|
||||
# order_count = Order.objects.filter(owner=self.user).count()
|
||||
# self.assertEqual(order_count, 1)
|
||||
|
||||
# # One more order
|
||||
# p = SampleRecurringProduct.objects.create(owner=self.user)
|
||||
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
|
||||
# order_count = Order.objects.filter(owner=self.user).count()
|
||||
# self.assertEqual(order_count, 2)
|
||||
|
||||
# # Should create 2 orders
|
||||
# p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user)
|
||||
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
|
||||
# order_count = Order.objects.filter(owner=self.user).count()
|
||||
# self.assertEqual(order_count, 4)
|
||||
|
||||
|
||||
# def test_update_recurring_order(self):
|
||||
# """
|
||||
# Ensure creating orders from product only creates 1 order
|
||||
# """
|
||||
|
||||
# p = SampleRecurringProduct.objects.create(owner=self.user)
|
||||
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
|
||||
# p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,4)))
|
||||
|
||||
# # FIXME: where is the assert?
|
||||
|
||||
|
||||
class BillingAddressTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create(
|
||||
username='random_user',
|
||||
email='jane.random@domain.tld')
|
||||
|
||||
|
||||
def test_user_no_address(self):
|
||||
"""
|
||||
Raise an error, when there is no address
|
||||
"""
|
||||
|
||||
self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist,
|
||||
BillingAddress.get_address_for,
|
||||
self.user)
|
||||
|
||||
def test_user_only_inactive_address(self):
|
||||
"""
|
||||
Raise an error, when there is no active address
|
||||
"""
|
||||
|
||||
ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=False)
|
||||
|
||||
self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist,
|
||||
BillingAddress.get_address_for,
|
||||
self.user)
|
||||
|
||||
def test_find_active_address(self):
|
||||
"""
|
||||
Find the active address
|
||||
"""
|
||||
|
||||
ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
|
||||
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||
|
||||
def test_find_right_address_with_multiple_addresses(self):
|
||||
"""
|
||||
Find the active address only, skip inactive
|
||||
"""
|
||||
|
||||
ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
ba2 = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=False)
|
||||
|
||||
|
||||
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||
|
||||
def test_change_addresses(self):
|
||||
"""
|
||||
Switch the active address
|
||||
"""
|
||||
|
||||
ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||
|
||||
ba.active=False
|
||||
ba.save()
|
||||
|
||||
ba2 = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=True)
|
||||
|
||||
self.assertEqual(BillingAddress.get_address_for(self.user), ba2)
|
||||
|
||||
|
||||
|
||||
class BillTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user_without_address = get_user_model().objects.create(
|
||||
username='no_home_person',
|
||||
email='far.away@domain.tld')
|
||||
|
||||
self.user = get_user_model().objects.create(
|
||||
username='jdoe',
|
||||
email='john.doe@domain.tld')
|
||||
|
||||
self.recurring_user = get_user_model().objects.create(
|
||||
username='recurrent_product_user',
|
||||
email='jane.doe@domain.tld')
|
||||
|
||||
self.user_addr = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
self.recurring_user_addr = BillingAddress.objects.create(
|
||||
owner=self.recurring_user,
|
||||
organization = 'Test org',
|
||||
street="Somewhere",
|
||||
city="Else",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
self.order_meta = {}
|
||||
self.order_meta[1] = {
|
||||
'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)),
|
||||
'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)),
|
||||
'price': 15,
|
||||
'description': 'One chocolate bar'
|
||||
}
|
||||
|
||||
self.one_time_order = Order.objects.create(
|
||||
owner=self.user,
|
||||
starting_date=self.order_meta[1]['starting_date'],
|
||||
ending_date=self.order_meta[1]['ending_date'],
|
||||
recurring_period=RecurringPeriod.ONE_TIME,
|
||||
price=self.order_meta[1]['price'],
|
||||
description=self.order_meta[1]['description'],
|
||||
billing_address=BillingAddress.get_address_for(self.user))
|
||||
|
||||
self.recurring_order = Order.objects.create(
|
||||
owner=self.recurring_user,
|
||||
starting_date=timezone.make_aware(datetime.datetime(2020,3,3)),
|
||||
recurring_period=RecurringPeriod.PER_30D,
|
||||
price=15,
|
||||
description="A pretty VM",
|
||||
billing_address=BillingAddress.get_address_for(self.recurring_user)
|
||||
)
|
||||
|
||||
# used for generating multiple bills
|
||||
self.bill_dates = [
|
||||
timezone.make_aware(datetime.datetime(2020,3,31)),
|
||||
timezone.make_aware(datetime.datetime(2020,4,30)),
|
||||
timezone.make_aware(datetime.datetime(2020,5,31)),
|
||||
]
|
||||
|
||||
|
||||
|
||||
def test_bill_one_time_one_bill_record(self):
|
||||
"""
|
||||
Ensure there is only 1 bill record per order
|
||||
"""
|
||||
|
||||
bill = Bill.create_next_bill_for_user_address(self.user_addr)
|
||||
|
||||
self.assertEqual(self.one_time_order.billrecord_set.count(), 1)
|
||||
|
||||
def test_bill_sum_onetime(self):
|
||||
"""
|
||||
Check the bill sum for a single one time order
|
||||
"""
|
||||
|
||||
bill = Bill.create_next_bill_for_user_address(self.user_addr)
|
||||
self.assertEqual(bill.sum, self.order_meta[1]['price'])
|
||||
|
||||
|
||||
def test_bill_creates_record_for_recurring_order(self):
|
||||
"""
|
||||
Ensure there is only 1 bill record per order
|
||||
"""
|
||||
|
||||
bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr)
|
||||
|
||||
self.assertEqual(self.recurring_order.billrecord_set.count(), 1)
|
||||
self.assertEqual(bill.billrecord_set.count(), 1)
|
||||
|
||||
|
||||
def test_new_bill_after_closing(self):
|
||||
"""
|
||||
After closing a bill and the user has a recurring product,
|
||||
the next bill run should create e new bill
|
||||
"""
|
||||
|
||||
for ending_date in self.bill_dates:
|
||||
b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date)
|
||||
b.close()
|
||||
|
||||
bill_count = Bill.objects.filter(owner=self.recurring_user).count()
|
||||
|
||||
self.assertEqual(len(self.bill_dates), bill_count)
|
||||
|
||||
def test_multi_addr_multi_bill(self):
|
||||
"""
|
||||
Ensure multiple bills are created if orders exist with different billing addresses
|
||||
"""
|
||||
|
||||
username="lotsofplaces"
|
||||
multi_addr_user = get_user_model().objects.create(
|
||||
username=username,
|
||||
email=f"{username}@example.org")
|
||||
|
||||
user_addr1 = BillingAddress.objects.create(
|
||||
owner=multi_addr_user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="unknown",
|
||||
active=True)
|
||||
|
||||
order1 = Order.objects.create(
|
||||
owner=multi_addr_user,
|
||||
starting_date=self.order_meta[1]['starting_date'],
|
||||
ending_date=self.order_meta[1]['ending_date'],
|
||||
recurring_period=RecurringPeriod.ONE_TIME,
|
||||
price=self.order_meta[1]['price'],
|
||||
description=self.order_meta[1]['description'],
|
||||
billing_address=BillingAddress.get_address_for(self.user))
|
||||
|
||||
# Make this address inactive
|
||||
user_addr1.active = False
|
||||
user_addr1.save()
|
||||
|
||||
user_addr2 = BillingAddress.objects.create(
|
||||
owner=multi_addr_user,
|
||||
organization = 'Test2 org',
|
||||
street="unknown2",
|
||||
city="unknown2",
|
||||
postal_code="unknown2",
|
||||
active=True)
|
||||
|
||||
order2 = Order.objects.create(
|
||||
owner=multi_addr_user,
|
||||
starting_date=self.order_meta[1]['starting_date'],
|
||||
ending_date=self.order_meta[1]['ending_date'],
|
||||
recurring_period=RecurringPeriod.ONE_TIME,
|
||||
price=self.order_meta[1]['price'],
|
||||
description=self.order_meta[1]['description'],
|
||||
billing_address=BillingAddress.get_address_for(self.user))
|
||||
|
||||
|
||||
bills = Bill.create_next_bills_for_user(multi_addr_user)
|
||||
|
||||
self.assertEqual(len(bills), 2)
|
||||
|
||||
|
||||
# TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing"
|
||||
# def test_skip_disabled_orders(self):
|
||||
# """
|
||||
# Ensure that a bill only considers "active" orders
|
||||
# """
|
||||
|
||||
# self.assertEqual(1, 2)
|
||||
|
||||
|
||||
class ModifyProductTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create(
|
||||
username='random_user',
|
||||
email='jane.random@domain.tld')
|
||||
|
||||
self.ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=True)
|
||||
|
||||
def test_no_pro_rata_first_bill(self):
|
||||
"""
|
||||
The bill should NOT contain a partial amount -- this is a BILL TEST :-)
|
||||
"""
|
||||
|
||||
price = 5
|
||||
|
||||
# Standard 30d recurring product
|
||||
product = SampleRecurringProduct.objects.create(owner=self.user,
|
||||
rc_price=price)
|
||||
|
||||
starting_date = timezone.make_aware(datetime.datetime(2020,3,3))
|
||||
ending_date = timezone.make_aware(datetime.datetime(2020,3,31))
|
||||
time_diff = (ending_date - starting_date).total_seconds()
|
||||
|
||||
product.create_order(starting_date)
|
||||
|
||||
bills = Bill.create_next_bills_for_user(self.user,
|
||||
ending_date=ending_date)
|
||||
|
||||
|
||||
# We expect 1 bill for 1 billing address and 1 time frame
|
||||
self.assertEqual(len(bills), 1)
|
||||
|
||||
pro_rata_amount = time_diff / product.default_recurring_period.value
|
||||
|
||||
self.assertNotEqual(bills[0].sum, pro_rata_amount * price)
|
||||
self.assertEqual(bills[0].sum, price)
|
||||
|
||||
|
||||
def test_downgrade_product(self):
|
||||
"""
|
||||
Test downgrading behaviour:
|
||||
|
||||
We create a recurring product (recurring time: 30 days) and downgrade after 15 days.
|
||||
|
||||
We create the bill right AFTER the end of the first order.
|
||||
|
||||
Expected result:
|
||||
|
||||
- First bill record for 30 days
|
||||
- Second bill record starting after 30 days
|
||||
- Bill contains two bill records
|
||||
|
||||
"""
|
||||
|
||||
user = self.user
|
||||
|
||||
|
||||
|
||||
starting_price = 10
|
||||
downgrade_price = 5
|
||||
|
||||
|
||||
starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
first_order_should_end_at = starting_date + datetime.timedelta(days=30)
|
||||
change1_date = start_after(starting_date + datetime.timedelta(days=15))
|
||||
bill_ending_date = change1_date + datetime.timedelta(days=1)
|
||||
|
||||
|
||||
product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price)
|
||||
product.create_order(starting_date)
|
||||
|
||||
product.rc_price = downgrade_price
|
||||
product.save()
|
||||
product.create_or_update_recurring_order(when_to_start=change1_date)
|
||||
|
||||
bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date)
|
||||
|
||||
bill = bills[0]
|
||||
bill_records = BillRecord.objects.filter(bill=bill)
|
||||
|
||||
self.assertEqual(len(bill_records), 2)
|
||||
|
||||
self.assertEqual(bill_records[0].starting_date, starting_date)
|
||||
|
||||
self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at)
|
||||
|
||||
# self.assertEqual(bill_records[0].ending_date, first_order_should_end_at)
|
||||
|
||||
# self.assertEqual(bill_records[0].quantity, 1)
|
||||
|
||||
# self.assertEqual(bill_records[1].quantity, 1)
|
||||
# self.assertEqual(int(bill.sum), 15)
|
||||
|
||||
def test_upgrade_product(self):
|
||||
"""
|
||||
Test upgrading behaviour
|
||||
"""
|
||||
|
||||
user = self.user
|
||||
|
||||
# Create product
|
||||
starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
starting_price = 10
|
||||
product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price)
|
||||
product.create_order(starting_date)
|
||||
|
||||
change1_date = start_after(starting_date + datetime.timedelta(days=15))
|
||||
product.rc_price = 20
|
||||
product.save()
|
||||
product.create_or_update_recurring_order(when_to_start=change1_date)
|
||||
|
||||
bill_ending_date = change1_date + datetime.timedelta(days=1)
|
||||
bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date)
|
||||
|
||||
bill = bills[0]
|
||||
bill_records = BillRecord.objects.filter(bill=bill)
|
||||
|
||||
self.assertEqual(len(bill_records), 2)
|
||||
self.assertEqual(bill_records[0].quantity, .5)
|
||||
|
||||
self.assertEqual(bill_records[0].ending_date, end_before(change1_date))
|
||||
|
||||
self.assertEqual(bill_records[1].quantity, 1)
|
||||
self.assertEqual(bill_records[1].starting_date, change1_date)
|
||||
|
||||
self.assertEqual(int(bill.sum), 25)
|
||||
|
||||
|
||||
|
||||
# def test_bill_for_increasing_product(self):
|
||||
# """
|
||||
# Modify a product, see one pro rata entry
|
||||
# """
|
||||
|
||||
# # Create product
|
||||
# starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
# starting_price = 30.5
|
||||
# product = SampleRecurringProduct.objects.create(owner=self.user,
|
||||
# rc_price=starting_price)
|
||||
# product.create_order(starting_date)
|
||||
|
||||
# recurring_period = product.default_recurring_period.value
|
||||
|
||||
# # First change
|
||||
# change1_date = timezone.make_aware(datetime.datetime(2019,4,17))
|
||||
# product.rc_price = 49.5
|
||||
# product.save()
|
||||
# product.create_or_update_recurring_order(when_to_start=change1_date)
|
||||
|
||||
# # Second change
|
||||
# change2_date = timezone.make_aware(datetime.datetime(2019,5,8))
|
||||
# product.rc_price = 56.5
|
||||
# product.save()
|
||||
# product.create_or_update_recurring_order(when_to_start=change2_date)
|
||||
|
||||
# # Create bill one month after 2nd change
|
||||
# bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30))
|
||||
# bills = Bill.create_next_bills_for_user(self.user,
|
||||
# ending_date=bill_ending_date)
|
||||
|
||||
# # only one bill in this test case
|
||||
# bill = bills[0]
|
||||
|
||||
# expected_amount = starting_price
|
||||
|
||||
# d2 = starting_date + recurring_period
|
||||
# duration2 = change1_date - d2
|
||||
|
||||
# expected_amount = 0
|
||||
|
||||
# # Expected bill sum & records:
|
||||
# # 2019-03-03 - 2019-04-02 +30d: 30.5
|
||||
# # 2019-04-02 - 2019-04-17: +15d: 15.25
|
||||
# # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5
|
||||
# # 2019-05-08 - 2019-06-07: +30d: 56.5
|
||||
# # 2019-06-07 - 2019-07-07: +30d: 56.5
|
||||
|
||||
|
||||
# self.assertEqual(bills[0].sum, price)
|
||||
|
||||
# # expeted result:
|
||||
# # 1x 5 chf bill record
|
||||
# # 1x 5 chf bill record
|
||||
# # 1x 10 partial bill record
|
||||
|
||||
|
||||
|
||||
# class NotABillingTC(TestCase):
|
||||
# #class BillingTestCase(TestCase):
|
||||
# def setUp(self):
|
||||
# self.user = get_user_model().objects.create(
|
||||
# username='jdoe',
|
||||
# email='john.doe@domain.tld')
|
||||
# self.billing_address = BillingAddress.objects.create(
|
||||
# owner=self.user,
|
||||
# street="unknown",
|
||||
# city="unknown",
|
||||
# postal_code="unknown")
|
||||
|
||||
# def test_basic_monthly_billing(self):
|
||||
# one_time_price = 10
|
||||
# recurring_price = 20
|
||||
# description = "Test Product 1"
|
||||
|
||||
# # Three months: full, full, partial.
|
||||
# # starting_date = datetime.fromisoformat('2020-03-01')
|
||||
# starting_date = datetime(2020,3,1)
|
||||
# ending_date = datetime(2020,5,8)
|
||||
|
||||
# # Create order to be billed.
|
||||
# order = Order.objects.create(
|
||||
# owner=self.user,
|
||||
# starting_date=starting_date,
|
||||
# ending_date=ending_date,
|
||||
# recurring_period=RecurringPeriod.PER_30D,
|
||||
# recurring_price=recurring_price,
|
||||
# one_time_price=one_time_price,
|
||||
# description=description,
|
||||
# billing_address=self.billing_address)
|
||||
|
||||
# # Generate & check bill for first month: full recurring_price + setup.
|
||||
# first_month_bills = order.generate_initial_bill()
|
||||
# self.assertEqual(len(first_month_bills), 1)
|
||||
# self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price)
|
||||
|
||||
# # Generate & check bill for second month: full recurring_price.
|
||||
# second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||
# self.assertEqual(len(second_month_bills), 1)
|
||||
# self.assertEqual(second_month_bills[0].amount, recurring_price)
|
||||
|
||||
# # Generate & check bill for third and last month: partial recurring_price.
|
||||
# third_month_bills = Bill.generate_for(2020, 5, self.user)
|
||||
# self.assertEqual(len(third_month_bills), 1)
|
||||
# # 31 days in May.
|
||||
# self.assertEqual(float(third_month_bills[0].amount),
|
||||
# round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS))
|
||||
|
||||
# # Check that running Bill.generate_for() twice does not create duplicates.
|
||||
# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0)
|
||||
|
||||
# def test_basic_yearly_billing(self):
|
||||
# one_time_price = 10
|
||||
# recurring_price = 150
|
||||
# description = "Test Product 1"
|
||||
|
||||
# starting_date = datetime.fromisoformat('2020-03-31T08:05:23')
|
||||
|
||||
# # Create order to be billed.
|
||||
# order = Order.objects.create(
|
||||
# owner=self.user,
|
||||
# starting_date=starting_date,
|
||||
# recurring_period=RecurringPeriod.PER_365D,
|
||||
# recurring_price=recurring_price,
|
||||
# one_time_price=one_time_price,
|
||||
# description=description,
|
||||
# billing_address=self.billing_address)
|
||||
|
||||
# # Generate & check bill for first year: recurring_price + setup.
|
||||
# first_year_bills = order.generate_initial_bill()
|
||||
# self.assertEqual(len(first_year_bills), 1)
|
||||
# self.assertEqual(first_year_bills[0].starting_date.date(),
|
||||
# date.fromisoformat('2020-03-31'))
|
||||
# self.assertEqual(first_year_bills[0].ending_date.date(),
|
||||
# date.fromisoformat('2021-03-30'))
|
||||
# self.assertEqual(first_year_bills[0].amount,
|
||||
# recurring_price + one_time_price)
|
||||
|
||||
# # Generate & check bill for second year: recurring_price.
|
||||
# second_year_bills = Bill.generate_for(2021, 3, self.user)
|
||||
# self.assertEqual(len(second_year_bills), 1)
|
||||
# self.assertEqual(second_year_bills[0].starting_date.date(),
|
||||
# date.fromisoformat('2021-03-31'))
|
||||
# self.assertEqual(second_year_bills[0].ending_date.date(),
|
||||
# date.fromisoformat('2022-03-30'))
|
||||
# self.assertEqual(second_year_bills[0].amount, recurring_price)
|
||||
|
||||
# # Check that running Bill.generate_for() twice does not create duplicates.
|
||||
# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0)
|
||||
# self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0)
|
||||
# self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0)
|
||||
# self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0)
|
||||
|
||||
# def test_basic_hourly_billing(self):
|
||||
# one_time_price = 10
|
||||
# recurring_price = 1.4
|
||||
# description = "Test Product 1"
|
||||
|
||||
# starting_date = datetime.fromisoformat('2020-03-31T08:05:23')
|
||||
# ending_date = datetime.fromisoformat('2020-04-01T11:13:32')
|
||||
|
||||
# # Create order to be billed.
|
||||
# order = Order.objects.create(
|
||||
# owner=self.user,
|
||||
# starting_date=starting_date,
|
||||
# ending_date=ending_date,
|
||||
# recurring_period=RecurringPeriod.PER_HOUR,
|
||||
# recurring_price=recurring_price,
|
||||
# one_time_price=one_time_price,
|
||||
# description=description,
|
||||
# billing_address=self.billing_address)
|
||||
|
||||
# # Generate & check bill for first month: recurring_price + setup.
|
||||
# first_month_bills = order.generate_initial_bill()
|
||||
# self.assertEqual(len(first_month_bills), 1)
|
||||
# self.assertEqual(float(first_month_bills[0].amount),
|
||||
# round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price)
|
||||
|
||||
# # Generate & check bill for first month: recurring_price.
|
||||
# second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||
# self.assertEqual(len(second_month_bills), 1)
|
||||
# self.assertEqual(float(second_month_bills[0].amount),
|
||||
# round(12 * recurring_price, AMOUNT_DECIMALS))
|
1000
uncloud_pay/tests.py
1000
uncloud_pay/tests.py
File diff suppressed because it is too large
Load Diff
|
@ -201,6 +201,7 @@ class BillViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
Allow to download
|
||||
"""
|
||||
bill = self.get_object()
|
||||
provider = UncloudProvider.get_provider()
|
||||
output_file = NamedTemporaryFile()
|
||||
bill_html = render_to_string("bill.html.j2", {'bill': bill})
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_service', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='genericserviceproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='matrixserviceproduct',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_service', '0005_auto_20200928_1844'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='genericserviceproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='genericserviceproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='genericserviceproduct',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='matrixserviceproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='matrixserviceproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='matrixserviceproduct',
|
||||
name='status',
|
||||
),
|
||||
]
|
|
@ -3,7 +3,7 @@ from uncloud_pay.models import Product, RecurringPeriod, AMOUNT_MAX_DIGITS, AMOU
|
|||
from uncloud_vm.models import VMProduct, VMDiskImageProduct
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
class MatrixServiceProduct(Product):
|
||||
class MatrixServiceProduct(models.Model):
|
||||
monthly_managment_fee = 20
|
||||
|
||||
description = "Managed Matrix HomeServer"
|
||||
|
@ -15,8 +15,8 @@ class MatrixServiceProduct(Product):
|
|||
domain = models.CharField(max_length=255, default='domain.tld')
|
||||
|
||||
# Default recurring price is PER_MONT, see Product class.
|
||||
def recurring_price(self, recurring_period=RecurringPeriod.PER_30D):
|
||||
return self.monthly_managment_fee
|
||||
# def recurring_price(self, recurring_period=RecurringPeriod.PER_30D):
|
||||
# return self.monthly_managment_fee
|
||||
|
||||
@staticmethod
|
||||
def base_image():
|
||||
|
@ -24,17 +24,17 @@ class MatrixServiceProduct(Product):
|
|||
#e return VMDiskImageProduct.objects.get(uuid="93e564c5-adb3-4741-941f-718f76075f02")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def allowed_recurring_periods():
|
||||
return list(filter(
|
||||
lambda pair: pair[0] in [RecurringPeriod.PER_30D],
|
||||
RecurringPeriod.choices))
|
||||
# @staticmethod
|
||||
# def allowed_recurring_periods():
|
||||
# return list(filter(
|
||||
# lambda pair: pair[0] in [RecurringPeriod.PER_30D],
|
||||
# RecurringPeriod.choices))
|
||||
|
||||
@property
|
||||
def one_time_price(self):
|
||||
return 30
|
||||
|
||||
class GenericServiceProduct(Product):
|
||||
class GenericServiceProduct(models.Model):
|
||||
custom_description = models.TextField()
|
||||
custom_recurring_price = models.DecimalField(default=0.0,
|
||||
max_digits=AMOUNT_MAX_DIGITS,
|
||||
|
|
|
@ -17,8 +17,8 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ['order', 'owner', 'status']
|
||||
|
||||
class OrderMatrixServiceProductSerializer(MatrixServiceProductSerializer):
|
||||
recurring_period = serializers.ChoiceField(
|
||||
choices=MatrixServiceProduct.allowed_recurring_periods())
|
||||
# recurring_period = serializers.ChoiceField(
|
||||
# choices=MatrixServiceProduct.allowed_recurring_periods())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OrderMatrixServiceProductSerializer, self).__init__(*args, **kwargs)
|
||||
|
@ -42,8 +42,8 @@ class GenericServiceProductSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = [ 'owner', 'status']
|
||||
|
||||
class OrderGenericServiceProductSerializer(GenericServiceProductSerializer):
|
||||
recurring_period = serializers.ChoiceField(
|
||||
choices=GenericServiceProduct.allowed_recurring_periods())
|
||||
# recurring_period = serializers.ChoiceField(
|
||||
# choices=GenericServiceProduct.allowed_recurring_periods())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OrderGenericServiceProductSerializer, self).__init__(*args, **kwargs)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_vm', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vmdiskproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmsnapshotproduct',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,49 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_vm', '0005_auto_20200928_1844'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vmdiskproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmdiskproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmdiskproduct',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmproduct',
|
||||
name='status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmsnapshotproduct',
|
||||
name='extra_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmsnapshotproduct',
|
||||
name='owner',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmsnapshotproduct',
|
||||
name='status',
|
||||
),
|
||||
]
|
|
@ -49,7 +49,7 @@ class VMHost(UncloudModel):
|
|||
|
||||
|
||||
|
||||
class VMProduct(Product):
|
||||
class VMProduct(models.Model):
|
||||
vmhost = models.ForeignKey(
|
||||
VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True
|
||||
)
|
||||
|
@ -71,12 +71,12 @@ class VMProduct(Product):
|
|||
return "Virtual machine '{}': {} core(s), {}GB memory".format(
|
||||
self.name, self.cores, self.ram_in_gb)
|
||||
|
||||
@staticmethod
|
||||
def allowed_recurring_periods():
|
||||
return list(filter(
|
||||
lambda pair: pair[0] in [RecurringPeriod.PER_365D,
|
||||
RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR],
|
||||
RecurringPeriod.choices))
|
||||
# @staticmethod
|
||||
# def allowed_recurring_periods():
|
||||
# return list(filter(
|
||||
# lambda pair: pair[0] in [RecurringPeriod.PER_365D,
|
||||
# RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR],
|
||||
# RecurringPeriod.choices))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
@ -133,7 +133,7 @@ class VMDiskType(models.TextChoices):
|
|||
LOCAL_HDD = 'local/hdd'
|
||||
|
||||
|
||||
class VMDiskProduct(Product):
|
||||
class VMDiskProduct(models.Model):
|
||||
"""
|
||||
The VMDiskProduct is attached to a VM.
|
||||
|
||||
|
@ -180,7 +180,7 @@ class VMNetworkCard(models.Model):
|
|||
null=True)
|
||||
|
||||
|
||||
class VMSnapshotProduct(Product):
|
||||
class VMSnapshotProduct(models.Model):
|
||||
gb_ssd = models.FloatField(editable=False)
|
||||
gb_hdd = models.FloatField(editable=False)
|
||||
|
||||
|
|
|
@ -101,8 +101,8 @@ class VMProductSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = ['order', 'owner', 'status']
|
||||
|
||||
class OrderVMProductSerializer(VMProductSerializer):
|
||||
recurring_period = serializers.ChoiceField(
|
||||
choices=VMWithOSProduct.allowed_recurring_periods())
|
||||
# recurring_period = serializers.ChoiceField(
|
||||
# choices=VMWithOSProduct.allowed_recurring_periods())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VMProductSerializer, self).__init__(*args, **kwargs)
|
||||
|
@ -133,8 +133,8 @@ class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer):
|
|||
"""
|
||||
|
||||
# Custom field used at creation (= ordering) only.
|
||||
recurring_period = serializers.ChoiceField(
|
||||
choices=VMProduct.allowed_recurring_periods())
|
||||
# recurring_period = serializers.ChoiceField(
|
||||
# choices=VMProduct.allowed_recurring_periods())
|
||||
|
||||
os_disk_uuid = serializers.UUIDField()
|
||||
# os_disk_size =
|
||||
|
|
Loading…
Reference in New Issue