From e03cdf214acc318fb1ae29155040fa0357f83bfb Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Thu, 8 Oct 2020 19:54:04 +0200 Subject: [PATCH] update VAT importer --- doc/uncloud-manual-2020-08-01.org | 16 +++ .../management/commands/db-add-defaults.py | 31 +++++- uncloud/settings.py | 4 +- uncloud_pay/admin.py | 11 +- .../management/commands/import-vat-rates.py | 57 +++++----- uncloud_pay/models.py | 101 ++++++++++++------ 6 files changed, 141 insertions(+), 79 deletions(-) diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org index d316d18..658aa0d 100644 --- a/doc/uncloud-manual-2020-08-01.org +++ b/doc/uncloud-manual-2020-08-01.org @@ -60,6 +60,22 @@ 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. diff --git a/uncloud/management/commands/db-add-defaults.py b/uncloud/management/commands/db-add-defaults.py index b513d2f..49cc991 100644 --- a/uncloud/management/commands/db-add-defaults.py +++ b/uncloud/management/commands/db-add-defaults.py @@ -1,6 +1,13 @@ -from django.core.management.base import BaseCommand +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_pay.models import RecurringPeriod, Product class Command(BaseCommand): help = 'Add standard uncloud values' @@ -9,6 +16,24 @@ class Command(BaseCommand): pass def handle(self, *args, **options): - # Order matters, objects are somewhat dependent on each other + # 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() diff --git a/uncloud/settings.py b/uncloud/settings.py index df3ba17..17f5200 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -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: diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index aa648d6..2123397 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -11,7 +11,7 @@ from django.http import FileResponse from django.template.loader import render_to_string -from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, Product, RecurringPeriod, ProductToRecurringPeriod +from uncloud_pay.models import * class BillRecordInline(admin.TabularInline): @@ -88,10 +88,5 @@ admin.site.register(Bill, BillAdmin) admin.site.register(ProductToRecurringPeriod) admin.site.register(Product, ProductAdmin) -#admin.site.register(Order, OrderAdmin) -#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: - -admin.site.register(Order) -admin.site.register(BillRecord) -admin.site.register(BillingAddress) -admin.site.register(RecurringPeriod) +for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate ]: + admin.site.register(m) diff --git a/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_pay/management/commands/import-vat-rates.py index 2eaf80b..89381ba 100644 --- a/uncloud_pay/management/commands/import-vat-rates.py +++ b/uncloud_pay/management/commands/import-vat-rates.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/kdeldycke/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( - starting_date=row["start_date"], - ending_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"] + ) diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 36057d3..dbe0aca 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -1,28 +1,26 @@ +import logging +import itertools +import datetime +from math import ceil +from calendar import monthrange +from decimal import Decimal +from functools import reduce + from django.db import models from django.db.models import Q from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator from django.utils import timezone from django.core.exceptions import ObjectDoesNotExist, ValidationError - -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType - -import logging -from functools import reduce -import itertools -from math import ceil -import datetime -from calendar import monthrange -from decimal import Decimal +from django.conf import settings import uncloud_pay.stripe from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES from uncloud.models import UncloudModel, UncloudStatus -from decimal import Decimal -import decimal # Used to generate bill due dates. BILL_PAYMENT_DELAY=datetime.timedelta(days=10) @@ -102,22 +100,6 @@ class StripeCustomer(models.Model): on_delete=models.CASCADE) stripe_id = models.CharField(max_length=32) -### -# Hosting company configuration - -class HostingProvider(models.Model): - """ - 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 - - WIP. - """ - starting_date = models.DateField() - ending_date = models.DateField() - - ### # Payments and Payment Methods. @@ -267,7 +249,6 @@ class RecurringPeriod(models.Model): obj, created = cls.objects.get_or_create(name=name, defaults={ 'duration_seconds': seconds }) - @staticmethod def secs_to_name(secs): name = "" @@ -290,14 +271,11 @@ class RecurringPeriod(models.Model): return f"{self.name} ({duration})" - - ### # Bills. class BillingAddress(models.Model): owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - organization = models.CharField(max_length=100, blank=True, null=True) name = models.CharField(max_length=100) street = models.CharField(max_length=100) @@ -314,6 +292,32 @@ class BillingAddress(models.Model): name='one_active_billing_address_per_user') ] + @classmethod + def populate_db_defaults(cls): + """ + Ensure we have at least one billing address that is associated with the uncloud-admin. + + This way we are sure that an UncloudProvider can be created. + + Cannot use get_or_create as that looks for exactly one. + + """ + + owner = get_user_model().objects.get(username=settings.UNCLOUD_ADMIN_NAME) + billing_address = cls.objects.filter(owner=owner).first() + + if not billing_address: + billing_address = cls.objects.create(owner=owner, + organization="uncloud admins", + name="Uncloud Admin", + street="Uncloudstreet. 42", + city="Luchsingen", + postal_code="8775", + country="CH", + active=True) + + + @staticmethod def get_address_for(user): return BillingAddress.objects.get(owner=user, active=True) @@ -349,6 +353,10 @@ class VATRate(models.Model): logger.debug("Did not find VAT rate for %s, returning 0" % country_code) return 0 + + def __str__(self): + return f"{self.territory_codes}: {self.starting_date} - {self.ending_date}: {self.rate_type}" + ### # Products @@ -1205,3 +1213,30 @@ class ProductToRecurringPeriod(models.Model): def __str__(self): return f"{self.product} - {self.recurring_period} (default: {self.is_default})" + + +### +# Who is running / providing this instance of uncloud? + +class UncloudProvider(models.Model): + """ + 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. + """ + + valid_from = models.DateField() + valid_to = models.DateField(blank=True) + + billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + + @classmethod + def populate_db_defaults(cls): + ba = BillingAddress.objects.get_or_create() + # obj, created = cls.objects.get_or_create( + # valid_from=timezone.now() + # defaults={ 'duration_seconds': seconds })