Split bills between orders of the same billing address

This commit is contained in:
fnux 2020-04-18 11:21:11 +02:00
parent db9ff5d18b
commit 3a03717b12
2 changed files with 100 additions and 25 deletions

View File

@ -9,10 +9,10 @@ from django.core.exceptions import ObjectDoesNotExist
import uuid import uuid
import logging import logging
from functools import reduce from functools import reduce
import itertools
from math import ceil from math import ceil
from datetime import timedelta from datetime import timedelta
from calendar import monthrange from calendar import monthrange
from decimal import Decimal from decimal import Decimal
import uncloud_pay.stripe import uncloud_pay.stripe
@ -525,10 +525,12 @@ class Bill(models.Model):
@property @property
def billing_address(self): def billing_address(self):
# FIXME: make sure all the orders of a bill match the same billing address.
orders = Order.objects.filter(bill=self) 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 return orders[0].billing_address
# TODO: split this huuuge method!
@staticmethod @staticmethod
def generate_for(year, month, user): def generate_for(year, month, user):
# /!\ We exclusively work on the specified year and month. # /!\ 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 prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY
postpaid_due_date = max(creation_date, ending_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, creation_date=creation_date,
starting_date=starting_date, # FIXME: this is a hack! starting_date=starting_date, # FIXME: this is a hack!
ending_date=ending_date, ending_date=ending_date,
due_date=postpaid_due_date) due_date=postpaid_due_date)
# It is not possible to register many-to-many relationship before # It is not possible to register many-to-many relationship before
# the two end-objects are saved in database. # the two end-objects are saved in database.
for order in unpaid_orders['monthly_or_less']: for order in bill_orders:
order.bill.add(next_monthly_bill) 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)) .format(next_monthly_bill.uuid, next_monthly_bill.total, user))
# Add to output. # Add to output.
generated_bills.append(next_monthly_bill) generated_bills.append(next_monthly_bill)
# Handle yearly bills starting on working month. # Handle yearly bills starting on working month.
if len(unpaid_orders['yearly']) > 0: if len(unpaid_orders['yearly']) > 0:
@ -614,22 +623,29 @@ class Bill(models.Model):
ending_date = next_yearly_bill_start_on.replace( ending_date = next_yearly_bill_start_on.replace(
year=next_yearly_bill_start_on.year+1) - timedelta(days=1) year=next_yearly_bill_start_on.year+1) - timedelta(days=1)
next_yearly_bill = Bill.objects.create(owner=user, # There should not be any bill linked to orders with different
creation_date=creation_date, # billing addresses.
starting_date=next_yearly_bill_start_on, per_address_orders = itertools.groupby(
ending_date=ending_date, unpaid_orders['yearly'][next_yearly_bill_start_on],
due_date=prepaid_due_date) lambda o: o.billing_address)
# It is not possible to register many-to-many relationship before for addr, bill_orders in per_address_orders:
# the two end-objects are saved in database. next_yearly_bill = Bill.objects.create(owner=user,
for order in unpaid_orders['yearly'][next_yearly_bill_start_on]: creation_date=creation_date,
order.bill.add(next_yearly_bill) starting_date=next_yearly_bill_start_on,
ending_date=ending_date,
due_date=prepaid_due_date)
logger.info("Generated yearly bill {} (amount: {}) for user {}." # It is not possible to register many-to-many relationship before
.format(next_yearly_bill.uuid, next_yearly_bill.total, user)) # the two end-objects are saved in database.
for order in bill_orders:
order.bill.add(next_yearly_bill)
# Add to output. logger.info("Generated yearly bill {} (amount: {}) for user {}."
generated_bills.append(next_yearly_bill) .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 (monthly + yearly) bills.
return generated_bills return generated_bills

View File

@ -143,7 +143,6 @@ class ProductActivationTestCase(TestCase):
starting_date=starting_date, starting_date=starting_date,
recurring_period=RecurringPeriod.PER_MONTH, recurring_period=RecurringPeriod.PER_MONTH,
billing_address=self.billing_address) billing_address=self.billing_address)
order.save()
product = GenericServiceProduct( product = GenericServiceProduct(
custom_description="Test product", custom_description="Test product",
@ -154,7 +153,7 @@ class ProductActivationTestCase(TestCase):
product.save() product.save()
# XXX: to be automated. # 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. # Validate initial state: must be awaiting payment.
self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT)
@ -167,3 +166,63 @@ class ProductActivationTestCase(TestCase):
GenericServiceProduct.objects.get(uuid=product.uuid).status, GenericServiceProduct.objects.get(uuid=product.uuid).status,
UncloudStatus.PENDING 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)