From ad187c02dae6df1e0ea8a714a9a72621b25dea75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 15 Apr 2020 12:16:55 +0200 Subject: [PATCH 01/13] Import VAT rates "importer" from dynamicweb --- .../management/commands/import-vat-rates.py | 44 +++++++++++++++++++ .../migrations/0005_auto_20200415_1003.py | 31 +++++++++++++ .../uncloud/uncloud_pay/models.py | 9 ++++ 3 files changed, 84 insertions(+) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py new file mode 100644 index 0000000..32938e4 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand +from uncloud_pay.models import VATRate +import csv + + +class Command(BaseCommand): + help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv''' + + def add_arguments(self, parser): + parser.add_argument('csv_file', nargs='+', type=str) + + 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 + + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py new file mode 100644 index 0000000..c30f527 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.5 on 2020-04-15 10:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0004_auto_20200409_1225'), + ] + + operations = [ + migrations.CreateModel( + name='VATRate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField(blank=True, null=True)), + ('stop_date', models.DateField(blank=True, null=True)), + ('territory_codes', models.TextField(blank=True, default='')), + ('currency_code', models.CharField(max_length=10)), + ('rate', models.FloatField()), + ('rate_type', models.TextField(blank=True, default='')), + ('description', models.TextField(blank=True, default='')), + ], + ), + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 4cb1952..d6d0f2a 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -453,6 +453,15 @@ class BillRecord(): def amount(self): return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price +class VATRate(models.Model): + start_date = models.DateField(blank=True, null=True) + stop_date = models.DateField(blank=True, null=True) + territory_codes = models.TextField(blank=True, default='') + currency_code = models.CharField(max_length=10) + rate = models.FloatField() + rate_type = models.TextField(blank=True, default='') + description = models.TextField(blank=True, default='') + ### # Orders. From c6ca94800e0b7c92672a01647c8924b917f3d095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 15 Apr 2020 15:17:38 +0200 Subject: [PATCH 02/13] Add BillingAddress structure to users --- uncloud_django_based/uncloud/uncloud/urls.py | 3 +- .../uncloud/uncloud_pay/models.py | 263 ++++++++++++++++++ .../uncloud/uncloud_pay/serializers.py | 12 + .../uncloud/uncloud_pay/views.py | 22 +- 4 files changed, 298 insertions(+), 2 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py index 343e06b..14a87e8 100644 --- a/uncloud_django_based/uncloud/uncloud/urls.py +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -53,10 +53,11 @@ router.register(r'net/vpnreservation', netviews.VPNNetworkReservationViewSet, ba # Pay -router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-method') +router.register(r'address', payviews.BillingAddressViewSet, basename='address') router.register(r'bill', payviews.BillViewSet, basename='bill') router.register(r'order', payviews.OrderViewSet, basename='order') router.register(r'payment', payviews.PaymentViewSet, basename='payment') +router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-method') # admin/staff urls diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index d6d0f2a..e809f09 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -33,6 +33,248 @@ decimal.getcontext().prec = AMOUNT_DECIMALS + 1 # Used to generate bill due dates. BILL_PAYMENT_DELAY=timedelta(days=10) +# 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')), +) + # Initialize logger. logger = logging.getLogger(__name__) @@ -47,7 +289,16 @@ class RecurringPeriod(models.TextChoices): PER_MINUTE = 'MINUTE', _('Per Minute') PER_SECOND = 'SECOND', _('Per Second') +class CountryField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs.setdefault('choices', COUNTRIES) + kwargs.setdefault('default', 'CH') + kwargs.setdefault('max_length', 2) + super(CountryField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "CharField" def get_balance_for_user(user): bills = reduce( @@ -453,6 +704,18 @@ class BillRecord(): def amount(self): return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price +class BillingAddress(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE) + + street = models.CharField(max_length=100) + city = models.CharField(max_length=50) + postal_code = models.CharField(max_length=50) + country = CountryField(blank=True) + vat_number = models.CharField(max_length=100, default="", blank=True) + +# Populated with the import-vat-numbers django command. class VATRate(models.Model): start_date = models.DateField(blank=True, null=True) stop_date = models.DateField(blank=True, null=True) diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index f408d1b..664e19b 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -69,3 +69,15 @@ class BillSerializer(serializers.ModelSerializer): model = Bill fields = ['reference', 'owner', 'total', 'due_date', 'creation_date', 'starting_date', 'ending_date', 'records', 'final'] + +class BillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + fields = ['uuid', 'street', 'city', 'postal_code', 'country', 'vat_number'] + +# We do not want users to mutate the country / VAT number of an address, as it +# will change VAT on existing bills. +class UpdateBillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + fields = ['uuid', 'street', 'city', 'postal_code'] diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index b64981f..36a291a 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -1,7 +1,7 @@ from django.shortcuts import render from django.db import transaction from django.contrib.auth import get_user_model -from rest_framework import viewsets, permissions, status, views +from rest_framework import viewsets, mixins, permissions, status, views from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response from rest_framework.decorators import action @@ -187,6 +187,26 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return Order.objects.filter(owner=self.request.user) +class BillingAddressViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_serializer_class(self): + if self.action == 'update': + return UpdateBillingAddressSerializer + else: + return BillingAddressSerializer + + def get_queryset(self): + return self.request.user.billingaddress_set.all() + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(owner=request.user) ### # Old admin stuff. From 3fa1d5753ef030d6e89565bb4a05472863a49010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Wed, 15 Apr 2020 16:01:31 +0200 Subject: [PATCH 03/13] Minimal VAT validation on billing address registration --- uncloud_django_based/uncloud/requirements.txt | 4 ++++ .../uncloud/uncloud_pay/serializers.py | 7 +----- .../uncloud/uncloud_pay/views.py | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/uncloud_django_based/uncloud/requirements.txt b/uncloud_django_based/uncloud/requirements.txt index 90c9882..a7fc9f2 100644 --- a/uncloud_django_based/uncloud/requirements.txt +++ b/uncloud_django_based/uncloud/requirements.txt @@ -18,3 +18,7 @@ django-hardcopy # schema support pyyaml uritemplate + +# Comprehensive interface to validate VAT numbers, making use of the VIES +# service for European countries. +vat-validator diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 664e19b..5579b14 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -56,15 +56,10 @@ class BillRecordSerializer(serializers.Serializer): order = serializers.HyperlinkedRelatedField( view_name='order-detail', read_only=True) - description = serializers.CharField() - recurring_period = serializers.CharField() - recurring_price = serializers.DecimalField(max_digits=10, decimal_places=2) - recurring_count = serializers.DecimalField(max_digits=10, decimal_places=2) - one_time_price = serializers.DecimalField(max_digits=10, decimal_places=2) - amount = serializers.DecimalField(max_digits=10, decimal_places=2) class BillSerializer(serializers.ModelSerializer): records = BillRecordSerializer(many=True, read_only=True) + class Meta: model = Bill fields = ['reference', 'owner', 'total', 'due_date', 'creation_date', diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 36a291a..5bd1ae6 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -7,12 +7,15 @@ from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.reverse import reverse from rest_framework.decorators import renderer_classes +from vat_validator import validate_vat, vies +from vat_validator.countries import EU_COUNTRY_CODES import json from .models import * from .serializers import * from datetime import datetime +from vat_validator import sanitize_vat import uncloud_pay.stripe as uncloud_stripe ### @@ -206,7 +209,28 @@ class BillingAddressViewSet(mixins.CreateModelMixin, def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + + # Validate VAT numbers. + country = serializer.validated_data["country"] + vat_number = serializer.validated_data["vat_number"] + + # We ignore empty VAT numbers. + if vat_number != "": + if not validate_vat(country, vat_number): + return Response( + {'error': 'Malformed VAT number.'}, + status=status.HTTP_400_BAD_REQUEST) + elif country in EU_COUNTRY_CODES: + # FIXME: make a synchroneous call to a third patry API here is + # not a good idea... + vies_state = vies.check_vat(country, vat_number) + if not vies_state.valid: + return Response( + {'error': 'European VAT number does not exist in VIES.'}, + status=status.HTTP_400_BAD_REQUEST) + serializer.save(owner=request.user) + return Response(serializer.data) ### # Old admin stuff. From 0522927c50accc6a5e51eb835e79a9eff197a7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 08:30:45 +0200 Subject: [PATCH 04/13] Start wiring BillingAddresses to bills & orders --- .../uncloud/uncloud_pay/models.py | 62 +++++++++++++++---- .../uncloud/uncloud_pay/serializers.py | 13 ++-- .../uncloud/uncloud_service/serializers.py | 10 ++- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index e809f09..932a6d1 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -440,6 +440,36 @@ class PaymentMethod(models.Model): ### # Bills. +class BillingAddress(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + + name = models.CharField(max_length=100) + street = models.CharField(max_length=100) + city = models.CharField(max_length=50) + postal_code = models.CharField(max_length=50) + country = CountryField(blank=True) + vat_number = models.CharField(max_length=100, default="", blank=True) + + @staticmethod + def get_addresses_for(user): + return BillingAddress.objects.filter(owner=user) + + @staticmethod + def get_preferred_address_for(user): + addresses = get_addresses_for(user) + if len(addresses) == 0: + return None + else: + # TODO: allow user to set primary/preferred address + return addresses[0] + + def __str__(self): + return "{}, {}, {} {}, {}".format( + self.name, self.street, self.postal_code, self.city, + self.country) + + class Bill(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey(get_user_model(), @@ -484,6 +514,7 @@ class Bill(models.Model): # A bill is final when its ending date is passed. return self.ending_date < timezone.now() +<<<<<<< HEAD def activate_products(self): for order in self.order_set.all(): # FIXME: using __something might not be a good idea. @@ -492,8 +523,12 @@ class Bill(models.Model): if product.status == UncloudStatus.AWAITING_PAYMENT: product.status = UncloudStatus.PENDING product.save() + @property + def billing_address(self): + return self.order.billing_address @staticmethod + def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. generated_bills = [] @@ -704,17 +739,6 @@ class BillRecord(): def amount(self): return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price -class BillingAddress(models.Model): - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - street = models.CharField(max_length=100) - city = models.CharField(max_length=50) - postal_code = models.CharField(max_length=50) - country = CountryField(blank=True) - vat_number = models.CharField(max_length=100, default="", blank=True) - # Populated with the import-vat-numbers django command. class VATRate(models.Model): start_date = models.DateField(blank=True, null=True) @@ -725,6 +749,21 @@ class VATRate(models.Model): rate_type = models.TextField(blank=True, default='') description = models.TextField(blank=True, default='') + @staticmethod + def get_for_country(country_code): + vat_rate = None + try: + vat_rate = VATRates.objects.get( + territory_codes=country, start_date__isnull=False, stop_date=None + ) + logger.debug("VAT rate for %s is %s" % (country, vat_rate.rate)) + return vat_rate.rate + except VATRates.DoesNotExist as dne: + logger.debug(str(dne)) + logger.debug("Did not find VAT rate for %s, returning 0" % country) + return 0 + + ### # Orders. @@ -735,6 +774,7 @@ class Order(models.Model): owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, editable=False) + billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) # TODO: enforce ending_date - starting_date to be larger than recurring_period. creation_date = models.DateTimeField(auto_now_add=True) diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 5579b14..659092c 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -57,18 +57,19 @@ class BillRecordSerializer(serializers.Serializer): view_name='order-detail', read_only=True) +class BillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + fields = ['uuid', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number'] + class BillSerializer(serializers.ModelSerializer): + billing_address = BillingAddressSerializer(read_only=True) records = BillRecordSerializer(many=True, read_only=True) class Meta: model = Bill fields = ['reference', 'owner', 'total', 'due_date', 'creation_date', - 'starting_date', 'ending_date', 'records', 'final'] - -class BillingAddressSerializer(serializers.ModelSerializer): - class Meta: - model = BillingAddress - fields = ['uuid', 'street', 'city', 'postal_code', 'country', 'vat_number'] + 'starting_date', 'ending_date', 'records', 'final', 'billing_address'] # We do not want users to mutate the country / VAT number of an address, as it # will change VAT on existing bills. diff --git a/uncloud_django_based/uncloud/uncloud_service/serializers.py b/uncloud_django_based/uncloud/uncloud_service/serializers.py index 1d50bbf..2be2cee 100644 --- a/uncloud_django_based/uncloud/uncloud_service/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_service/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from .models import * from uncloud_vm.serializers import ManagedVMProductSerializer from uncloud_vm.models import VMProduct -from uncloud_pay.models import RecurringPeriod +from uncloud_pay.models import RecurringPeriod, BillingAddress class MatrixServiceProductSerializer(serializers.ModelSerializer): vm = ManagedVMProductSerializer() @@ -11,9 +11,15 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer): recurring_period = serializers.ChoiceField( choices=MatrixServiceProduct.allowed_recurring_periods()) + def __init__(self, *args, **kwargs): + super(MatrixServiceProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for(self.context['request'].user)) + class Meta: model = MatrixServiceProduct - fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period'] + fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', + 'recurring_period'] read_only_fields = ['uuid', 'order', 'owner', 'status'] class GenericServiceProductSerializer(serializers.ModelSerializer): From e6eba7542bdf9f61d3186b72e2c35951b0526a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 09:20:06 +0200 Subject: [PATCH 05/13] Minor fixes, DB sync after rebase --- ...415_1003.py => 0006_auto_20200415_1003.py} | 2 +- .../migrations/0007_auto_20200418_0722.py | 37 +++++++++++++++++++ .../uncloud/uncloud_pay/models.py | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) rename uncloud_django_based/uncloud/uncloud_pay/migrations/{0005_auto_20200415_1003.py => 0006_auto_20200415_1003.py} (96%) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py similarity index 96% rename from uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py rename to uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py index c30f527..1f37eae 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200415_1003.py +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0004_auto_20200409_1225'), + ('uncloud_pay', '0005_auto_20200413_0924'), ] operations = [ diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py new file mode 100644 index 0000000..6e5a198 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py @@ -0,0 +1,37 @@ +# Generated by Django 3.0.5 on 2020-04-18 07:22 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uncloud_pay.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0006_auto_20200415_1003'), + ] + + operations = [ + migrations.CreateModel( + name='BillingAddress', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('street', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', uncloud_pay.models.CountryField(blank=True, choices=[('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')], default='CH', max_length=2)), + ('vat_number', models.CharField(blank=True, default='', max_length=100)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='order', + name='billing_address', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.BillingAddress'), + preserve_default=False, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 932a6d1..8a71abb 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -514,7 +514,6 @@ class Bill(models.Model): # A bill is final when its ending date is passed. return self.ending_date < timezone.now() -<<<<<<< HEAD def activate_products(self): for order in self.order_set.all(): # FIXME: using __something might not be a good idea. @@ -523,6 +522,7 @@ class Bill(models.Model): if product.status == UncloudStatus.AWAITING_PAYMENT: product.status = UncloudStatus.PENDING product.save() + @property def billing_address(self): return self.order.billing_address From 9bbe3b3b5672f775da978e1e339301010f485e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 09:26:34 +0200 Subject: [PATCH 06/13] Adapt uncloud_pay tests to support billing addresses --- .../uncloud/uncloud_pay/tests.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index 9e8728d..5236c8a 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -10,6 +10,11 @@ class BillingTestCase(TestCase): 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 @@ -25,7 +30,8 @@ class BillingTestCase(TestCase): owner=self.user, starting_date=starting_date, ending_date=ending_date, - recurring_period=RecurringPeriod.PER_MONTH) + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address) order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first month: full recurring_price + setup. @@ -59,7 +65,8 @@ class BillingTestCase(TestCase): order = Order.objects.create( owner=self.user, starting_date=starting_date, - recurring_period=RecurringPeriod.PER_YEAR) + recurring_period=RecurringPeriod.PER_YEAR, + billing_address=self.billing_address) order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first year: recurring_price + setup. @@ -100,7 +107,8 @@ class BillingTestCase(TestCase): owner=self.user, starting_date=starting_date, ending_date=ending_date, - recurring_period=RecurringPeriod.PER_HOUR) + recurring_period=RecurringPeriod.PER_HOUR, + billing_address=self.billing_address) order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first month: recurring_price + setup. @@ -121,13 +129,20 @@ class ProductActivationTestCase(TestCase): 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_product_activation(self): starting_date = datetime.fromisoformat('2020-03-01') order = Order.objects.create( owner=self.user, starting_date=starting_date, - recurring_period=RecurringPeriod.PER_MONTH) + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address) order.save() product = GenericServiceProduct( From c0e12884e1ce4b34262715b4d51a05d00a99b550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 09:38:12 +0200 Subject: [PATCH 07/13] Sync migrations - again! --- .../migrations/0006_billingaddress.py | 29 +++++++++++++ .../migrations/0007_auto_20200418_0722.py | 37 ---------------- .../migrations/0007_auto_20200418_0737.py | 42 +++++++++++++++++++ 3 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py delete mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py new file mode 100644 index 0000000..79b25ab --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.5 on 2020-04-15 12:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uncloud_pay.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0006_auto_20200415_1003'), + ] + + operations = [ + migrations.CreateModel( + name='BillingAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('street', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', uncloud_pay.models.CountryField(choices=[('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')], default='CH', max_length=2)), + ('vat_number', models.CharField(blank=True, default='', max_length=100)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py deleted file mode 100644 index 6e5a198..0000000 --- a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0722.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.0.5 on 2020-04-18 07:22 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uncloud_pay.models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_pay', '0006_auto_20200415_1003'), - ] - - operations = [ - migrations.CreateModel( - name='BillingAddress', - fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100)), - ('street', models.CharField(max_length=100)), - ('city', models.CharField(max_length=50)), - ('postal_code', models.CharField(max_length=50)), - ('country', uncloud_pay.models.CountryField(blank=True, choices=[('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')], default='CH', max_length=2)), - ('vat_number', models.CharField(blank=True, default='', max_length=100)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AddField( - model_name='order', - name='billing_address', - field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.BillingAddress'), - preserve_default=False, - ), - ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py new file mode 100644 index 0000000..c9c2342 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py @@ -0,0 +1,42 @@ +# Generated by Django 3.0.5 on 2020-04-18 07:37 + +from django.db import migrations, models +import django.db.models.deletion +import uncloud_pay.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0006_billingaddress'), + ] + + operations = [ + migrations.RemoveField( + model_name='billingaddress', + name='id', + ), + migrations.AddField( + model_name='billingaddress', + name='name', + field=models.CharField(default='unknown', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='billingaddress', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + migrations.AddField( + model_name='order', + name='billing_address', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.BillingAddress'), + preserve_default=False, + ), + migrations.AlterField( + model_name='billingaddress', + name='country', + field=uncloud_pay.models.CountryField(blank=True, choices=[('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')], default='CH', max_length=2), + ), + ] From dd0c1cba94e593c4934eb4e481945b12456fd94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 10:39:57 +0200 Subject: [PATCH 08/13] Remove legacy ungleich_service migrations --- .../uncloud/uncloud_service/models.py | 3 +- .../uncloud/uncloud_service/serializers.py | 24 ++++++++++--- .../uncloud/uncloud_service/views.py | 11 +++++- .../migrations/0005_auto_20200417_0551.py | 18 ---------- .../migrations/0006_genericserviceproduct.py | 36 ------------------- 5 files changed, 31 insertions(+), 61 deletions(-) delete mode 100644 uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py delete mode 100644 uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py diff --git a/uncloud_django_based/uncloud/uncloud_service/models.py b/uncloud_django_based/uncloud/uncloud_service/models.py index 26bedfd..35a479e 100644 --- a/uncloud_django_based/uncloud/uncloud_service/models.py +++ b/uncloud_django_based/uncloud/uncloud_service/models.py @@ -46,7 +46,8 @@ class GenericServiceProduct(Product): decimal_places=AMOUNT_DECIMALS, validators=[MinValueValidator(0)]) - def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): + @property + def recurring_price(self): # FIXME: handle recurring_period somehow. return self.custom_recurring_price diff --git a/uncloud_django_based/uncloud/uncloud_service/serializers.py b/uncloud_django_based/uncloud/uncloud_service/serializers.py index 2be2cee..eda1377 100644 --- a/uncloud_django_based/uncloud/uncloud_service/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_service/serializers.py @@ -23,12 +23,26 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer): read_only_fields = ['uuid', 'order', 'owner', 'status'] class GenericServiceProductSerializer(serializers.ModelSerializer): - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=GenericServiceProduct.allowed_recurring_periods()) - class Meta: model = GenericServiceProduct fields = ['uuid', 'order', 'owner', 'status', 'custom_recurring_price', - 'custom_description', 'custom_one_time_price', 'recurring_period'] + 'custom_description', 'custom_one_time_price'] read_only_fields = ['uuid', 'order', 'owner', 'status'] + +class OrderGenericServiceProductSerializer(GenericServiceProductSerializer): + recurring_period = serializers.ChoiceField( + choices=GenericServiceProduct.allowed_recurring_periods()) + + def __init__(self, *args, **kwargs): + super(OrderGenericServiceProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for( + self.context['request'].user) + ) + + class Meta: + model = GenericServiceProductSerializer.Meta.model + fields = GenericServiceProductSerializer.Meta.fields + [ + 'recurring_period', 'billing_address' + ] + read_only_fields = GenericServiceProductSerializer.Meta.read_only_fields diff --git a/uncloud_django_based/uncloud/uncloud_service/views.py b/uncloud_django_based/uncloud/uncloud_service/views.py index d4be3a6..2f0f9c2 100644 --- a/uncloud_django_based/uncloud/uncloud_service/views.py +++ b/uncloud_django_based/uncloud/uncloud_service/views.py @@ -44,11 +44,13 @@ class MatrixServiceProductViewSet(ProductViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") # Create base order.) order = Order.objects.create( recurring_period=order_recurring_period, owner=request.user, + billing_address=order_billing_address, starting_date=timezone.now() ) order.save() @@ -72,22 +74,29 @@ class MatrixServiceProductViewSet(ProductViewSet): class GenericServiceProductViewSet(ProductViewSet): permission_classes = [permissions.IsAuthenticated] - serializer_class = GenericServiceProductSerializer def get_queryset(self): return GenericServiceProduct.objects.filter(owner=self.request.user) + def get_serializer_class(self): + if self.action == 'create': + return OrderGenericServiceProductSerializer + else: + return GenericServiceProductSerializer + @transaction.atomic def create(self, request): # Extract serializer data. serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") # Create base order. order = Order.objects.create( recurring_period=order_recurring_period, owner=request.user, + billing_address=order_billing_address, starting_date=timezone.now() ) order.save() diff --git a/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py b/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py deleted file mode 100644 index aed07b6..0000000 --- a/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.5 on 2020-04-17 05:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ungleich_service', '0004_auto_20200403_1727'), - ] - - operations = [ - migrations.AlterField( - model_name='matrixserviceproduct', - name='status', - field=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), - ), - ] diff --git a/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py b/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py deleted file mode 100644 index f4bda32..0000000 --- a/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 3.0.5 on 2020-04-17 08:02 - -from django.conf import settings -import django.contrib.postgres.fields.jsonb -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_pay', '0005_auto_20200417_0551'), - ('ungleich_service', '0005_auto_20200417_0551'), - ] - - operations = [ - migrations.CreateModel( - name='GenericServiceProduct', - fields=[ - ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('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)), - ('custom_description', models.TextField()), - ('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), - ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] From a49fe6ff51bac95c710f70199e405d9994ba1902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 10:40:11 +0200 Subject: [PATCH 09/13] Properly wire billing addresses to uncloud_service --- .../uncloud/uncloud_service/serializers.py | 30 +++++++++++++------ .../uncloud/uncloud_service/views.py | 6 ++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_service/serializers.py b/uncloud_django_based/uncloud/uncloud_service/serializers.py index eda1377..6666a15 100644 --- a/uncloud_django_based/uncloud/uncloud_service/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_service/serializers.py @@ -4,24 +4,36 @@ from uncloud_vm.serializers import ManagedVMProductSerializer from uncloud_vm.models import VMProduct from uncloud_pay.models import RecurringPeriod, BillingAddress +# XXX: the OrderSomethingSomthingProductSerializer classes add a lot of +# boilerplate: can we reduce it somehow? + class MatrixServiceProductSerializer(serializers.ModelSerializer): vm = ManagedVMProductSerializer() - # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=MatrixServiceProduct.allowed_recurring_periods()) - - def __init__(self, *args, **kwargs): - super(MatrixServiceProductSerializer, self).__init__(*args, **kwargs) - self.fields['billing_address'] = serializers.ChoiceField( - choices=BillingAddress.get_addresses_for(self.context['request'].user)) - class Meta: model = MatrixServiceProduct fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period'] read_only_fields = ['uuid', 'order', 'owner', 'status'] +class OrderMatrixServiceProductSerializer(MatrixServiceProductSerializer): + recurring_period = serializers.ChoiceField( + choices=MatrixServiceProduct.allowed_recurring_periods()) + + def __init__(self, *args, **kwargs): + super(OrderMatrixServiceProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for( + self.context['request'].user) + ) + + class Meta: + model = MatrixServiceProductSerializer.Meta.model + fields = MatrixServiceProductSerializer.Meta.fields + [ + 'recurring_period', 'billing_address' + ] + read_only_fields = MatrixServiceProductSerializer.Meta.read_only_fields + class GenericServiceProductSerializer(serializers.ModelSerializer): class Meta: model = GenericServiceProduct diff --git a/uncloud_django_based/uncloud/uncloud_service/views.py b/uncloud_django_based/uncloud/uncloud_service/views.py index 2f0f9c2..abd4a05 100644 --- a/uncloud_django_based/uncloud/uncloud_service/views.py +++ b/uncloud_django_based/uncloud/uncloud_service/views.py @@ -38,6 +38,12 @@ class MatrixServiceProductViewSet(ProductViewSet): def get_queryset(self): return MatrixServiceProduct.objects.filter(owner=self.request.user) + def get_serializer_class(self): + if self.action == 'create': + return OrderMatrixServiceProductSerializer + else: + return MatrixServiceProductSerializer + @transaction.atomic def create(self, request): # Extract serializer data. From db9ff5d18be98892d40b2e570f1917028f992334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 10:43:23 +0200 Subject: [PATCH 10/13] Display allr elevant values on Bill serializer/page --- .../uncloud/uncloud_pay/models.py | 18 +++++++++++++----- .../uncloud/uncloud_pay/serializers.py | 7 +++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 8a71abb..3b545fb 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -525,10 +525,11 @@ class Bill(models.Model): @property def billing_address(self): - return self.order.billing_address + # FIXME: make sure all the orders of a bill match the same billing address. + orders = Order.objects.filter(bill=self) + return orders[0].billing_address @staticmethod - def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. generated_bills = [] @@ -605,7 +606,6 @@ class Bill(models.Model): # Handle yearly bills starting on working month. if len(unpaid_orders['yearly']) > 0: - # For every starting date, generate new bill. for next_yearly_bill_start_on in unpaid_orders['yearly']: # No postpaid for yearly payments. @@ -735,6 +735,10 @@ class BillRecord(): raise Exception('Unsupported recurring period: {}.'. format(record.recurring_period)) + @property + def vat(self): + return 0 + @property def amount(self): return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price @@ -891,12 +895,12 @@ class Product(UncloudModel): if being_created: record = OrderRecord( one_time_price=self.one_time_price, - recurring_price=self.recurring_price(recurring_period=self.recurring_period), + recurring_price=self.recurring_price, description=self.description) self.order.orderrecord_set.add(record, bulk=False) @property - def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): + def recurring_price(self): pass # To be implemented in child. @property @@ -907,6 +911,10 @@ class Product(UncloudModel): def recurring_period(self): return self.order.recurring_period + @property + def billing_address(self): + return self.order.billing_address + @staticmethod def allowed_recurring_periods(): return RecurringPeriod.choices diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 659092c..1f6eb62 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -56,6 +56,13 @@ class BillRecordSerializer(serializers.Serializer): order = serializers.HyperlinkedRelatedField( view_name='order-detail', read_only=True) + 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_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + vat = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) class BillingAddressSerializer(serializers.ModelSerializer): class Meta: From 3a03717b1205fb42c6f4778b8b0cfd0d13d86cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 11:21:11 +0200 Subject: [PATCH 11/13] Split bills between orders of the same billing address --- .../uncloud/uncloud_pay/models.py | 62 +++++++++++------- .../uncloud/uncloud_pay/tests.py | 63 ++++++++++++++++++- 2 files changed, 100 insertions(+), 25 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 3b545fb..f10f813 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -9,10 +9,10 @@ from django.core.exceptions import ObjectDoesNotExist import uuid import logging from functools import reduce +import itertools from math import ceil from datetime import timedelta from calendar import monthrange - from decimal import Decimal import uncloud_pay.stripe @@ -525,10 +525,12 @@ class Bill(models.Model): @property def billing_address(self): - # FIXME: make sure all the orders of a bill match the same billing address. orders = Order.objects.filter(bill=self) + # The genrate_for method makes sure all the orders of a bill share the + # same billing address. TODO: It would be nice to enforce that somehow... return orders[0].billing_address + # TODO: split this huuuge method! @staticmethod def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. @@ -587,22 +589,29 @@ class Bill(models.Model): prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY postpaid_due_date = max(creation_date, ending_date) + BILL_PAYMENT_DELAY - next_monthly_bill = Bill.objects.create(owner=user, + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['monthly_or_less'], + lambda o: o.billing_address) + + for addr, bill_orders in per_address_orders: + next_monthly_bill = Bill.objects.create(owner=user, creation_date=creation_date, starting_date=starting_date, # FIXME: this is a hack! ending_date=ending_date, due_date=postpaid_due_date) - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in unpaid_orders['monthly_or_less']: - order.bill.add(next_monthly_bill) + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_monthly_bill) - logger.info("Generated monthly bill {} (amount: {}) for user {}." + logger.info("Generated monthly bill {} (amount: {}) for user {}." .format(next_monthly_bill.uuid, next_monthly_bill.total, user)) - # Add to output. - generated_bills.append(next_monthly_bill) + # Add to output. + generated_bills.append(next_monthly_bill) # Handle yearly bills starting on working month. if len(unpaid_orders['yearly']) > 0: @@ -614,22 +623,29 @@ class Bill(models.Model): ending_date = next_yearly_bill_start_on.replace( year=next_yearly_bill_start_on.year+1) - timedelta(days=1) - next_yearly_bill = Bill.objects.create(owner=user, - creation_date=creation_date, - starting_date=next_yearly_bill_start_on, - ending_date=ending_date, - due_date=prepaid_due_date) + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['yearly'][next_yearly_bill_start_on], + lambda o: o.billing_address) - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in unpaid_orders['yearly'][next_yearly_bill_start_on]: - order.bill.add(next_yearly_bill) + for addr, bill_orders in per_address_orders: + next_yearly_bill = Bill.objects.create(owner=user, + creation_date=creation_date, + starting_date=next_yearly_bill_start_on, + ending_date=ending_date, + due_date=prepaid_due_date) - logger.info("Generated yearly bill {} (amount: {}) for user {}." - .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_yearly_bill) - # Add to output. - generated_bills.append(next_yearly_bill) + logger.info("Generated yearly bill {} (amount: {}) for user {}." + .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) + + # Add to output. + generated_bills.append(next_yearly_bill) # Return generated (monthly + yearly) bills. return generated_bills diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index 5236c8a..9b23c68 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -143,7 +143,6 @@ class ProductActivationTestCase(TestCase): starting_date=starting_date, recurring_period=RecurringPeriod.PER_MONTH, billing_address=self.billing_address) - order.save() product = GenericServiceProduct( custom_description="Test product", @@ -154,7 +153,7 @@ class ProductActivationTestCase(TestCase): product.save() # XXX: to be automated. - order.add_record(product.one_time_price, product.recurring_price(), product.description) + order.add_record(product.one_time_price, product.recurring_price, product.description) # Validate initial state: must be awaiting payment. self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) @@ -167,3 +166,63 @@ class ProductActivationTestCase(TestCase): GenericServiceProduct.objects.get(uuid=product.uuid).status, UncloudStatus.PENDING ) + +class BillingAddressTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + self.billing_address_01 = BillingAddress.objects.create( + owner=self.user, + street="unknown1", + city="unknown1", + postal_code="unknown1", + country="CH") + + self.billing_address_02 = BillingAddress.objects.create( + owner=self.user, + street="unknown2", + city="unknown2", + postal_code="unknown2", + country="CH") + + def test_billing_with_single_address(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + + # We need a single bill since we work with a single address. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 1) + + def test_billing_with_multiple_addresses(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_02) + + # We need different bills since we work with different addresses. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 2) From b3afad5d5d723efcaab5e5b7d25bf66ea967661b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 11:43:55 +0200 Subject: [PATCH 12/13] Compute VAT rate and amount on bill generation --- .../uncloud/uncloud_pay/models.py | 67 +++++++++++-------- .../uncloud/uncloud_pay/serializers.py | 9 ++- .../uncloud/uncloud_pay/tests.py | 16 ++--- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index f10f813..bcce598 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -469,6 +469,28 @@ class BillingAddress(models.Model): self.name, self.street, self.postal_code, self.city, self.country) +# Populated with the import-vat-numbers django command. +class VATRate(models.Model): + start_date = models.DateField(blank=True, null=True) + stop_date = models.DateField(blank=True, null=True) + territory_codes = models.TextField(blank=True, default='') + currency_code = models.CharField(max_length=10) + rate = models.FloatField() + rate_type = models.TextField(blank=True, default='') + description = models.TextField(blank=True, default='') + + @staticmethod + def get_for_country(country_code): + vat_rate = None + try: + vat_rate = VATRate.objects.get( + territory_codes=country_code, start_date__isnull=False, stop_date=None + ) + return vat_rate.rate + except VATRate.DoesNotExist as dne: + logger.debug(str(dne)) + logger.debug("Did not find VAT rate for %s, returning 0" % country_code) + return 0 class Bill(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -506,9 +528,17 @@ class Bill(models.Model): return bill_records @property - def total(self): + def amount(self): return reduce(lambda acc, record: acc + record.amount, self.records, 0) + @property + def vat_amount(self): + return reduce(lambda acc, record: acc + record.vat_amount, self.records, 0) + + @property + def total(self): + return self.amount + self.vat_amount + @property def final(self): # A bill is final when its ending date is passed. @@ -752,37 +782,20 @@ class BillRecord(): format(record.recurring_period)) @property - def vat(self): - return 0 + def vat_rate(self): + return Decimal(VATRate.get_for_country(self.bill.billing_address.country)) + + @property + def vat_amount(self): + return self.amount * self.vat_rate @property def amount(self): return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price -# Populated with the import-vat-numbers django command. -class VATRate(models.Model): - start_date = models.DateField(blank=True, null=True) - stop_date = models.DateField(blank=True, null=True) - territory_codes = models.TextField(blank=True, default='') - currency_code = models.CharField(max_length=10) - rate = models.FloatField() - rate_type = models.TextField(blank=True, default='') - description = models.TextField(blank=True, default='') - - @staticmethod - def get_for_country(country_code): - vat_rate = None - try: - vat_rate = VATRates.objects.get( - territory_codes=country, start_date__isnull=False, stop_date=None - ) - logger.debug("VAT rate for %s is %s" % (country, vat_rate.rate)) - return vat_rate.rate - except VATRates.DoesNotExist as dne: - logger.debug(str(dne)) - logger.debug("Did not find VAT rate for %s, returning 0" % country) - return 0 - + @property + def total(self): + return self.amount + self.vat_amount ### # Orders. diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py index 1f6eb62..1b5db24 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -61,8 +61,10 @@ class BillRecordSerializer(serializers.Serializer): recurring_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) recurring_period = serializers.ChoiceField(choices=RecurringPeriod.choices) recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - vat = 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) amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) class BillingAddressSerializer(serializers.ModelSerializer): class Meta: @@ -75,8 +77,9 @@ class BillSerializer(serializers.ModelSerializer): class Meta: model = Bill - fields = ['reference', 'owner', 'total', 'due_date', 'creation_date', - 'starting_date', 'ending_date', 'records', 'final', 'billing_address'] + fields = ['reference', 'owner', 'amount', 'vat_amount', 'total', + 'due_date', 'creation_date', 'starting_date', 'ending_date', + 'records', 'final', 'billing_address'] # We do not want users to mutate the country / VAT number of an address, as it # will change VAT on existing bills. diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index 9b23c68..64f0442 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -37,18 +37,18 @@ class BillingTestCase(TestCase): # Generate & check bill for first month: full recurring_price + setup. first_month_bills = order.bills # Initial bill generated at order creation. self.assertEqual(len(first_month_bills), 1) - self.assertEqual(first_month_bills[0].total, one_time_price + recurring_price) + 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].total, recurring_price) + 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].total), + self.assertEqual(float(third_month_bills[0].amount), round((7/31) * recurring_price, AMOUNT_DECIMALS)) # Check that running Bill.generate_for() twice does not create duplicates. @@ -76,7 +76,7 @@ class BillingTestCase(TestCase): 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].total, + self.assertEqual(first_year_bills[0].amount, recurring_price + one_time_price) # Generate & check bill for second year: recurring_price. @@ -86,7 +86,7 @@ class BillingTestCase(TestCase): 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].total, recurring_price) + 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) @@ -114,13 +114,13 @@ class BillingTestCase(TestCase): # Generate & check bill for first month: recurring_price + setup. first_month_bills = order.bills self.assertEqual(len(first_month_bills), 1) - self.assertEqual(float(first_month_bills[0].total), + 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].total), + self.assertEqual(float(second_month_bills[0].amount), round(12 * recurring_price, AMOUNT_DECIMALS)) class ProductActivationTestCase(TestCase): @@ -159,7 +159,7 @@ class ProductActivationTestCase(TestCase): self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) # Pay initial bill, check that product is activated. - amount = product.order.bills[0].total + amount = product.order.bills[0].amount payment = Payment(owner=self.user, amount=amount) payment.save() self.assertEqual( From f61b91dab23a0edc24aee25d3c4c4d3d719c790e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 11:51:13 +0200 Subject: [PATCH 13/13] Catch any exception from VIES VAT check --- .../uncloud/uncloud_pay/views.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py index 5bd1ae6..aaf90e2 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/views.py +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -11,6 +11,7 @@ from vat_validator import validate_vat, vies from vat_validator.countries import EU_COUNTRY_CODES import json +import logging from .models import * from .serializers import * @@ -18,6 +19,8 @@ from datetime import datetime from vat_validator import sanitize_vat import uncloud_pay.stripe as uncloud_stripe +logger = logging.getLogger(__name__) + ### # Payments and Payment Methods. @@ -221,13 +224,19 @@ class BillingAddressViewSet(mixins.CreateModelMixin, {'error': 'Malformed VAT number.'}, status=status.HTTP_400_BAD_REQUEST) elif country in EU_COUNTRY_CODES: - # FIXME: make a synchroneous call to a third patry API here is - # not a good idea... - vies_state = vies.check_vat(country, vat_number) - if not vies_state.valid: + # XXX: make a synchroneous call to a third patry API here might not be a good idea.. + try: + vies_state = vies.check_vat(country, vat_number) + if not vies_state.valid: + return Response( + {'error': 'European VAT number does not exist in VIES.'}, + status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.warning(e) return Response( - {'error': 'European VAT number does not exist in VIES.'}, - status=status.HTTP_400_BAD_REQUEST) + {'error': 'Could not validate EU VAT number against VIES. Try again later..'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + serializer.save(owner=request.user) return Response(serializer.data)