++bridge update
This commit is contained in:
parent
6b9b15e663
commit
a920887100
16 changed files with 275 additions and 138 deletions
0
bin/make-migrations-from-scratch.sh
Normal file → Executable file
0
bin/make-migrations-from-scratch.sh
Normal file → Executable file
|
@ -1,4 +1,4 @@
|
|||
* Bootstrap / Installation
|
||||
* Bootstrap / Installation / Deployment
|
||||
** Pre-requisites by operating system
|
||||
*** General
|
||||
To run uncloud you need:
|
||||
|
@ -150,7 +150,6 @@ g #+END_SRC
|
|||
|
||||
Workers usually should have an "uncloud" user account, even though
|
||||
strictly speaking the username can be any.
|
||||
|
||||
*** WireGuardVPN Server
|
||||
- Allow write access to /etc/wireguard for uncloud user
|
||||
- Allow sudo access to "ip" and "wg"
|
||||
|
@ -161,7 +160,11 @@ g #+END_SRC
|
|||
app ALL=(ALL) NOPASSWD:/sbin/ip
|
||||
app ALL=(ALL) NOPASSWD:/usr/bin/wg
|
||||
#+END_SRC
|
||||
|
||||
** Typical source code based deployment
|
||||
- Deploy using bin/deploy.sh on a remote server
|
||||
- Remote server should have
|
||||
- postgresql running, accessible via TLS from outside
|
||||
- rabbitmq-configured [in progress]
|
||||
|
||||
* Testing / CLI Access
|
||||
Access via the commandline (CLI) can be done using curl or
|
||||
|
@ -462,6 +465,21 @@ Q vpn-2a0ae5c1200.ungleich.ch
|
|||
- query on that flag
|
||||
- verify it every time
|
||||
|
||||
|
||||
***** TODO Generating bill for admins/staff
|
||||
-
|
||||
|
||||
|
||||
|
||||
|
||||
**** Bill fixes needed
|
||||
***** TODO Double bill in bill id
|
||||
***** TODO Name the currency
|
||||
***** TODO Maybe remove the chromium pdf rendering artefacts
|
||||
- date on the top
|
||||
- title on the top
|
||||
- filename bottom left
|
||||
- page number could even stay
|
||||
***** TODO Try to shorten the timestamp (remove time zone?)
|
||||
***** TODO Bill date might be required
|
||||
***** TODO Total and VAT are empty
|
||||
***** TODO Line below detail/ heading
|
||||
|
|
19
uncloud/migrations/0004_auto_20210101_1308.py
Normal file
19
uncloud/migrations/0004_auto_20210101_1308.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -61,7 +61,7 @@ class UncloudAddress(models.Model):
|
|||
street = models.CharField(max_length=256)
|
||||
city = models.CharField(max_length=256)
|
||||
postal_code = models.CharField(max_length=64)
|
||||
country = CountryField(blank=True)
|
||||
country = CountryField(blank=False, null=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
|
4
uncloud/static/uncloud/uncloud.css
Normal file
4
uncloud/static/uncloud/uncloud.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
#content {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
{% load bootstrap4 %}
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
@ -15,7 +14,33 @@
|
|||
{% block header %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block bootstrap4_content %}
|
||||
{% block bootstrap4_content %}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{% url 'uncloudindex' %}">uncloud</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<!-- <li class="nav-item"> -->
|
||||
<!-- <a class="nav-link" href="/random/">Generate random prefix</a> -->
|
||||
<!-- </li> -->
|
||||
{% if user.is_authenticated %}
|
||||
<span class="navbar-text">Logged in as {{ user }}.</span>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,16 @@
|
|||
|
||||
{% block body %}
|
||||
<div id="content">
|
||||
|
||||
<div id="intro" class="row">
|
||||
<div class=col>
|
||||
<h1>Welcome to uncloud</h1>
|
||||
<div id="intro">
|
||||
</div>
|
||||
</div>
|
||||
<div id="intro" class="row">
|
||||
<div class="col"><h3>About uncloud</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
Welcome to uncloud, the Open Source cloud management
|
||||
system by <a href="https://ungleich.ch">ungleich</a>.
|
||||
It is an <a href="{% url 'api-root' %}">API</a> driven system with
|
||||
|
@ -13,16 +21,45 @@
|
|||
Framework</a>. You can
|
||||
freely <a href="https://code.ungleich.ch/uncloud/uncloud/">access
|
||||
the source code of uncloud</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="howto" class="row">
|
||||
<div class="col"><h3>Getting started</h3></div>
|
||||
<div class="col-8">
|
||||
<p>uncloud is designed to be as easy as possible to use. However,
|
||||
there are some "real world" requirements that need to be met to
|
||||
start using uncloud:
|
||||
|
||||
<ul>
|
||||
<li>First you need
|
||||
to <a href="https://account.ungleich.ch">register an
|
||||
account</a>. If you already have one, you can
|
||||
<a href="{% url 'login' %}">login</a>.
|
||||
<li>If you have forgotten your password or other issues with
|
||||
logging in, you can contact the ungleich support
|
||||
via <strong>support at ungleich.ch</strong>.
|
||||
|
||||
<div id="creditcards">
|
||||
<h2>Credit cards</h2>
|
||||
<div>
|
||||
<li>Secondy you will need to
|
||||
<a href="{% url 'billingaddress-list' %}">create a billing
|
||||
address</a>. This is required for determining the correct
|
||||
tax.
|
||||
<li>Next you will need to
|
||||
<a href="{% url 'cc_register' %}">register a credit card</a>
|
||||
from which payments can be made. Your credit card will not
|
||||
be charged without your consent.
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="creditcards" class="row">
|
||||
<div class="col"><h3>Credit cards</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
Credit cards are registered with stripe. We only save a the
|
||||
last 4 digits and the expiry date of the card to make
|
||||
identification for you easier.
|
||||
</div>
|
||||
<div>
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
|
||||
(this is required to be done via Javascript so that we never see
|
||||
|
@ -35,19 +72,40 @@
|
|||
then set it on another credit card.
|
||||
</div>
|
||||
</div>
|
||||
<div id="pay">
|
||||
<h2>Payments and Balance</h2>
|
||||
To trigger a payment
|
||||
<div id="pay" class="row">
|
||||
<div class="col"><h3>Billing Address, Payments and Balance</h3></div>
|
||||
<div class="col-8">
|
||||
<p>Billing addresses behave similar to credit cards: you can
|
||||
have many of them, but only one can be active. The active
|
||||
billing address is taken for creating new orders.</p>
|
||||
|
||||
<p>In uncloud we use the pre-paid model: you can add money to
|
||||
your account via payments. You can always check your
|
||||
balance. The products you use will automatically be charged from
|
||||
your existing balance.
|
||||
</p>
|
||||
|
||||
<p>In the future you will be able opt-in to automatically
|
||||
recharging your account at a certain time frame or whenever it
|
||||
is below a certain amount</p>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="{% url 'billingaddress-list' %}">Create or list
|
||||
your billing addresses</a>
|
||||
<li><a href="{% url 'payment-list' %}">Make a payment or list your payments</a>
|
||||
<li><a href="{% url 'payment-balance-list' %}">Show your balance</a>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="net">
|
||||
<h2>Networking</h2>
|
||||
With uncloud you can use a variety of network related services.
|
||||
</div>
|
||||
|
||||
<div id="net" class="row">
|
||||
<div class="col"><h3>Networking</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
With uncloud you can use a variety of network related
|
||||
services.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>You can <a href="{% url 'wireguardvpnnetwork-list' %}">list or
|
||||
|
@ -56,7 +114,6 @@
|
|||
%}">list which network sizes are available</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -37,7 +37,7 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc
|
|||
|
||||
|
||||
# Pay
|
||||
# router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress')
|
||||
|
||||
# router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill')
|
||||
# router.register(r'v1/my/order', payviews.OrderViewSet, basename='order')
|
||||
# router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method')
|
||||
|
@ -49,9 +49,9 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc
|
|||
# router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet)
|
||||
|
||||
# User/Account
|
||||
router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
|
||||
router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin')
|
||||
router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register')
|
||||
# router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
|
||||
# router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin')
|
||||
# router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register')
|
||||
|
||||
|
||||
################################################################################
|
||||
|
@ -65,7 +65,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam
|
|||
router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard')
|
||||
router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment')
|
||||
router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance')
|
||||
|
||||
router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress')
|
||||
|
||||
urlpatterns = [
|
||||
path(r'api/', include(router.urls), name='api'),
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
{% extends 'uncloud/base.html' %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
<form method="post">
|
||||
<h1>Login to uncloud</h1>
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input type="submit" value="Login">
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -47,9 +47,13 @@ class BillAdmin(admin.ModelAdmin):
|
|||
raise self._get_404_exception(object_id)
|
||||
|
||||
output_file = NamedTemporaryFile()
|
||||
bill_html = render_to_string("bill.html.j2", {'bill': bill,
|
||||
bill_html = render_to_string(
|
||||
"uncloud_pay/bill.html.j2",
|
||||
{
|
||||
'bill': bill,
|
||||
'bill_records': bill.billrecord_set.all()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
bytestring_to_pdf(bill_html.encode('utf-8'), output_file)
|
||||
response = FileResponse(output_file, content_type="application/pdf")
|
||||
|
@ -63,7 +67,7 @@ class BillAdmin(admin.ModelAdmin):
|
|||
if bill is None:
|
||||
raise self._get_404_exception(object_id)
|
||||
|
||||
return render(request, 'bill.html.j2',
|
||||
return render(request, 'uncloud_pay/bill.html.j2',
|
||||
{'bill': bill,
|
||||
'bill_records': bill.billrecord_set.all()
|
||||
})
|
||||
|
|
19
uncloud_pay/migrations/0011_auto_20210101_1308.py
Normal file
19
uncloud_pay/migrations/0011_auto_20210101_1308.py
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,24 +1,23 @@
|
|||
import logging
|
||||
import itertools
|
||||
import datetime
|
||||
|
||||
from math import ceil
|
||||
from calendar import monthrange
|
||||
from decimal import Decimal
|
||||
from functools import reduce
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
# Verify whether or not to use them here
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
|
||||
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
|
||||
from uncloud.models import UncloudAddress
|
||||
from .services import *
|
||||
|
||||
# Used to generate bill due dates.
|
||||
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
||||
|
@ -26,36 +25,6 @@ BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
|||
# Initialize logger.
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def start_of_month(a_day):
|
||||
""" Returns first of the month of a given datetime object"""
|
||||
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
|
||||
|
||||
def end_of_month(a_day):
|
||||
""" Returns first of the month of a given datetime object"""
|
||||
|
||||
_, last_day = monthrange(a_day.year, a_day.month)
|
||||
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
|
||||
|
||||
def start_of_this_month():
|
||||
""" Returns first of this month"""
|
||||
a_day = timezone.now()
|
||||
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
|
||||
|
||||
def end_of_this_month():
|
||||
""" Returns first of this month"""
|
||||
a_day = timezone.now()
|
||||
|
||||
_, last_day = monthrange(a_day.year, a_day.month)
|
||||
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
|
||||
|
||||
def end_before(a_date):
|
||||
""" Return suitable datetimefield for ending just before a_date """
|
||||
return a_date - datetime.timedelta(seconds=1)
|
||||
|
||||
def start_after(a_date):
|
||||
""" Return suitable datetimefield for starting just after a_date """
|
||||
return a_date + datetime.timedelta(seconds=1)
|
||||
|
||||
def default_payment_delay():
|
||||
return timezone.now() + BILL_PAYMENT_DELAY
|
||||
|
||||
|
@ -68,7 +37,6 @@ class Currency(models.TextChoices):
|
|||
# USD = 'USD', _('US Dollar')
|
||||
|
||||
|
||||
|
||||
###
|
||||
# Stripe
|
||||
|
||||
|
@ -95,7 +63,7 @@ class StripeCreditCard(models.Model):
|
|||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner'],
|
||||
condition=Q(active=True),
|
||||
condition=models.Q(active=True),
|
||||
name='one_active_card_per_user')
|
||||
]
|
||||
|
||||
|
@ -117,9 +85,7 @@ class Payment(models.Model):
|
|||
('stripe', 'Stripe'),
|
||||
('voucher', 'Voucher'),
|
||||
('referral', 'Referral'),
|
||||
('unknown', 'Unknown')
|
||||
),
|
||||
default='unknown')
|
||||
))
|
||||
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
|
@ -135,6 +101,11 @@ class Payment(models.Model):
|
|||
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
"""
|
||||
Not sure if this is still in use
|
||||
|
||||
"""
|
||||
|
||||
owner = models.ForeignKey(get_user_model(),
|
||||
on_delete=models.CASCADE,
|
||||
editable=False)
|
||||
|
@ -151,15 +122,6 @@ class PaymentMethod(models.Model):
|
|||
stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)
|
||||
|
||||
# @property
|
||||
# def stripe_card_last4(self):
|
||||
# if self.source == 'stripe' and self.active:
|
||||
# payment_method = uncloud_pay.stripe.get_payment_method(
|
||||
# self.stripe_payment_method_id)
|
||||
# return payment_method.card.last4
|
||||
# else:
|
||||
# return None
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
if self.source == 'stripe' and self.stripe_payment_method_id != None:
|
||||
|
@ -276,7 +238,7 @@ class BillingAddress(UncloudAddress):
|
|||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner'],
|
||||
condition=Q(active=True),
|
||||
condition=models.Q(active=True),
|
||||
name='one_active_billing_address_per_user')
|
||||
]
|
||||
|
||||
|
@ -297,18 +259,13 @@ class BillingAddress(UncloudAddress):
|
|||
if not billing_address:
|
||||
billing_address = cls.objects.create(owner=owner,
|
||||
organization="uncloud admins",
|
||||
name="Uncloud Admin",
|
||||
full_name="Uncloud Admin",
|
||||
street="Uncloudstreet. 42",
|
||||
city="Luchsingen",
|
||||
postal_code="8775",
|
||||
country="CH",
|
||||
active=True)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_address_for(user):
|
||||
return BillingAddress.objects.get(owner=user, active=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}, {}, {} {}, {}".format(
|
||||
self.owner,
|
||||
|
@ -1186,7 +1143,7 @@ class Bill(models.Model):
|
|||
return bill
|
||||
|
||||
def __str__(self):
|
||||
return f"Bill {self.owner}-{self.id}"
|
||||
return f"{self.owner}-{self.id}"
|
||||
|
||||
|
||||
class BillRecord(models.Model):
|
||||
|
@ -1256,7 +1213,7 @@ class ProductToRecurringPeriod(models.Model):
|
|||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['product'],
|
||||
condition=Q(is_default=True),
|
||||
condition=models.Q(is_default=True),
|
||||
name='one_default_recurring_period_per_product'),
|
||||
models.UniqueConstraint(fields=['product', 'recurring_period'],
|
||||
name='recurring_period_once_per_product')
|
||||
|
|
|
@ -21,3 +21,6 @@ def get_spendings_for_user(user):
|
|||
@transaction.atomic
|
||||
def get_balance_for_user(user):
|
||||
return get_payments_for_user(user) - get_spendings_for_user(user)
|
||||
|
||||
def get_billing_address_for_user(user):
|
||||
return BillingAddress.objects.get(owner=user, active=True)
|
||||
|
|
|
@ -36,6 +36,11 @@ class PaymentSerializer(serializers.ModelSerializer):
|
|||
class BalanceSerializer(serializers.Serializer):
|
||||
balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS)
|
||||
|
||||
class BillingAddressSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BillingAddress
|
||||
exclude = [ "owner" ]
|
||||
|
||||
|
||||
################################################################################
|
||||
# Unchecked code
|
||||
|
@ -96,11 +101,6 @@ class BillRecordSerializer(serializers.Serializer):
|
|||
amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||
|
||||
class BillingAddressSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BillingAddress
|
||||
fields = ['uuid', 'organization', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number']
|
||||
|
||||
class BillSerializer(serializers.ModelSerializer):
|
||||
billing_address = BillingAddressSerializer(read_only=True)
|
||||
records = BillRecordSerializer(many=True, read_only=True)
|
||||
|
|
32
uncloud_pay/services.py
Normal file
32
uncloud_pay/services.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.utils import timezone
|
||||
|
||||
|
||||
def start_of_month(a_day):
|
||||
""" Returns first of the month of a given datetime object"""
|
||||
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
|
||||
|
||||
def end_of_month(a_day):
|
||||
""" Returns first of the month of a given datetime object"""
|
||||
|
||||
_, last_day = monthrange(a_day.year, a_day.month)
|
||||
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
|
||||
|
||||
def start_of_this_month():
|
||||
""" Returns first of this month"""
|
||||
a_day = timezone.now()
|
||||
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
|
||||
|
||||
def end_of_this_month():
|
||||
""" Returns first of this month"""
|
||||
a_day = timezone.now()
|
||||
|
||||
_, last_day = monthrange(a_day.year, a_day.month)
|
||||
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
|
||||
|
||||
def end_before(a_date):
|
||||
""" Return suitable datetimefield for ending just before a_date """
|
||||
return a_date - datetime.timedelta(seconds=1)
|
||||
|
||||
def start_after(a_date):
|
||||
""" Return suitable datetimefield for starting just after a_date """
|
||||
return a_date + datetime.timedelta(seconds=1)
|
|
@ -680,11 +680,9 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
</div>
|
||||
<div class="d4">
|
||||
<div class="b1">
|
||||
{{ bill.starting_date|date:"c" }} -
|
||||
{{ bill.ending_date|date:"c" }}
|
||||
<br>Bill id: {{ bill }}
|
||||
<br>Due: {{ bill.due_date }}
|
||||
|
||||
Bill id: {{ bill }}
|
||||
<br>{{ bill.starting_date|date:"Ymd" }} -
|
||||
{{ bill.ending_date|date:"Ymd" }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="clear: both;"></div>
|
||||
|
@ -703,8 +701,8 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
|||
<tbody>
|
||||
{% for record in bill_records %}
|
||||
<tr class="table-list">
|
||||
<td>{{ record.starting_date|date:"c" }}
|
||||
- {{ record.ending_date|date:"c" }}
|
||||
<td>{{ record.starting_date|date:"Ymd-H:i:s" }}
|
||||
- {{ record.ending_date|date:"Ymd-H:i:s" }}
|
||||
{{ record.order }}
|
||||
</td>
|
||||
<td>{{ record.price|floatformat:2 }}</td>
|
||||
|
|
Loading…
Reference in a new issue