Add Balance Feature
This commit is contained in:
parent
d700e2a3d4
commit
6c6f70202e
32 changed files with 675 additions and 32 deletions
|
@ -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"
|
|
@ -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"
|
|
@ -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()
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
0
yuyu/admin/projects_balance/__init__.py
Normal file
0
yuyu/admin/projects_balance/__init__.py
Normal file
65
yuyu/admin/projects_balance/forms.py
Normal file
65
yuyu/admin/projects_balance/forms.py
Normal 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.'))
|
20
yuyu/admin/projects_balance/panel.py
Normal file
20
yuyu/admin/projects_balance/panel.py
Normal 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"
|
74
yuyu/admin/projects_balance/tables.py
Normal file
74
yuyu/admin/projects_balance/tables.py
Normal 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,)
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
24
yuyu/admin/projects_balance/urls.py
Normal file
24
yuyu/admin/projects_balance/urls.py
Normal 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'),
|
||||
]
|
144
yuyu/admin/projects_balance/views.py
Normal file
144
yuyu/admin/projects_balance/views.py
Normal 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']
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
34
yuyu/cases/balance_use_case.py
Normal file
34
yuyu/cases/balance_use_case.py
Normal 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()
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
10
yuyu/local/enabled/_6116_project_balance.py
Normal file
10
yuyu/local/enabled/_6116_project_balance.py
Normal 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'
|
0
yuyu/project/balance/__init__.py
Normal file
0
yuyu/project/balance/__init__.py
Normal file
20
yuyu/project/balance/panel.py
Normal file
20
yuyu/project/balance/panel.py
Normal 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"
|
21
yuyu/project/balance/tables.py
Normal file
21
yuyu/project/balance/tables.py
Normal 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")
|
33
yuyu/project/balance/templates/balance/balance_table.html
Normal file
33
yuyu/project/balance/templates/balance/balance_table.html
Normal 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 %}
|
19
yuyu/project/balance/urls.py
Normal file
19
yuyu/project/balance/urls.py
Normal 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'),
|
||||
]
|
51
yuyu/project/balance/views.py
Normal file
51
yuyu/project/balance/views.py
Normal 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)
|
Loading…
Add table
Reference in a new issue