diff --git a/uncloud_pay/migrations/0011_bill_billing_address.py b/uncloud_pay/migrations/0011_bill_billing_address.py new file mode 100644 index 0000000..e13e7b3 --- /dev/null +++ b/uncloud_pay/migrations/0011_bill_billing_address.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-08-09 10:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0010_auto_20200809_0856'), + ] + + operations = [ + migrations.AddField( + model_name='bill', + name='billing_address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.billingaddress'), + ), + ] diff --git a/uncloud_pay/migrations/0012_auto_20200809_1026.py b/uncloud_pay/migrations/0012_auto_20200809_1026.py new file mode 100644 index 0000000..dff0e78 --- /dev/null +++ b/uncloud_pay/migrations/0012_auto_20200809_1026.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-08-09 10:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0011_bill_billing_address'), + ] + + operations = [ + migrations.AlterField( + model_name='bill', + name='billing_address', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.billingaddress'), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 4005908..5d69523 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -347,6 +347,11 @@ class Order(models.Model): def is_one_time(self): return not self.is_recurring + def replace_with(self, new_order): + new_order.replaces = self + self.ending_date = new_order.starting_date - datetime.timedelta(seconds=1) + self.save() + def save(self, *args, **kwargs): if self.ending_date and self.ending_date < self.starting_date: @@ -395,6 +400,13 @@ class Bill(models.Model): ending_date = models.DateTimeField() due_date = models.DateField(default=default_payment_delay) + + billing_address = models.ForeignKey(BillingAddress, + on_delete=models.CASCADE, + editable=True, + null=False) + # FIXME: editable=True -> is in the admin, but also editable in DRF + is_final = models.BooleanField(default=False) class Meta: @@ -408,10 +420,13 @@ class Bill(models.Model): def __str__(self): return f"Bill {self.owner}-{self.id}" - @property - def billing_address(self): - pass -# if self.order_set.all + def close(self): + """ + Close/finish a bill + """ + + self.is_final = True + self.save() @property def sum(self): @@ -420,12 +435,26 @@ class Bill(models.Model): @classmethod - def create_next_bill_for_user(cls, owner, ending_date=None): - last_bill = cls.objects.filter(owner=owner).order_by('id').last() + def create_next_bills_for_user(cls, owner, ending_date=None): + """ + Create one bill per billing address, as the VAT rates might be different + for each address + """ + + bills = [] + + for billing_address in BillingAddress.objects.filter(owner=owner): + bills.append(cls.create_next_bill_for_user_address(owner, billing_address, ending_date)) + + return bills + + @classmethod + def create_next_bill_for_user_address(cls, owner, billing_address, ending_date=None): + last_bill = cls.objects.filter(owner=owner, billing_address=billing_address).order_by('id').last() # it is important to sort orders here, as bill records will be # created (and listed) in this order - all_orders = Order.objects.filter(owner=owner).order_by('id') + all_orders = Order.objects.filter(owner=owner, billing_address=billing_address).order_by('id') first_order = all_orders.first() bill = None @@ -452,7 +481,8 @@ class Bill(models.Model): bill = cls.objects.create( owner=owner, starting_date=starting_date, - ending_date=ending_date) + ending_date=ending_date, + billing_address=billing_address) for order in all_orders: if order.is_one_time: @@ -557,23 +587,6 @@ class Bill(models.Model): cls.create_next_bill_for_user(owner) - # for order in Order.objects.filter(Q(starting_date__gte=self.starting_date), - # Q(starting_date__lte=self.ending_date), - - #for order in Order.objects.filter(owner=owner, - # bill_records=None): - - # For each recurring order get the usage and bill it - - - def close(self): - """ - Close/finish a bill - """ - - self.is_final = True - self.save() - class BillRecord(models.Model): """ Entry of a bill, dynamically generated from an order. @@ -657,8 +670,6 @@ class Product(UncloudModel): one_time_order = None if recurring_period != RecurringPeriod.ONE_TIME: - print("not one time") - if one_time_order: recurring_order = Order.objects.create(owner=self.owner, billing_address=billing_address, @@ -700,15 +711,6 @@ class Product(UncloudModel): self.order = new_order - -# def save(self, *args, **kwargs): - # if not self.order: - # raise ValidationError("Cannot create product without order") - -# self.create_or_update_order() - -# super().save(*args, **kwargs) - @property def recurring_price(self): """ implement correct values in the child class """ @@ -724,9 +726,6 @@ class Product(UncloudModel): def is_recurring(self): return self.recurring_price > 0 - # on is_one_time as this should be has_one_time which is the same as > 0 again... - - @property def billing_address(self): return self.order.billing_address diff --git a/uncloud_pay/templates/bill.html.j2 b/uncloud_pay/templates/bill.html.j2 index 456e4a6..d5993ae 100644 --- a/uncloud_pay/templates/bill.html.j2 +++ b/uncloud_pay/templates/bill.html.j2 @@ -674,7 +674,7 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
{% if bill.billing_address.organization != "" %} ORG{{ bill.billing_address.organization }} -
{{ bill.billing_address.name }} +
{{ bill.billing_address }} {% else %} {{ bill.billing_address.name }} {% endif %} diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 21ac730..3ae8a65 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -72,6 +72,15 @@ class BillingAddressTestCase(TestCase): email='jane.random@domain.tld') + def test_user_no_address(self): + """ + Raise an error, when there is no address + """ + + self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, + BillingAddress.get_address_for, + self.user) + def test_user_only_inactive_address(self): """ Raise an error, when there is no active address @@ -89,7 +98,7 @@ class BillingAddressTestCase(TestCase): BillingAddress.get_address_for, self.user) - def test_user_only_active_address(self): + def test_find_active_address(self): """ Find the active address """ @@ -105,7 +114,7 @@ class BillingAddressTestCase(TestCase): self.assertEqual(BillingAddress.get_address_for(self.user), ba) - def test_multiple_addresses(self): + def test_find_right_address_with_multiple_addresses(self): """ Find the active address only, skip inactive """ @@ -129,6 +138,35 @@ class BillingAddressTestCase(TestCase): self.assertEqual(BillingAddress.get_address_for(self.user), ba) + def test_change_addresses(self): + """ + Switch the active address + """ + + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + self.assertEqual(BillingAddress.get_address_for(self.user), ba) + + ba.active=False + ba.save() + + ba2 = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + self.assertEqual(BillingAddress.get_address_for(self.user), ba2) + + class BillAndOrderTestCase(TestCase): def setUp(self): @@ -144,7 +182,7 @@ class BillAndOrderTestCase(TestCase): username='recurrent_product_user', email='jane.doe@domain.tld') - BillingAddress.objects.create( + self.user_addr = BillingAddress.objects.create( owner=self.user, organization = 'Test org', street="unknown", @@ -152,7 +190,7 @@ class BillAndOrderTestCase(TestCase): postal_code="unknown", active=True) - BillingAddress.objects.create( + self.recurring_user_addr = BillingAddress.objects.create( owner=self.recurring_user, organization = 'Test org', street="Somewhere", @@ -194,12 +232,13 @@ class BillAndOrderTestCase(TestCase): ] + def test_bill_one_time_one_bill_record(self): """ Ensure there is only 1 bill record per order """ - bill = Bill.create_next_bill_for_user(self.user) + bill = Bill.create_next_bill_for_user_address(self.user, self.user_addr) self.assertEqual(self.one_time_order.billrecord_set.count(), 1) @@ -208,7 +247,7 @@ class BillAndOrderTestCase(TestCase): Check the bill sum for a single one time order """ - bill = Bill.create_next_bill_for_user(self.user) + bill = Bill.create_next_bill_for_user_address(self.user, self.user_addr) self.assertEqual(bill.sum, self.order_meta[1]['price']) @@ -217,7 +256,8 @@ class BillAndOrderTestCase(TestCase): Ensure there is only 1 bill record per order """ - bill = Bill.create_next_bill_for_user(self.recurring_user) + bill = Bill.create_next_bill_for_user_address(self.recurring_user, + self.recurring_user_addr) self.assertEqual(self.recurring_order.billrecord_set.count(), 1) self.assertEqual(bill.billrecord_set.count(), 1) @@ -230,13 +270,65 @@ class BillAndOrderTestCase(TestCase): """ for ending_date in self.bill_dates: - b = Bill.create_next_bill_for_user(self.recurring_user, ending_date) + b = Bill.create_next_bill_for_user_address(self.recurring_user, self.recurring_user_addr, ending_date) b.close() bill_count = Bill.objects.filter(owner=self.recurring_user).count() self.assertEqual(len(self.bill_dates), bill_count) + def test_multi_addr_multi_bill(self): + """ + Ensure multiple bills are created if orders exist with different billing addresses + """ + + username="lotsofplaces" + multi_addr_user = get_user_model().objects.create( + username=username, + email=f"{username}@example.org") + + user_addr1 = BillingAddress.objects.create( + owner=multi_addr_user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + order1 = Order.objects.create( + owner=multi_addr_user, + starting_date=self.order_meta[1]['starting_date'], + ending_date=self.order_meta[1]['ending_date'], + recurring_period=RecurringPeriod.ONE_TIME, + price=self.order_meta[1]['price'], + description=self.order_meta[1]['description'], + billing_address=BillingAddress.get_address_for(self.user)) + + # Make this address inactive + user_addr1.active = False + user_addr1.save() + + user_addr2 = BillingAddress.objects.create( + owner=multi_addr_user, + organization = 'Test2 org', + street="unknown2", + city="unknown2", + postal_code="unknown2", + active=True) + + order2 = Order.objects.create( + owner=multi_addr_user, + starting_date=self.order_meta[1]['starting_date'], + ending_date=self.order_meta[1]['ending_date'], + recurring_period=RecurringPeriod.ONE_TIME, + price=self.order_meta[1]['price'], + description=self.order_meta[1]['description'], + billing_address=BillingAddress.get_address_for(self.user)) + + + bills = Bill.create_next_bills_for_user(multi_addr_user) + + self.assertEqual(len(bills), 2) # class NotABillingTC(TestCase): # #class BillingTestCase(TestCase):