Add Balance Feature

This commit is contained in:
Setyo Nugroho 2023-03-08 03:33:29 +00:00
parent d700e2a3d4
commit 6c6f70202e
32 changed files with 675 additions and 32 deletions

View file

@ -13,11 +13,13 @@ rm $horizon_path/openstack_dashboard/local/enabled/_6103_admin_billing_price_con
rm $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
rm $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
rm $horizon_path/openstack_dashboard/local/enabled/_6105_admin_billing_projects_invoice.py
rm $horizon_path/openstack_dashboard/local/enabled/_6105_admin_notification_center.py
rm $horizon_path/openstack_dashboard/local/enabled/_6106_admin_notification_center.py
rm $horizon_path/openstack_dashboard/local/enabled/_6107_admin_billing_projects_balance.py
rm $horizon_path/openstack_dashboard/local/enabled/_6111_project_billing_panel_group.py
rm $horizon_path/openstack_dashboard/local/enabled/_6112_project_billing_overview.py
rm $horizon_path/openstack_dashboard/local/enabled/_6113_project_billing_usage_cost.py
rm $horizon_path/openstack_dashboard/local/enabled/_6114_project_billing_invoice.py
rm $horizon_path/openstack_dashboard/local/enabled/_6115_project_billing_setting.py
rm $horizon_path/openstack_dashboard/local/enabled/_6116_project_balance.py
echo "Yuyu Removal Done"

View file

@ -14,12 +14,14 @@ ln -sf $root/yuyu/local/enabled/_6104_admin_billing_setting.py $horizon_path/ope
ln -sf $root/yuyu/local/enabled/_6104_admin_billing_setting.py $horizon_path/openstack_dashboard/local/enabled/_6104_admin_billing_setting.py
ln -sf $root/yuyu/local/enabled/_6105_admin_billing_projects_invoice.py $horizon_path/openstack_dashboard/local/enabled/_6105_admin_billing_projects_invoice.py
ln -sf $root/yuyu/local/enabled/_6106_admin_notification_center.py $horizon_path/openstack_dashboard/local/enabled/_6105_admin_notification_center.py
ln -sf $root/yuyu/local/enabled/_6107_admin_billing_projects_balance.py $horizon_path/openstack_dashboard/local/enabled/_6107_admin_billing_projects_balance.py
ln -sf $root/yuyu/local/enabled/_6111_project_billing_panel_group.py $horizon_path/openstack_dashboard/local/enabled/_6111_project_billing_panel_group.py
ln -sf $root/yuyu/local/enabled/_6112_project_billing_overview.py $horizon_path/openstack_dashboard/local/enabled/_6112_project_billing_overview.py
ln -sf $root/yuyu/local/enabled/_6113_project_billing_usage_cost.py $horizon_path/openstack_dashboard/local/enabled/_6113_project_billing_usage_cost.py
ln -sf $root/yuyu/local/enabled/_6114_project_billing_invoice.py $horizon_path/openstack_dashboard/local/enabled/_6114_project_billing_invoice.py
ln -sf $root/yuyu/local/enabled/_6115_project_billing_setting.py $horizon_path/openstack_dashboard/local/enabled/_6115_project_billing_setting.py
ln -sf $root/yuyu/local/enabled/_6116_project_balance.py $horizon_path/openstack_dashboard/local/enabled/_6116_project_balance.py
echo "Symlink Creation Done"
echo "Now you can configure and use yuyu dashboard"

View file

@ -23,6 +23,10 @@ class SettingForm(forms.SelfHandlingForm):
invoice_tax = forms.IntegerField(label=_("INVOICE TAX (%)"),
required=True)
invoice_auto_deduct_balance = forms.BooleanField(label=_("Invoice Auto Deduct Balance"),
required=False, )
how_to_top_up = forms.CharField(label=_("HOW TO TOP UP"),
required=True, widget=forms.Textarea())
def clean(self):
data = super(SettingForm, self).clean()

View file

@ -8,5 +8,7 @@
<p>{% trans '<b>Company Logo</b> : A logo that will be used in invoice' %}</p>
<p>{% trans '<b>Email Admin</b> : Used to send a notification related invoice and error. You can put multiple email here separated by `,` (comma). Example: admin1@company.com,admin2@company.com' %}</p>
<p>{% trans '<b>Invoice Tax</b> : Tax that will be calculated for each invoice' %}</p>
<p>{% trans '<b>Invoice Auto Deduct Balance</b> : Automatically deduct project balance and finish invoice every end of the month if project balance is sufficient.' %}</p>
<p>{% trans '<b>How To Top Up</b> : Instruction how to top up project balance that can be seen by project user' %}</p>
{% endblock %}

View file

View file

@ -0,0 +1,65 @@
import base64
from django.core import validators
from django.utils.translation import ugettext_lazy as _
from djmoney.forms import MoneyField
from horizon import forms, messages, exceptions
from openstack_dashboard.dashboards.yuyu.cases.balance_use_case import BalanceUseCase
from openstack_dashboard.dashboards.yuyu.cases.setting_use_case import SettingUseCase
class TopUpForm(forms.SelfHandlingForm):
USE_CASE = BalanceUseCase()
amount = MoneyField(label=_("Amount"), min_value=0, max_digits=10)
description = forms.CharField(label=_("Description"))
def __init__(self, request, *args, **kwargs):
self.project_id = kwargs['project_id']
del kwargs['project_id']
super().__init__(request, *args, **kwargs)
def handle(self, request, data):
try:
payload = {
"amount": data['amount'].amount,
"amount_currency": data['amount'].currency.code,
'description': data['description'],
}
result = self.USE_CASE.top_up(request, self.project_id, payload)
messages.success(request, _(f"Successfully Top Up"))
return result
except Exception as e:
exceptions.handle(request,
_('Unable to top up.'))
class TopDownForm(forms.SelfHandlingForm):
USE_CASE = BalanceUseCase()
amount = MoneyField(label=_("Amount"), min_value=0, max_digits=10)
description = forms.CharField(label=_("Description"))
def __init__(self, request, *args, **kwargs):
self.project_id = kwargs['project_id']
del kwargs['project_id']
super().__init__(request, *args, **kwargs)
def handle(self, request, data):
try:
payload = {
"amount": data['amount'].amount,
"amount_currency": data['amount'].currency.code,
'description': data['description'],
}
result = self.USE_CASE.top_down(request, self.project_id, payload)
messages.success(request, _(f"Successfully Top Down"))
return result
except Exception as e:
exceptions.handle(request,
_('Unable to top down.'))

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class ProjectsBalance(horizon.Panel):
name = _("Projects Balance")
slug = "projects_balance"

View file

@ -0,0 +1,74 @@
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class DetailAction(tables.LinkAction):
name = "detail"
verbose_name = "Detail"
def get_link_url(self, datum=None, ):
return reverse("horizon:admin:projects_balance:balance_detail", kwargs={
"project_id": datum['project_id'],
})
class TopUpAction(tables.LinkAction):
name = "top_up"
verbose_name = "Top Up"
icon = "plus"
classes = ("ajax-modal",)
def get_link_url(self, datum=None, ):
return reverse("horizon:admin:projects_balance:top_up", kwargs={
"project_id": self.table.kwargs['project_id'],
})
class TopDownAction(tables.LinkAction):
name = "top_down"
verbose_name = "Top Down"
icon = "minus"
classes = ("ajax-modal",)
def get_link_url(self, datum=None, ):
return reverse("horizon:admin:projects_balance:top_down", kwargs={
"project_id": self.table.kwargs['project_id'],
})
class BalanceProjectTable(tables.DataTable):
project = tables.WrappingColumn('project', verbose_name=_('Project'))
amount = tables.WrappingColumn('amount', verbose_name=_('Amount'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['project']
class Meta(object):
name = "balance_project_table"
hidden_title = True
verbose_name = _("Project Balance")
row_actions = (DetailAction,)
class BalanceTransactionTable(tables.DataTable):
date = tables.WrappingColumn('date', verbose_name=_('Date'))
amount = tables.WrappingColumn('amount', verbose_name=_('Amount'))
action = tables.Column('action', verbose_name=_('Action'))
description = tables.Column('description', verbose_name=_('Description'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['date']
class Meta(object):
name = "balance_table"
hidden_title = True
verbose_name = _("Balance Transaction")
table_actions = (TopUpAction, TopDownAction,)

View file

@ -0,0 +1,9 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Top down balance for current project' %}</p>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Top up balance for current project' %}</p>
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Project Balance") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}
{% block js %}
{{ block.super }}
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=page_title %}
{% endblock page_header %}
{% block main %}
<h3>Current Balance: {{ amount }}</h3>
{{ table.render }}
{% endblock %}
{% block js %}
{{ block.super }}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Top Up" %}{% endblock %}
{% block main %}
{% include "admin/projects_balance/_form_top_down.html" %}
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Top Up" %}{% endblock %}
{% block main %}
{% include "admin/projects_balance/_form_top_up.html" %}
{% endblock %}

View file

@ -0,0 +1,24 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.dashboards.yuyu.admin.projects_balance import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^detail/(?P<project_id>[^/]+)/$', views.DetailBalanceView.as_view(), name='balance_detail'),
url(r'^detail/(?P<project_id>[^/]+)/topup/$',
views.TopUpView.as_view(), name='top_up'),
url(r'^detail/(?P<project_id>[^/]+)/top_down/$',
views.TopDownView.as_view(), name='top_down'),
]

View file

@ -0,0 +1,144 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import dateutil.parser
from django.urls import reverse_lazy, reverse
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from djmoney.money import Money
from horizon import exceptions, forms
from horizon import tables
from openstack_dashboard import api
from .forms import TopUpForm, TopDownForm
from .tables import BalanceProjectTable, BalanceTransactionTable
from ...cases.balance_use_case import BalanceUseCase
from keystoneclient import exceptions as keystone_exceptions
class IndexView(tables.DataTableView):
table_class = BalanceProjectTable
page_title = _("Balance")
template_name = "admin/projects_balance/balance_projects.html"
balance_uc = BalanceUseCase()
def get_data(self):
try:
data = []
project_list, has_more = api.keystone.tenant_list(self.request)
for d in self.balance_uc.list(self.request):
project = list(filter(lambda x: x.id == d['project']['tenant_id'], project_list))
project_name = 'Unknown Project'
project_id = 'invalid'
if len(project) > 0:
project_name = project[0].name
project_id = project[0].id
data.append({
'id': d['id'],
'project_id': project_id,
'project': project_name,
'amount': Money(amount=d['amount'], currency=d['amount_currency']),
})
return list(data)
except Exception as e:
error_message = _('Unable to get balance')
exceptions.handle(self.request, error_message)
class DetailBalanceView(tables.DataTableView):
table_class = BalanceTransactionTable
template_name = "admin/projects_balance/balance_table.html"
balance_uc = BalanceUseCase()
def get_context_data(self, **kwargs):
project_id = self.kwargs['project_id']
balance = self.balance_uc.retrieve_by_project(self.request, project_id)
try:
project = api.keystone.tenant_get(self.request, project_id)
except keystone_exceptions.NotFound:
project = 'Unknown'
context = super().get_context_data(**kwargs)
context['page_title'] = f'Project {project.name} Balance'
context['amount'] = Money(amount=balance['amount'], currency=balance['amount_currency']) if balance else 0
return context
def get_data(self):
project_id = self.kwargs['project_id']
data = []
for d in self.balance_uc.transaction_by_project(self.request, project_id):
data.append({
'id': d['id'],
'date': formats.date_format(dateutil.parser.isoparse(d['created_at']), 'd M Y H:m'),
'amount': Money(amount=d['amount'], currency=d['amount_currency']),
'action': d['action'],
'description': d['description'],
})
return list(data)
class TopUpView(forms.ModalFormView):
form_class = TopUpForm
form_id = "top_up_form"
modal_id = "top_up_modal"
modal_header = _("Top Up")
page_title = _("Top Up")
submit_label = _("Top Up")
template_name = 'admin/projects_balance/form_top_up.html'
def get_form_kwargs(self):
kwargs = super(TopUpView, self).get_form_kwargs()
kwargs['project_id'] = self.kwargs['project_id']
return kwargs
def get_context_data(self, **kwargs):
context = super(TopUpView, self).get_context_data(**kwargs)
context['submit_url'] = reverse('horizon:admin:projects_balance:top_up', kwargs={
'project_id': self.kwargs['project_id']
})
return context
def get_success_url(self):
return reverse('horizon:admin:projects_balance:balance_detail', kwargs={
'project_id': self.kwargs['project_id']
})
class TopDownView(forms.ModalFormView):
form_class = TopDownForm
form_id = "top_down_form"
modal_id = "top_down_modal"
modal_header = _("Top Down")
page_title = _("Top Down")
submit_label = _("Top Down")
template_name = 'admin/projects_balance/form_top_down.html'
def get_form_kwargs(self):
kwargs = super(TopDownView, self).get_form_kwargs()
kwargs['project_id'] = self.kwargs['project_id']
return kwargs
def get_context_data(self, **kwargs):
context = super(TopDownView, self).get_context_data(**kwargs)
context['submit_url'] = reverse('horizon:admin:projects_balance:top_down', kwargs={
'project_id': self.kwargs['project_id']
})
return context
def get_success_url(self):
return reverse('horizon:admin:projects_balance:balance_detail', kwargs={
'project_id': self.kwargs['project_id']
})

View file

@ -2,17 +2,7 @@
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% if invoice %}
{% if invoice.state == 2 %}
<a class="btn btn-primary"
href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}">Set
to Finished</a>
{% endif %}
{% if invoice.state == 100 %}
<a class="btn btn-danger"
href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}">Rollback
to Unpaid</a>
{% endif %}
{% include 'admin/projects_invoice/manage_invoice_modal.html' with invoice=invoice project_balance_amount=project_balance_amount %}
<button onclick="javascript:downloadPdf();" class="btn btn-default">Download PDF</button>

View file

@ -2,19 +2,10 @@
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
<a class="btn btn-default" href="?print=true" target="_blank">Download PDF</a>
{% if invoice.state == 2 %}
<a class="btn btn-primary"
href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}">Set to
Finished</a>
{% endif %}
{% include 'admin/projects_invoice/manage_invoice_modal.html' with invoice=invoice project_balance_amount=project_balance_amount %}
<br/>
<br/>
{% if invoice.state == 100 %}
<a class="btn btn-danger"
href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}">Rollback
to Unpaid</a>
{% endif %}
<br/>
<br/>
{% include 'admin/projects_invoice/base_invoice.html' %}
{% endblock %}
{% block js %}

View file

@ -0,0 +1,51 @@
{% if invoice.state == 2 %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#invoice_finish_modal">
Set to Finished
</button>
{% endif %}
{% if invoice.state == 100 %}
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#invoice_rollback_unpaid_modal">
Rollback to Unpaid
</button>
{% endif %}
<div class="modal fade" id="invoice_finish_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myModalLabel">Finish Invoice</h4>
</div>
<div class="modal-body">
Project Balance: {{ project_balance_amount }}
<br><br>
<a class="btn btn-warning"
href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}&skip_balance=true">Finish without Deduct Balance</a>
<a class="btn btn-success"
href="{% url 'horizon:admin:projects_invoice:finish_invoice' invoice.id %}?next={{ request.path }}">Finish and Deduct Balance</a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="invoice_rollback_unpaid_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myModalLabel">Rollback to Unpaid</h4>
</div>
<div class="modal-body">
Project Balance: {{ project_balance_amount }}
<br><br>
<a class="btn btn-danger"
href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}&skip_balance=true">Rollback without Refund Balance</a>
<a class="btn btn-warning"
href="{% url 'horizon:admin:projects_invoice:rollback_to_unpaid' invoice.id %}?next={{ request.path }}">Rollback and Refund Balance</a>
</div>
</div>
</div>
</div>

View file

@ -27,6 +27,7 @@ from horizon import views
from openstack_dashboard import api
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
from .tables import InvoiceTable
from ...cases.balance_use_case import BalanceUseCase
from ...cases.setting_use_case import SettingUseCase
from ...core.usage_cost.tables import InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, \
SnapshotCostTable, ImageCostTable
@ -42,7 +43,7 @@ class IndexView(tables.DataTableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['project_list'], _ = api.keystone.tenant_list(self.request, user=self.request.user.id)
context['project_list'], _ = api.keystone.tenant_list(self.request)
context['current_project_id'] = self.request.GET.get('project_id', self.request.user.project_id)
context['current_project_name'] = self.request.GET.get('project_name', self.request.user.project_id)
return context
@ -71,6 +72,7 @@ class InvoiceView(views.APIView):
invoice_uc = InvoiceUseCase()
setting_uc = SettingUseCase()
balance_uc = BalanceUseCase()
def get_template_names(self):
if self.request.GET.get('print', None):
@ -81,8 +83,11 @@ class InvoiceView(views.APIView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'], tenant_id=self.kwargs['project_id'])
balance = self.balance_uc.retrieve_by_project(self.request, self.kwargs['project_id'])
context['invoice'] = invoice
context['setting'] = self.setting_uc.get_settings(self.request)
context['project_balance_amount'] = Money(amount=balance['amount'],
currency=balance['amount_currency']) if balance else 0
context['instance_cost'] = self.get_sum_price(invoice, 'instances')
context['volume_cost'] = self.get_sum_price(invoice, 'volumes')
context['fip_cost'] = self.get_sum_price(invoice, 'floating_ips')
@ -108,6 +113,7 @@ class UsageCostView(tables.MultiTableView):
template_name = "admin/projects_invoice/cost_tables.html"
invoice_uc = InvoiceUseCase()
balance_uc = BalanceUseCase()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -119,7 +125,10 @@ class UsageCostView(tables.MultiTableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
balance = self.balance_uc.retrieve_by_project(self.request, self.kwargs['project_id'])
context['invoice'] = self.request.invoice
context['project_balance_amount'] = Money(amount=balance['amount'],
currency=balance['amount_currency']) if balance else 0
return context
def _get_flavor_name(self, flavor_id):
@ -257,7 +266,9 @@ class FinishInvoice(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
self.invoice_uc.finish_invoice(request, kwargs['id'])
skip_balance = request.GET.get('skip_balance', '')
self.invoice_uc.\
finish_invoice(request, kwargs['id'], skip_balance)
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
return HttpResponseRedirect(next_url)
@ -266,6 +277,7 @@ class RollbackToUnpaidInvoice(views.APIView):
invoice_uc = InvoiceUseCase()
def get(self, request, *args, **kwargs):
self.invoice_uc.rollback_to_unpaid_invoice(request, kwargs['id'])
skip_balance = request.GET.get('skip_balance', '')
self.invoice_uc.rollback_to_unpaid_invoice(request, kwargs['id'], skip_balance)
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
return HttpResponseRedirect(next_url)

View file

@ -0,0 +1,34 @@
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
class BalanceUseCase():
def list(self, request):
response = yuyu_client.get(request, f"balance/")
return response.json()
def retrieve_by_project(self, request, tenant_id=None):
if not tenant_id:
tenant_id = request.user.project_id
response = yuyu_client.get(request, f"balance/{tenant_id}/retrieve_by_project/")
if response.status_code == 404:
return None
return response.json()
def transaction_by_project(self, request, tenant_id=None):
if not tenant_id:
tenant_id = request.user.project_id
response = yuyu_client.get(request, f"balance/{tenant_id}/transaction_by_project/")
if response.status_code == 404:
return []
return response.json()
def top_up(self, request, tenant_id, payload):
response = yuyu_client.post(request, f"balance/{tenant_id}/top_up_by_project/", payload)
return response.json()
def top_down(self, request, tenant_id, payload):
response = yuyu_client.post(request, f"balance/{tenant_id}/top_down_by_project/", payload)
return response.json()

View file

@ -110,12 +110,12 @@ class InvoiceUseCase:
def reset_billing(self, request):
yuyu_client.post(request, f"invoice/reset_billing/", {})
def finish_invoice(self, request, id):
response = yuyu_client.get(request, f"invoice/{id}/finish/")
def finish_invoice(self, request, id, skip_balance):
response = yuyu_client.get(request, f"invoice/{id}/finish/?skip_balance={skip_balance}")
data = response.json()
return data
def rollback_to_unpaid_invoice(self, request, id):
response = yuyu_client.get(request, f"invoice/{id}/rollback_to_unpaid/")
def rollback_to_unpaid_invoice(self, request, id, skip_balance):
response = yuyu_client.get(request, f"invoice/{id}/rollback_to_unpaid/?skip_balance={skip_balance}")
data = response.json()
return data

View file

@ -17,6 +17,11 @@ class SettingUseCase:
return response
def get_setting(self, request, key):
response = yuyu_client.get(request, f"settings/{key}").json()
return response
def set_setting(self, request, key, value):
return yuyu_client.patch(request, f"settings/{key}/", {
"value": value

View file

@ -12,7 +12,8 @@ class SettingName:
"company_logo": _("Company Logo"),
"company_address": _("Company Address"),
"email_admin": _("Email Admin"),
"email_notification": _("Email Notification")
"email_notification": _("Email Notification"),
'how_to_top_up': _("How To Top Up"),
}
def get_setting_name(self, setting):

View file

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'projects_balance'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.admin.projects_balance.panel.ProjectsBalance'

View file

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'balance'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'billing'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.project.balance.panel.Balance'
# ADD_PANEL = 'openstack_dashboard.dashboards.yuyu.project.invoice.panel.Invoice'

View file

View file

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Balance(horizon.Panel):
name = _("Balance")
slug = "balance"

View file

@ -0,0 +1,21 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class BalanceTransactionTable(tables.DataTable):
date = tables.WrappingColumn('date', verbose_name=_('Date'))
amount = tables.WrappingColumn('amount', verbose_name=_('Amount'))
action = tables.Column('action', verbose_name=_('Action'))
description = tables.Column('description', verbose_name=_('Description'))
def get_object_id(self, datum):
return datum['id']
def get_object_display(self, datum):
return datum['date']
class Meta(object):
name = "balance_table"
hidden_title = True
verbose_name = _("Balance Transaction")

View file

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block title %}{{ page_title }}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Project Balance") %}
{% endblock page_header %}
{% block main %}
<h3>Current Balance: {{ amount }}</h3>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#how_to_topup_modal">
How To Top Up?
</button>
<div class="modal fade" id="how_to_topup_modal" tabindex="-1" role="dialog" aria-labelledby="howToTopUpLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myModalLabel">How To Top Up?</h4>
</div>
<div class="modal-body">
{{ how_to_top_up.how_to_top_up }}
</div>
</div>
</div>
</div>
<br>
<hr>
<br>
<h3>Balance History</h3>
{{ table.render }}
{% endblock %}
{% block js %}
{{ block.super }}
{% endblock %}

View file

@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from openstack_dashboard.dashboards.yuyu.project.balance import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]

View file

@ -0,0 +1,51 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import dateutil.parser
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from djmoney.money import Money
from horizon import exceptions
from horizon import tables
from .tables import BalanceTransactionTable
from ...cases.balance_use_case import BalanceUseCase
from ...cases.setting_use_case import SettingUseCase
class IndexView(tables.DataTableView):
table_class = BalanceTransactionTable
page_title = _("Balance")
template_name = "project/balance/balance_table.html"
balance_uc = BalanceUseCase()
setting_uc = SettingUseCase()
def get_context_data(self, **kwargs):
balance = self.balance_uc.retrieve_by_project(self.request)
context = super().get_context_data(**kwargs)
context['amount'] = Money(amount=balance['amount'], currency=balance['amount_currency']) if balance else 0
context['how_to_top_up'] = self.setting_uc.get_setting(self.request, 'how_to_top_up')
return context
def get_data(self):
data = []
for d in self.balance_uc.transaction_by_project(self.request):
data.append({
'id': d['id'],
'date': formats.date_format(dateutil.parser.isoparse(d['created_at']), 'd M Y H:m'),
'amount': Money(amount=d['amount'], currency=d['amount_currency']),
'action': d['action'],
'description': d['description'],
})
return list(data)