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
 | 
			
		||||
    #+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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        vat_url = options['vat_url']
 | 
			
		||||
        url_open = urllib.request.urlopen(vat_url)
 | 
			
		||||
 | 
			
		||||
        # 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"] is not "" else None,
 | 
			
		||||
                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"]
 | 
			
		||||
            )
 | 
			
		||||
                        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:
 | 
			
		||||
            print(" *** Error occurred. Details {}".format(str(e)))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue