forked from uncloud/uncloud
first bill generation works
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
This commit is contained in:
parent
fd39526350
commit
d7c0c40926
6 changed files with 136 additions and 65 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
22
uncloud_pay/migrations/0009_auto_20200808_2113.py
Normal file
22
uncloud_pay/migrations/0009_auto_20200808_2113.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,29 +407,36 @@ 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:
|
||||||
starting_date = last_bill.end_date + datetime.timedelta(seconds=1)
|
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)
|
||||||
else:
|
else:
|
||||||
if first_order:
|
if first_order:
|
||||||
starting_date = first_order.starting_date
|
starting_date = first_order.starting_date
|
||||||
else:
|
else:
|
||||||
starting_date = timezone.now()
|
starting_date = timezone.now()
|
||||||
|
|
||||||
ending_date = end_of_month(starting_date)
|
|
||||||
|
|
||||||
# FIXME above: maybe even use different date / active / open bill
|
if not ending_date:
|
||||||
bill, created = cls.objects.get_or_create(
|
ending_date = end_of_month(starting_date)
|
||||||
owner=owner,
|
|
||||||
starting_date=starting_date,
|
# create new bill, if previous is closed/does not exist
|
||||||
ending_date=ending_date)
|
if not bill:
|
||||||
|
|
||||||
|
bill = cls.objects.create(
|
||||||
|
owner=owner,
|
||||||
|
starting_date=starting_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,34 +584,54 @@ 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)
|
billing_address = BillingAddress.get_address_for(self.owner)
|
||||||
|
|
||||||
order = Order.objects.create(owner=self.owner,
|
if not billing_address:
|
||||||
billing_address=billing_address,
|
raise ValidationError("Cannot order without a 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 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,
|
||||||
|
starting_date=when_to_start,
|
||||||
|
price=self.one_time_price,
|
||||||
|
recurring_period=RecurringPeriod.ONE_TIME,
|
||||||
|
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:
|
if not when_to_start:
|
||||||
when_to_start = timezone.now()
|
when_to_start = timezone.now()
|
||||||
|
|
||||||
if not self.order:
|
if not self.order:
|
||||||
billing_address = BillingAddress.get_address_for(self.owner)
|
self.create_order_at(when_to_start, recurring_period)
|
||||||
|
|
||||||
if not billing_address:
|
|
||||||
raise ValidationError("Cannot order without a billing address")
|
|
||||||
|
|
||||||
self.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))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
previous_order = self.order
|
previous_order = self.order
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue