++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
|
** Pre-requisites by operating system
|
||||||
*** General
|
*** General
|
||||||
To run uncloud you need:
|
To run uncloud you need:
|
||||||
|
@ -150,7 +150,6 @@ g #+END_SRC
|
||||||
|
|
||||||
Workers usually should have an "uncloud" user account, even though
|
Workers usually should have an "uncloud" user account, even though
|
||||||
strictly speaking the username can be any.
|
strictly speaking the username can be any.
|
||||||
|
|
||||||
*** WireGuardVPN Server
|
*** WireGuardVPN Server
|
||||||
- Allow write access to /etc/wireguard for uncloud user
|
- Allow write access to /etc/wireguard for uncloud user
|
||||||
- Allow sudo access to "ip" and "wg"
|
- Allow sudo access to "ip" and "wg"
|
||||||
|
@ -161,7 +160,11 @@ g #+END_SRC
|
||||||
app ALL=(ALL) NOPASSWD:/sbin/ip
|
app ALL=(ALL) NOPASSWD:/sbin/ip
|
||||||
app ALL=(ALL) NOPASSWD:/usr/bin/wg
|
app ALL=(ALL) NOPASSWD:/usr/bin/wg
|
||||||
#+END_SRC
|
#+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
|
* Testing / CLI Access
|
||||||
Access via the commandline (CLI) can be done using curl or
|
Access via the commandline (CLI) can be done using curl or
|
||||||
|
@ -462,6 +465,21 @@ Q vpn-2a0ae5c1200.ungleich.ch
|
||||||
- query on that flag
|
- query on that flag
|
||||||
- verify it every time
|
- verify it every time
|
||||||
|
|
||||||
|
|
||||||
***** TODO Generating bill for admins/staff
|
***** 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)
|
street = models.CharField(max_length=256)
|
||||||
city = models.CharField(max_length=256)
|
city = models.CharField(max_length=256)
|
||||||
postal_code = models.CharField(max_length=64)
|
postal_code = models.CharField(max_length=64)
|
||||||
country = CountryField(blank=True)
|
country = CountryField(blank=False, null=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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 %}
|
{% load bootstrap4 %}
|
||||||
|
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -15,7 +14,33 @@
|
||||||
{% block header %}{% endblock %}
|
{% block header %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<div class="container">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,16 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
|
||||||
|
<div id="intro" class="row">
|
||||||
|
<div class=col>
|
||||||
<h1>Welcome to uncloud</h1>
|
<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
|
Welcome to uncloud, the Open Source cloud management
|
||||||
system by <a href="https://ungleich.ch">ungleich</a>.
|
system by <a href="https://ungleich.ch">ungleich</a>.
|
||||||
It is an <a href="{% url 'api-root' %}">API</a> driven system with
|
It is an <a href="{% url 'api-root' %}">API</a> driven system with
|
||||||
|
@ -13,16 +21,45 @@
|
||||||
Framework</a>. You can
|
Framework</a>. You can
|
||||||
freely <a href="https://code.ungleich.ch/uncloud/uncloud/">access
|
freely <a href="https://code.ungleich.ch/uncloud/uncloud/">access
|
||||||
the source code of uncloud</a>.
|
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">
|
<li>Secondy you will need to
|
||||||
<h2>Credit cards</h2>
|
<a href="{% url 'billingaddress-list' %}">create a billing
|
||||||
<div>
|
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
|
Credit cards are registered with stripe. We only save a the
|
||||||
last 4 digits and the expiry date of the card to make
|
last 4 digits and the expiry date of the card to make
|
||||||
identification for you easier.
|
identification for you easier.
|
||||||
</div>
|
</p>
|
||||||
<div>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
|
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
|
||||||
(this is required to be done via Javascript so that we never see
|
(this is required to be done via Javascript so that we never see
|
||||||
|
@ -35,19 +72,40 @@
|
||||||
then set it on another credit card.
|
then set it on another credit card.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="pay">
|
<div id="pay" class="row">
|
||||||
<h2>Payments and Balance</h2>
|
<div class="col"><h3>Billing Address, Payments and Balance</h3></div>
|
||||||
To trigger a payment
|
<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>
|
<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-list' %}">Make a payment or list your payments</a>
|
||||||
<li><a href="{% url 'payment-balance-list' %}">Show your balance</a>
|
<li><a href="{% url 'payment-balance-list' %}">Show your balance</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div id="net">
|
</div>
|
||||||
<h2>Networking</h2>
|
|
||||||
With uncloud you can use a variety of network related services.
|
<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>
|
<ul>
|
||||||
<li>You can <a href="{% url 'wireguardvpnnetwork-list' %}">list or
|
<li>You can <a href="{% url 'wireguardvpnnetwork-list' %}">list or
|
||||||
|
@ -56,7 +114,6 @@
|
||||||
%}">list which network sizes are available</a>
|
%}">list which network sizes are available</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -37,7 +37,7 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc
|
||||||
|
|
||||||
|
|
||||||
# Pay
|
# 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/bill', payviews.BillViewSet, basename='bill')
|
||||||
# router.register(r'v1/my/order', payviews.OrderViewSet, basename='order')
|
# router.register(r'v1/my/order', payviews.OrderViewSet, basename='order')
|
||||||
# router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method')
|
# 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)
|
# router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet)
|
||||||
|
|
||||||
# User/Account
|
# User/Account
|
||||||
router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
|
# router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
|
||||||
router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin')
|
# 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/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/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard')
|
||||||
router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment')
|
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/balance', payviews.BalanceViewSet, basename='payment-balance')
|
||||||
|
router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(r'api/', include(router.urls), name='api'),
|
path(r'api/', include(router.urls), name='api'),
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{% extends 'uncloud/base.html' %}
|
{% extends 'uncloud/base.html' %}
|
||||||
|
{% load bootstrap4 %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<h1>Login to uncloud</h1>
|
||||||
|
<form method="post" class="form">
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{% bootstrap_form form %}
|
||||||
<input type="submit" value="Login">
|
{% buttons %}
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
{% endbuttons %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -47,9 +47,13 @@ class BillAdmin(admin.ModelAdmin):
|
||||||
raise self._get_404_exception(object_id)
|
raise self._get_404_exception(object_id)
|
||||||
|
|
||||||
output_file = NamedTemporaryFile()
|
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()
|
'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)
|
||||||
response = FileResponse(output_file, content_type="application/pdf")
|
response = FileResponse(output_file, content_type="application/pdf")
|
||||||
|
@ -63,7 +67,7 @@ class BillAdmin(admin.ModelAdmin):
|
||||||
if bill is None:
|
if bill is None:
|
||||||
raise self._get_404_exception(object_id)
|
raise self._get_404_exception(object_id)
|
||||||
|
|
||||||
return render(request, 'bill.html.j2',
|
return render(request, 'uncloud_pay/bill.html.j2',
|
||||||
{'bill': bill,
|
{'bill': bill,
|
||||||
'bill_records': bill.billrecord_set.all()
|
'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 logging
|
||||||
import itertools
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
from decimal import Decimal
|
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.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 import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
|
||||||
from uncloud.models import UncloudAddress
|
from uncloud.models import UncloudAddress
|
||||||
|
from .services import *
|
||||||
|
|
||||||
# Used to generate bill due dates.
|
# Used to generate bill due dates.
|
||||||
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
||||||
|
@ -26,36 +25,6 @@ BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
|
||||||
# Initialize logger.
|
# Initialize logger.
|
||||||
logger = logging.getLogger(__name__)
|
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():
|
def default_payment_delay():
|
||||||
return timezone.now() + BILL_PAYMENT_DELAY
|
return timezone.now() + BILL_PAYMENT_DELAY
|
||||||
|
|
||||||
|
@ -68,7 +37,6 @@ class Currency(models.TextChoices):
|
||||||
# USD = 'USD', _('US Dollar')
|
# USD = 'USD', _('US Dollar')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Stripe
|
# Stripe
|
||||||
|
|
||||||
|
@ -95,7 +63,7 @@ class StripeCreditCard(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=['owner'],
|
models.UniqueConstraint(fields=['owner'],
|
||||||
condition=Q(active=True),
|
condition=models.Q(active=True),
|
||||||
name='one_active_card_per_user')
|
name='one_active_card_per_user')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -117,9 +85,7 @@ class Payment(models.Model):
|
||||||
('stripe', 'Stripe'),
|
('stripe', 'Stripe'),
|
||||||
('voucher', 'Voucher'),
|
('voucher', 'Voucher'),
|
||||||
('referral', 'Referral'),
|
('referral', 'Referral'),
|
||||||
('unknown', 'Unknown')
|
))
|
||||||
),
|
|
||||||
default='unknown')
|
|
||||||
|
|
||||||
timestamp = models.DateTimeField(default=timezone.now)
|
timestamp = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
@ -135,6 +101,11 @@ class Payment(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethod(models.Model):
|
class PaymentMethod(models.Model):
|
||||||
|
"""
|
||||||
|
Not sure if this is still in use
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
owner = models.ForeignKey(get_user_model(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
@ -151,15 +122,6 @@ class PaymentMethod(models.Model):
|
||||||
stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
|
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)
|
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
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
if self.source == 'stripe' and self.stripe_payment_method_id != None:
|
if self.source == 'stripe' and self.stripe_payment_method_id != None:
|
||||||
|
@ -276,7 +238,7 @@ class BillingAddress(UncloudAddress):
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=['owner'],
|
models.UniqueConstraint(fields=['owner'],
|
||||||
condition=Q(active=True),
|
condition=models.Q(active=True),
|
||||||
name='one_active_billing_address_per_user')
|
name='one_active_billing_address_per_user')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -297,18 +259,13 @@ class BillingAddress(UncloudAddress):
|
||||||
if not billing_address:
|
if not billing_address:
|
||||||
billing_address = cls.objects.create(owner=owner,
|
billing_address = cls.objects.create(owner=owner,
|
||||||
organization="uncloud admins",
|
organization="uncloud admins",
|
||||||
name="Uncloud Admin",
|
full_name="Uncloud Admin",
|
||||||
street="Uncloudstreet. 42",
|
street="Uncloudstreet. 42",
|
||||||
city="Luchsingen",
|
city="Luchsingen",
|
||||||
postal_code="8775",
|
postal_code="8775",
|
||||||
country="CH",
|
country="CH",
|
||||||
active=True)
|
active=True)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_address_for(user):
|
|
||||||
return BillingAddress.objects.get(owner=user, active=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} - {}, {}, {} {}, {}".format(
|
return "{} - {}, {}, {} {}, {}".format(
|
||||||
self.owner,
|
self.owner,
|
||||||
|
@ -1186,7 +1143,7 @@ class Bill(models.Model):
|
||||||
return bill
|
return bill
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Bill {self.owner}-{self.id}"
|
return f"{self.owner}-{self.id}"
|
||||||
|
|
||||||
|
|
||||||
class BillRecord(models.Model):
|
class BillRecord(models.Model):
|
||||||
|
@ -1256,7 +1213,7 @@ class ProductToRecurringPeriod(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=['product'],
|
models.UniqueConstraint(fields=['product'],
|
||||||
condition=Q(is_default=True),
|
condition=models.Q(is_default=True),
|
||||||
name='one_default_recurring_period_per_product'),
|
name='one_default_recurring_period_per_product'),
|
||||||
models.UniqueConstraint(fields=['product', 'recurring_period'],
|
models.UniqueConstraint(fields=['product', 'recurring_period'],
|
||||||
name='recurring_period_once_per_product')
|
name='recurring_period_once_per_product')
|
||||||
|
|
|
@ -21,3 +21,6 @@ def get_spendings_for_user(user):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def get_balance_for_user(user):
|
def get_balance_for_user(user):
|
||||||
return get_payments_for_user(user) - get_spendings_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):
|
class BalanceSerializer(serializers.Serializer):
|
||||||
balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS)
|
balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS)
|
||||||
|
|
||||||
|
class BillingAddressSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BillingAddress
|
||||||
|
exclude = [ "owner" ]
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Unchecked code
|
# Unchecked code
|
||||||
|
@ -96,11 +101,6 @@ class BillRecordSerializer(serializers.Serializer):
|
||||||
amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
|
||||||
total = 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):
|
class BillSerializer(serializers.ModelSerializer):
|
||||||
billing_address = BillingAddressSerializer(read_only=True)
|
billing_address = BillingAddressSerializer(read_only=True)
|
||||||
records = BillRecordSerializer(many=True, 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>
|
||||||
<div class="d4">
|
<div class="d4">
|
||||||
<div class="b1">
|
<div class="b1">
|
||||||
{{ bill.starting_date|date:"c" }} -
|
Bill id: {{ bill }}
|
||||||
{{ bill.ending_date|date:"c" }}
|
<br>{{ bill.starting_date|date:"Ymd" }} -
|
||||||
<br>Bill id: {{ bill }}
|
{{ bill.ending_date|date:"Ymd" }}
|
||||||
<br>Due: {{ bill.due_date }}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="clear: both;"></div>
|
<div style="clear: both;"></div>
|
||||||
|
@ -703,8 +701,8 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for record in bill_records %}
|
{% for record in bill_records %}
|
||||||
<tr class="table-list">
|
<tr class="table-list">
|
||||||
<td>{{ record.starting_date|date:"c" }}
|
<td>{{ record.starting_date|date:"Ymd-H:i:s" }}
|
||||||
- {{ record.ending_date|date:"c" }}
|
- {{ record.ending_date|date:"Ymd-H:i:s" }}
|
||||||
{{ record.order }}
|
{{ record.order }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ record.price|floatformat:2 }}</td>
|
<td>{{ record.price|floatformat:2 }}</td>
|
||||||
|
|
Loading…
Reference in a new issue