first bill generation works

Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
This commit is contained in:
Nico Schottelius 2020-08-09 00:37:27 +02:00
parent fd39526350
commit d7c0c40926
6 changed files with 136 additions and 65 deletions

View file

@ -27,8 +27,6 @@ class BillRecordInline(admin.TabularInline):
class BillAdmin(admin.ModelAdmin): class BillAdmin(admin.ModelAdmin):
inlines = [ BillRecordInline ] inlines = [ BillRecordInline ]
# change_list_template = "uncloud_pay/change_list.html"
def get_urls(self): def get_urls(self):
""" """
Create URLs for PDF view Create URLs for PDF view
@ -43,15 +41,6 @@ class BillAdmin(admin.ModelAdmin):
return url_patterns return url_patterns
# def changelist_view(self, request, extra_context=None):
# extra_context = extra_context or {}
# print("view exec")
# return super().changelist_view(
# request, extra_context=extra_context,
# )
def my_view(self, request, object_id): def my_view(self, request, object_id):
bill = self.get_object(request, object_id=object_id) bill = self.get_object(request, object_id=object_id)
print(bill) print(bill)
@ -61,7 +50,7 @@ class BillAdmin(admin.ModelAdmin):
output_file = NamedTemporaryFile() output_file = NamedTemporaryFile()
bill_html = render_to_string("bill.html.j2", {'bill': bill, bill_html = render_to_string("bill.html.j2", {'bill': bill,
'bill_records': bill.bill_records.all() 'bill_records': bill.billrecord_set.all()
}) })
bytestring_to_pdf(bill_html.encode('utf-8'), output_file) bytestring_to_pdf(bill_html.encode('utf-8'), output_file)

View file

@ -5,8 +5,8 @@ from django.utils import timezone
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uncloud_pay.models import * from uncloud_pay.models import *
#import opennebula.models as one
from uncloud_vm.models import * from uncloud_vm.models import *
import sys import sys
def vm_price_2020(cpu=1, ram=2, v6only=False): def vm_price_2020(cpu=1, ram=2, v6only=False):
@ -35,6 +35,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
user = get_user_model().objects.get(username=options['username']) user = get_user_model().objects.get(username=options['username'])
addr, created = BillingAddress.objects.get_or_create( addr, created = BillingAddress.objects.get_or_create(
owner=user, owner=user,
active=True, active=True,
@ -72,6 +73,8 @@ class Command(BaseCommand):
vm25206.save() vm25206.save()
vm25206.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,4,17))) vm25206.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,4,17)))
Bill.create_next_bill_for_user(user)
sys.exit(0) sys.exit(0)

View file

@ -0,0 +1,22 @@
# Generated by Django 3.1 on 2020-08-08 21:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('uncloud_pay', '0008_delete_orderrecord'),
]
operations = [
migrations.RemoveField(
model_name='bill',
name='valid',
),
migrations.AddField(
model_name='bill',
name='is_final',
field=models.BooleanField(default=False),
),
]

View file

@ -382,9 +382,7 @@ class Bill(models.Model):
starting_date = models.DateTimeField(default=start_of_this_month) starting_date = models.DateTimeField(default=start_of_this_month)
ending_date = models.DateTimeField() ending_date = models.DateTimeField()
due_date = models.DateField(default=default_payment_delay) due_date = models.DateField(default=default_payment_delay)
is_final = models.BooleanField(default=False)
# what is valid for? should this be "final"?
valid = models.BooleanField(default=True)
class Meta: class Meta:
constraints = [ constraints = [
@ -409,10 +407,16 @@ class Bill(models.Model):
all_orders = Order.objects.filter(owner=owner).order_by('id') all_orders = Order.objects.filter(owner=owner).order_by('id')
first_order = all_orders.first() first_order = all_orders.first()
bill = None
ending_date = None
# Calculate the start date # Get date & bill from previous bill
if last_bill: if last_bill:
# TODO: check that last bill is finished/closed, if not continue using it if not last_bill.is_final:
bill = last_bill
starting_date = last_bill.starting_date
ending_date = bill.ending_date
else:
starting_date = last_bill.end_date + datetime.timedelta(seconds=1) starting_date = last_bill.end_date + datetime.timedelta(seconds=1)
else: else:
if first_order: if first_order:
@ -420,18 +424,19 @@ class Bill(models.Model):
else: else:
starting_date = timezone.now() starting_date = timezone.now()
if not ending_date:
ending_date = end_of_month(starting_date) ending_date = end_of_month(starting_date)
# FIXME above: maybe even use different date / active / open bill # create new bill, if previous is closed/does not exist
bill, created = cls.objects.get_or_create( if not bill:
bill = cls.objects.create(
owner=owner, owner=owner,
starting_date=starting_date, starting_date=starting_date,
ending_date=ending_date) ending_date=ending_date)
for order in all_orders: for order in all_orders:
# check if order needs to be billed
# check if order has previous billing record
if order.is_one_time: if order.is_one_time:
if order.billrecord_set.count() == 0: if order.billrecord_set.count() == 0:
br = BillRecord.objects.create(bill=bill, br = BillRecord.objects.create(bill=bill,
@ -447,6 +452,10 @@ class Bill(models.Model):
starting_date=starting_date, starting_date=starting_date,
ending_date=ending_date) ending_date=ending_date)
# Filtering ideas:
# If order is replaced, it should not be added anymore if it has been billed "last time"
# If order has ended and finally charged, do not charge anymore
return bill return bill
@classmethod @classmethod
@ -546,7 +555,7 @@ class BillRecord(models.Model):
@property @property
def sum(self): def sum(self):
return self.order.price * self.quantity return self.order.price * Decimal(self.quantity)
def __str__(self): def __str__(self):
return f"{self.bill}: {self.quantity} x {self.order}" return f"{self.bill}: {self.quantity} x {self.order}"
@ -575,35 +584,55 @@ class Product(UncloudModel):
# Default period for all products # Default period for all products
default_recurring_period = RecurringPeriod.PER_30D default_recurring_period = RecurringPeriod.PER_30D
def create_order_at(self, when_to_start, *args, **kwargs): def create_order_at(self, when_to_start=None, recurring_period=None):
billing_address = BillingAddress.get_address_for(self.owner)
order = Order.objects.create(owner=self.owner,
billing_address=billing_address,
starting_date=when_to_start,
one_time_price=self.one_time_price,
recurring_period=self.default_recurring_period,
recurring_price=self.recurring_price,
description=str(self))
def create_or_update_order(self, when_to_start=None):
if not when_to_start:
when_to_start = timezone.now()
if not self.order:
billing_address = BillingAddress.get_address_for(self.owner) billing_address = BillingAddress.get_address_for(self.owner)
if not billing_address: if not billing_address:
raise ValidationError("Cannot order without a billing address") raise ValidationError("Cannot order without a billing address")
self.order = Order.objects.create(owner=self.owner, if not when_to_start:
when_to_start = timezone.now()
if not recurring_period:
recurring_period = self.default_recurring_period
one_time_order = None
if self.one_time_price > 0:
one_time_order = Order.objects.create(owner=self.owner,
billing_address=billing_address, billing_address=billing_address,
starting_date=when_to_start, starting_date=when_to_start,
one_time_price=self.one_time_price, price=self.one_time_price,
recurring_period=self.default_recurring_period, recurring_period=RecurringPeriod.ONE_TIME,
recurring_price=self.recurring_price,
description=str(self)) description=str(self))
if recurring_period != RecurringPeriod.ONE_TIME:
if one_time_order:
recurring_order = Order.objects.create(owner=self.owner,
billing_address=billing_address,
starting_date=when_to_start,
price=self.recurring_price,
recurring_period=recurring_period,
depends_on=one_time_order,
description=str(self))
else:
recurring_order = Order.objects.create(owner=self.owner,
billing_address=billing_address,
starting_date=when_to_start,
price=self.recurring_price,
recurring_period=recurring_period,
description=str(self))
def create_or_update_order(self, when_to_start=None, recurring_period=None):
if not when_to_start:
when_to_start = timezone.now()
if not self.order:
self.create_order_at(when_to_start, recurring_period)
else: else:
previous_order = self.order previous_order = self.order
when_to_end = when_to_start - datetime.timedelta(seconds=1) when_to_end = when_to_start - datetime.timedelta(seconds=1)
@ -611,9 +640,8 @@ class Product(UncloudModel):
new_order = Order.objects.create(owner=self.owner, new_order = Order.objects.create(owner=self.owner,
billing_address=self.order.billing_address, billing_address=self.order.billing_address,
starting_date=when_to_start, starting_date=when_to_start,
one_time_price=self.one_time_price, price=self.recurring_price,
recurring_period=self.default_recurring_period, recurring_period=recurring_period,
recurring_price=self.recurring_price,
description=str(self), description=str(self),
replaces=self.order) replaces=self.order)
@ -641,6 +669,14 @@ class Product(UncloudModel):
""" """
return 0 return 0
@property
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 @property
def billing_address(self): def billing_address(self):
return self.order.billing_address return self.order.billing_address

View file

@ -673,7 +673,7 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
</div> </div>
<div class="d1"> <div class="d1">
{% if bill.billing_address.organization != "" %} {% if bill.billing_address.organization != "" %}
<b>{{ bill.billing_address.organization }}</b> <b>ORG{{ bill.billing_address.organization }}</b>
<br>{{ bill.billing_address.name }} <bill.owner.email> <br>{{ bill.billing_address.name }} <bill.owner.email>
{% else %} {% else %}
<b>{{ bill.billing_address.name }} <bill.owner.email></b> <b>{{ bill.billing_address.name }} <bill.owner.email></b>
@ -709,19 +709,24 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Beschreibung</th>
<th>Detail</th> <th>Detail</th>
<th>Amount</th> <th>Quantity</th>
<th>VAT</th> <th>Price/Unit</th>
<th class="tr">Total</tH> <th class="tr">Total</tH>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for record in bill_records %} {% for record in bill_records %}
<tr class="table-list"> <tr class="table-list">
<td>A</td> <td>{{ record.starting_date|date:"c" }}
<td>{{ record.quantity }} {% if record.ending_date %}
- {{ record.ending_date|date:"c" }}
{% endif %}
{{ record.order.description }}
</td> </td>
<td>{{ record.quantity|floatformat:2 }}</td>
<td>{{ record.order.price|floatformat:2 }}</td>
<td>{{ record.sum|floatformat:2 }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -733,13 +738,13 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
</p> </p>
<p class="ts"> <p class="ts">
<span class="tl">VAT</span> <span class="tl">VAT</span>
<span class="tr">{{ bill.vat_amount }}</span> <span class="tr">{{ bill.vat_amount|floatformat:2 }}</span>
</p> </p>
</div> </div>
<div class="wf pc"> <div class="wf pc">
<p class="bold"> <p class="bold">
<span class="tl">Gesamtbetrag</span> <span class="tl">Total</span>
<span class="tr">{{ bill.total }}</span> <span class="tr">{{ bill.sum|floatformat:2 }}</span>
</p> </p>
</div> </div>
<div class="wf footer"> <div class="wf footer">

View file

@ -5,6 +5,24 @@ from datetime import datetime, date, timedelta
from .models import * from .models import *
from uncloud_service.models import GenericServiceProduct from uncloud_service.models import GenericServiceProduct
class ProductOrderTestCase(TestCase):
"""
Test products and products <-> order interaction
"""
def setUp(self):
self.user = get_user_model().objects.create(
username='random_user',
email='jane.random@domain.tld')
def test_update_one_time_product(self):
"""
One time payment products cannot be updated - can they?
"""
pass
class BillingAddressTestCase(TestCase): class BillingAddressTestCase(TestCase):
def setUp(self): def setUp(self):
self.user = get_user_model().objects.create( self.user = get_user_model().objects.create(
@ -70,7 +88,7 @@ class BillingAddressTestCase(TestCase):
self.assertEqual(BillingAddress.get_address_for(self.user), ba) self.assertEqual(BillingAddress.get_address_for(self.user), ba)
class ProductActivationTestCase(TestCase): class BillAndOrderTestCase(TestCase):
def setUp(self): def setUp(self):
self.user_without_address = get_user_model().objects.create( self.user_without_address = get_user_model().objects.create(
username='no_home_person', username='no_home_person',
@ -127,8 +145,6 @@ class ProductActivationTestCase(TestCase):
) )
def test_bill_one_time_one_bill_record(self): def test_bill_one_time_one_bill_record(self):
""" """
Ensure there is only 1 bill record per order Ensure there is only 1 bill record per order