Compare commits

...

27 Commits
0.7 ... master

Author SHA1 Message Date
Nico Schottelius ecc9e6f734 [reverseDNS] add basic logic 2020-10-25 22:43:34 +01:00
Nico Schottelius 20c7c86703 restructure to move uncloudnetwork into core 2020-10-25 21:00:30 +01:00
Nico Schottelius 8959bc6ad5 various updates 2020-10-25 13:52:36 +01:00
Nico Schottelius 0cd8a3a787 ++update ungleich_provider 2020-10-11 22:36:01 +02:00
Nico Schottelius bbc7625550 phase in configuration - move address to base 2020-10-11 22:32:08 +02:00
Nico Schottelius fe4e200dc0 Begin phasing in the uncloudprovider 2020-10-11 17:45:25 +02:00
Nico Schottelius e03cdf214a update VAT importer 2020-10-08 19:54:04 +02:00
Nico Schottelius 50fd9e1f37 ++work 2020-10-07 00:54:56 +02:00
Nico Schottelius 2e74661702 Fix first test case / billing 2020-10-06 23:14:32 +02:00
Nico Schottelius c26ff253de One step furter to allow saving of orders w/o explicit recurringperiod 2020-10-06 19:21:37 +02:00
Nico Schottelius 9623a77907 Updating for products/recurring periods 2020-10-06 18:53:13 +02:00
Nico Schottelius c435639241 gitignore some tests 2020-10-06 16:13:03 +02:00
Nico Schottelius 992c7c551e Make recurring period a database model
- For easier handling (foreignkeys, many2many)
- For higher flexibility (users can define their own periods)
2020-10-06 15:46:22 +02:00
Nico Schottelius 58883765d7 [tests] back to 5 working tests! 2020-09-28 23:16:17 +02:00
Nico Schottelius 8d8c4d660c Can order a generic product now 2020-09-28 21:59:35 +02:00
Nico Schottelius c32499199a Add JSON support for product description 2020-09-28 21:34:24 +02:00
Nico Schottelius c6bacab35a Phasing out Product model
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-09-28 20:59:08 +02:00
Nico Schottelius 1aead50170 remove big mistake: orders from product
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-09-28 20:44:50 +02:00
Nico Schottelius d8a7964fed Continue to refactor for shifting logic into the order 2020-09-09 00:35:55 +02:00
Nico Schottelius 077c665c53 ++update 2020-09-03 17:16:18 +02:00
Nico Schottelius f7274fe967 Adding logic to order to find out whether its closed 2020-09-03 16:38:51 +02:00
Nico Schottelius 1c7d81762d begin splitting bill record creation function 2020-09-02 16:02:28 +02:00
Nico Schottelius 18f9a3848a Implement ending/replacing date logic 2020-08-27 22:00:54 +02:00
Nico Schottelius 9211894b23 implement basic logic for updating a recurring order
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-27 14:45:37 +02:00
Nico Schottelius b8b15704a3 begin testing bill sums
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-25 21:53:25 +02:00
Nico Schottelius ab412cb877 Test that creating products w/o correct billing address fails 2020-08-25 21:31:12 +02:00
Nico Schottelius 7b83efe995 [pay] make sample products more modular 2020-08-25 21:11:28 +02:00
69 changed files with 4078 additions and 1076 deletions

View File

@ -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
-

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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()

View File

@ -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

View File

@ -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')),
)

6
uncloud/admin.py Normal file
View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import UncloudProvider, UncloudNetwork
for m in [ UncloudProvider, UncloudNetwork ]:
admin.site.register(m)

View File

@ -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()

View File

@ -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()),
],
),
]

View File

@ -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),
),
]

View File

@ -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

View File

@ -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,
),
]

View File

@ -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

View File

View File

@ -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}"

View File

@ -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:

View File

@ -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')

View File

@ -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)

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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)])),
],
),
]

View File

@ -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),
),
]

View File

@ -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,
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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}"

View File

@ -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')

View File

@ -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')),
)

View File

@ -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)

View File

@ -0,0 +1,2 @@
# Customer tests
customer-*.py

View File

@ -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

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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,
},
),
]

View File

@ -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,
),
]

View File

@ -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,
),
]

View File

@ -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',
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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)]),
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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')),
],
),
]

View File

@ -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',
),
]

View File

@ -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

View File

@ -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)

View File

@ -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 %}

View File

@ -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))

File diff suppressed because it is too large Load Diff

View File

@ -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})

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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,

View File

@ -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)

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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)

View File

@ -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 =