Compute VAT rate and amount on bill generation
This commit is contained in:
parent
3a03717b12
commit
b3afad5d5d
3 changed files with 54 additions and 38 deletions
|
@ -469,6 +469,28 @@ class BillingAddress(models.Model):
|
||||||
self.name, self.street, self.postal_code, self.city,
|
self.name, self.street, self.postal_code, self.city,
|
||||||
self.country)
|
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):
|
class Bill(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
@ -506,9 +528,17 @@ class Bill(models.Model):
|
||||||
return bill_records
|
return bill_records
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self):
|
def amount(self):
|
||||||
return reduce(lambda acc, record: acc + record.amount, self.records, 0)
|
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
|
@property
|
||||||
def final(self):
|
def final(self):
|
||||||
# A bill is final when its ending date is passed.
|
# A bill is final when its ending date is passed.
|
||||||
|
@ -752,37 +782,20 @@ class BillRecord():
|
||||||
format(record.recurring_period))
|
format(record.recurring_period))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vat(self):
|
def vat_rate(self):
|
||||||
return 0
|
return Decimal(VATRate.get_for_country(self.bill.billing_address.country))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vat_amount(self):
|
||||||
|
return self.amount * self.vat_rate
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price
|
return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price
|
||||||
|
|
||||||
# Populated with the import-vat-numbers django command.
|
@property
|
||||||
class VATRate(models.Model):
|
def total(self):
|
||||||
start_date = models.DateField(blank=True, null=True)
|
return self.amount + self.vat_amount
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Orders.
|
# Orders.
|
||||||
|
|
|
@ -61,8 +61,10 @@ class BillRecordSerializer(serializers.Serializer):
|
||||||
recurring_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_period = serializers.ChoiceField(choices=RecurringPeriod.choices)
|
||||||
recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
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)
|
amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||||
|
total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||||
|
|
||||||
class BillingAddressSerializer(serializers.ModelSerializer):
|
class BillingAddressSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -75,8 +77,9 @@ class BillSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bill
|
model = Bill
|
||||||
fields = ['reference', 'owner', 'total', 'due_date', 'creation_date',
|
fields = ['reference', 'owner', 'amount', 'vat_amount', 'total',
|
||||||
'starting_date', 'ending_date', 'records', 'final', 'billing_address']
|
'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
|
# We do not want users to mutate the country / VAT number of an address, as it
|
||||||
# will change VAT on existing bills.
|
# will change VAT on existing bills.
|
||||||
|
|
|
@ -37,18 +37,18 @@ class BillingTestCase(TestCase):
|
||||||
# Generate & check bill for first month: full recurring_price + setup.
|
# Generate & check bill for first month: full recurring_price + setup.
|
||||||
first_month_bills = order.bills # Initial bill generated at order creation.
|
first_month_bills = order.bills # Initial bill generated at order creation.
|
||||||
self.assertEqual(len(first_month_bills), 1)
|
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.
|
# Generate & check bill for second month: full recurring_price.
|
||||||
second_month_bills = Bill.generate_for(2020, 4, self.user)
|
second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||||
self.assertEqual(len(second_month_bills), 1)
|
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.
|
# Generate & check bill for third and last month: partial recurring_price.
|
||||||
third_month_bills = Bill.generate_for(2020, 5, self.user)
|
third_month_bills = Bill.generate_for(2020, 5, self.user)
|
||||||
self.assertEqual(len(third_month_bills), 1)
|
self.assertEqual(len(third_month_bills), 1)
|
||||||
# 31 days in May.
|
# 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))
|
round((7/31) * recurring_price, AMOUNT_DECIMALS))
|
||||||
|
|
||||||
# Check that running Bill.generate_for() twice does not create duplicates.
|
# Check that running Bill.generate_for() twice does not create duplicates.
|
||||||
|
@ -76,7 +76,7 @@ class BillingTestCase(TestCase):
|
||||||
date.fromisoformat('2020-03-31'))
|
date.fromisoformat('2020-03-31'))
|
||||||
self.assertEqual(first_year_bills[0].ending_date.date(),
|
self.assertEqual(first_year_bills[0].ending_date.date(),
|
||||||
date.fromisoformat('2021-03-30'))
|
date.fromisoformat('2021-03-30'))
|
||||||
self.assertEqual(first_year_bills[0].total,
|
self.assertEqual(first_year_bills[0].amount,
|
||||||
recurring_price + one_time_price)
|
recurring_price + one_time_price)
|
||||||
|
|
||||||
# Generate & check bill for second year: recurring_price.
|
# Generate & check bill for second year: recurring_price.
|
||||||
|
@ -86,7 +86,7 @@ class BillingTestCase(TestCase):
|
||||||
date.fromisoformat('2021-03-31'))
|
date.fromisoformat('2021-03-31'))
|
||||||
self.assertEqual(second_year_bills[0].ending_date.date(),
|
self.assertEqual(second_year_bills[0].ending_date.date(),
|
||||||
date.fromisoformat('2022-03-30'))
|
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.
|
# Check that running Bill.generate_for() twice does not create duplicates.
|
||||||
self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0)
|
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.
|
# Generate & check bill for first month: recurring_price + setup.
|
||||||
first_month_bills = order.bills
|
first_month_bills = order.bills
|
||||||
self.assertEqual(len(first_month_bills), 1)
|
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)
|
round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price)
|
||||||
|
|
||||||
# Generate & check bill for first month: recurring_price.
|
# Generate & check bill for first month: recurring_price.
|
||||||
second_month_bills = Bill.generate_for(2020, 4, self.user)
|
second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||||
self.assertEqual(len(second_month_bills), 1)
|
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))
|
round(12 * recurring_price, AMOUNT_DECIMALS))
|
||||||
|
|
||||||
class ProductActivationTestCase(TestCase):
|
class ProductActivationTestCase(TestCase):
|
||||||
|
@ -159,7 +159,7 @@ class ProductActivationTestCase(TestCase):
|
||||||
self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT)
|
self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT)
|
||||||
|
|
||||||
# Pay initial bill, check that product is activated.
|
# 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 = Payment(owner=self.user, amount=amount)
|
||||||
payment.save()
|
payment.save()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
Loading…
Reference in a new issue