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):
|
||||
inlines = [ BillRecordInline ]
|
||||
|
||||
# change_list_template = "uncloud_pay/change_list.html"
|
||||
|
||||
def get_urls(self):
|
||||
"""
|
||||
Create URLs for PDF view
|
||||
|
@ -43,15 +41,6 @@ class BillAdmin(admin.ModelAdmin):
|
|||
|
||||
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):
|
||||
bill = self.get_object(request, object_id=object_id)
|
||||
print(bill)
|
||||
|
@ -61,7 +50,7 @@ class BillAdmin(admin.ModelAdmin):
|
|||
|
||||
output_file = NamedTemporaryFile()
|
||||
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)
|
||||
|
|
|
@ -5,8 +5,8 @@ from django.utils import timezone
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from uncloud_pay.models import *
|
||||
#import opennebula.models as one
|
||||
from uncloud_vm.models import *
|
||||
|
||||
import sys
|
||||
|
||||
def vm_price_2020(cpu=1, ram=2, v6only=False):
|
||||
|
@ -35,6 +35,7 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **options):
|
||||
user = get_user_model().objects.get(username=options['username'])
|
||||
|
||||
addr, created = BillingAddress.objects.get_or_create(
|
||||
owner=user,
|
||||
active=True,
|
||||
|
@ -72,6 +73,8 @@ class Command(BaseCommand):
|
|||
vm25206.save()
|
||||
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)
|
||||
|
||||
|
|
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)
|
||||
ending_date = models.DateTimeField()
|
||||
due_date = models.DateField(default=default_payment_delay)
|
||||
|
||||
# what is valid for? should this be "final"?
|
||||
valid = models.BooleanField(default=True)
|
||||
is_final = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
|
@ -409,29 +407,36 @@ class Bill(models.Model):
|
|||
all_orders = Order.objects.filter(owner=owner).order_by('id')
|
||||
first_order = all_orders.first()
|
||||
|
||||
bill = None
|
||||
ending_date = None
|
||||
|
||||
# Calculate the start date
|
||||
# Get date & bill from previous bill
|
||||
if last_bill:
|
||||
# TODO: check that last bill is finished/closed, if not continue using it
|
||||
starting_date = last_bill.end_date + datetime.timedelta(seconds=1)
|
||||
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)
|
||||
else:
|
||||
if first_order:
|
||||
starting_date = first_order.starting_date
|
||||
else:
|
||||
starting_date = timezone.now()
|
||||
|
||||
ending_date = end_of_month(starting_date)
|
||||
|
||||
# FIXME above: maybe even use different date / active / open bill
|
||||
bill, created = cls.objects.get_or_create(
|
||||
owner=owner,
|
||||
starting_date=starting_date,
|
||||
ending_date=ending_date)
|
||||
if not ending_date:
|
||||
ending_date = end_of_month(starting_date)
|
||||
|
||||
# create new bill, if previous is closed/does not exist
|
||||
if not bill:
|
||||
|
||||
bill = cls.objects.create(
|
||||
owner=owner,
|
||||
starting_date=starting_date,
|
||||
ending_date=ending_date)
|
||||
|
||||
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.billrecord_set.count() == 0:
|
||||
br = BillRecord.objects.create(bill=bill,
|
||||
|
@ -447,6 +452,10 @@ class Bill(models.Model):
|
|||
starting_date=starting_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
|
||||
|
||||
@classmethod
|
||||
|
@ -546,7 +555,7 @@ class BillRecord(models.Model):
|
|||
|
||||
@property
|
||||
def sum(self):
|
||||
return self.order.price * self.quantity
|
||||
return self.order.price * Decimal(self.quantity)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bill}: {self.quantity} x {self.order}"
|
||||
|
@ -575,34 +584,54 @@ class Product(UncloudModel):
|
|||
# Default period for all products
|
||||
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))
|
||||
if not billing_address:
|
||||
raise ValidationError("Cannot order without a billing address")
|
||||
|
||||
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:
|
||||
when_to_start = timezone.now()
|
||||
|
||||
if not self.order:
|
||||
billing_address = BillingAddress.get_address_for(self.owner)
|
||||
|
||||
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))
|
||||
self.create_order_at(when_to_start, recurring_period)
|
||||
|
||||
else:
|
||||
previous_order = self.order
|
||||
|
@ -611,9 +640,8 @@ class Product(UncloudModel):
|
|||
new_order = Order.objects.create(owner=self.owner,
|
||||
billing_address=self.order.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,
|
||||
price=self.recurring_price,
|
||||
recurring_period=recurring_period,
|
||||
description=str(self),
|
||||
replaces=self.order)
|
||||
|
||||
|
@ -641,6 +669,14 @@ class Product(UncloudModel):
|
|||
"""
|
||||
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
|
||||
def billing_address(self):
|
||||
return self.order.billing_address
|
||||
|
|
|
@ -673,7 +673,7 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
</div>
|
||||
<div class="d1">
|
||||
{% 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>
|
||||
{% else %}
|
||||
<b>{{ bill.billing_address.name }} <bill.owner.email></b>
|
||||
|
@ -709,19 +709,24 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Beschreibung</th>
|
||||
<th>Detail</th>
|
||||
<th>Amount</th>
|
||||
<th>VAT</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price/Unit</th>
|
||||
<th class="tr">Total</tH>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in bill_records %}
|
||||
<tr class="table-list">
|
||||
<td>A</td>
|
||||
<td>{{ record.quantity }}
|
||||
<td>{{ record.starting_date|date:"c" }}
|
||||
{% if record.ending_date %}
|
||||
- {{ record.ending_date|date:"c" }}
|
||||
{% endif %}
|
||||
{{ record.order.description }}
|
||||
</td>
|
||||
<td>{{ record.quantity|floatformat:2 }}</td>
|
||||
<td>{{ record.order.price|floatformat:2 }}</td>
|
||||
<td>{{ record.sum|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -733,13 +738,13 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
</p>
|
||||
<p class="ts">
|
||||
<span class="tl">VAT</span>
|
||||
<span class="tr">{{ bill.vat_amount }}</span>
|
||||
<span class="tr">{{ bill.vat_amount|floatformat:2 }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="wf pc">
|
||||
<p class="bold">
|
||||
<span class="tl">Gesamtbetrag</span>
|
||||
<span class="tr">{{ bill.total }}</span>
|
||||
<span class="tl">Total</span>
|
||||
<span class="tr">{{ bill.sum|floatformat:2 }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="wf footer">
|
||||
|
|
|
@ -5,6 +5,24 @@ from datetime import datetime, date, timedelta
|
|||
from .models import *
|
||||
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):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create(
|
||||
|
@ -70,7 +88,7 @@ class BillingAddressTestCase(TestCase):
|
|||
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||
|
||||
|
||||
class ProductActivationTestCase(TestCase):
|
||||
class BillAndOrderTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user_without_address = get_user_model().objects.create(
|
||||
username='no_home_person',
|
||||
|
@ -127,8 +145,6 @@ class ProductActivationTestCase(TestCase):
|
|||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_bill_one_time_one_bill_record(self):
|
||||
"""
|
||||
Ensure there is only 1 bill record per order
|
||||
|
|
Loading…
Reference in a new issue