update VAT importer

This commit is contained in:
Nico Schottelius 2020-10-08 19:54:04 +02:00
parent 50fd9e1f37
commit e03cdf214a
6 changed files with 141 additions and 79 deletions

View file

@ -60,6 +60,22 @@ python manage.py migrate
python manage.py bootstrap-user --username nicocustomer python manage.py bootstrap-user --username nicocustomer
#+END_SRC #+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 * Testing / CLI Access
Access via the commandline (CLI) can be done using curl or Access via the commandline (CLI) can be done using curl or
httpie. In our examples we will use httpie. httpie. In our examples we will use httpie.

View file

@ -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): class Command(BaseCommand):
help = 'Add standard uncloud values' help = 'Add standard uncloud values'
@ -9,6 +16,24 @@ class Command(BaseCommand):
pass pass
def handle(self, *args, **options): 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() RecurringPeriod.populate_db_defaults()
Product.populate_db_defaults() Product.populate_db_defaults()

View file

@ -19,8 +19,6 @@ from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
LOGGING = {} LOGGING = {}
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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 # required for hardcopy / pdf rendering: https://github.com/loftylabs/django-hardcopy
CHROME_PATH = '/usr/bin/chromium-browser' 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 # Overwrite settings with local settings, if existing
try: try:

View file

@ -11,7 +11,7 @@ from django.http import FileResponse
from django.template.loader import render_to_string 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): class BillRecordInline(admin.TabularInline):
@ -88,10 +88,5 @@ admin.site.register(Bill, BillAdmin)
admin.site.register(ProductToRecurringPeriod) admin.site.register(ProductToRecurringPeriod)
admin.site.register(Product, ProductAdmin) admin.site.register(Product, ProductAdmin)
#admin.site.register(Order, OrderAdmin) for m in [ Order, BillRecord, BillingAddress, RecurringPeriod, VATRate ]:
#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: admin.site.register(m)
admin.site.register(Order)
admin.site.register(BillRecord)
admin.site.register(BillingAddress)
admin.site.register(RecurringPeriod)

View file

@ -1,44 +1,35 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from uncloud_pay.models import VATRate from uncloud_pay.models import VATRate
import csv
import urllib
import csv
import sys
import io
class Command(BaseCommand): class Command(BaseCommand):
help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv''' 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): 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): def handle(self, *args, **options):
try: vat_url = options['vat_url']
for c_file in options['csv_file']: url_open = urllib.request.urlopen(vat_url)
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
except Exception as e: # map to fileio using stringIO
print(" *** Error occurred. Details {}".format(str(e))) 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"]
)

View file

@ -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 import models
from django.db.models import Q from django.db.models import Q
from django.contrib.auth import get_user_model 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.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.conf import settings
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
import uncloud_pay.stripe import uncloud_pay.stripe
from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES
from uncloud.models import UncloudModel, UncloudStatus from uncloud.models import UncloudModel, UncloudStatus
from decimal import Decimal
import decimal
# Used to generate bill due dates. # Used to generate bill due dates.
BILL_PAYMENT_DELAY=datetime.timedelta(days=10) BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
@ -102,22 +100,6 @@ class StripeCustomer(models.Model):
on_delete=models.CASCADE) on_delete=models.CASCADE)
stripe_id = models.CharField(max_length=32) 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. # Payments and Payment Methods.
@ -267,7 +249,6 @@ class RecurringPeriod(models.Model):
obj, created = cls.objects.get_or_create(name=name, obj, created = cls.objects.get_or_create(name=name,
defaults={ 'duration_seconds': seconds }) defaults={ 'duration_seconds': seconds })
@staticmethod @staticmethod
def secs_to_name(secs): def secs_to_name(secs):
name = "" name = ""
@ -290,14 +271,11 @@ class RecurringPeriod(models.Model):
return f"{self.name} ({duration})" return f"{self.name} ({duration})"
### ###
# Bills. # Bills.
class BillingAddress(models.Model): class BillingAddress(models.Model):
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
organization = models.CharField(max_length=100, blank=True, null=True) organization = models.CharField(max_length=100, blank=True, null=True)
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
street = 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') 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 @staticmethod
def get_address_for(user): def get_address_for(user):
return BillingAddress.objects.get(owner=user, active=True) 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) logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
return 0 return 0
def __str__(self):
return f"{self.territory_codes}: {self.starting_date} - {self.ending_date}: {self.rate_type}"
### ###
# Products # Products
@ -1205,3 +1213,30 @@ class ProductToRecurringPeriod(models.Model):
def __str__(self): def __str__(self):
return f"{self.product} - {self.recurring_period} (default: {self.is_default})" 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 })