update VAT importer
This commit is contained in:
parent
50fd9e1f37
commit
e03cdf214a
6 changed files with 141 additions and 79 deletions
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
)
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
Loading…
Reference in a new issue