From 6c6f70202ed9c527b50b7c39db537c7141fcb7b7 Mon Sep 17 00:00:00 2001 From: Setyo Nugroho Date: Wed, 8 Mar 2023 03:33:29 +0000 Subject: [PATCH] Add Balance Feature --- remove_yuyu.sh | 4 +- setup_yuyu.sh | 2 + yuyu/admin/billing_setting/forms.py | 4 + .../billing_setting/_form_setting.html | 2 + yuyu/admin/projects_balance/__init__.py | 0 yuyu/admin/projects_balance/forms.py | 65 ++++++++ yuyu/admin/projects_balance/panel.py | 20 +++ yuyu/admin/projects_balance/tables.py | 74 +++++++++ .../projects_balance/_form_top_down.html | 9 ++ .../projects_balance/_form_top_up.html | 9 ++ .../projects_balance/balance_projects.html | 12 ++ .../projects_balance/balance_table.html | 13 ++ .../projects_balance/form_top_down.html | 8 + .../projects_balance/form_top_up.html | 8 + yuyu/admin/projects_balance/urls.py | 24 +++ yuyu/admin/projects_balance/views.py | 144 ++++++++++++++++++ .../projects_invoice/cost_tables.html | 12 +- .../templates/projects_invoice/invoice.html | 15 +- .../manage_invoice_modal.html | 51 +++++++ yuyu/admin/projects_invoice/views.py | 18 ++- yuyu/cases/balance_use_case.py | 34 +++++ yuyu/cases/invoice_use_case.py | 8 +- yuyu/cases/setting_use_case.py | 5 + yuyu/core/billing_setting/tables.py | 3 +- .../_6107_admin_billing_projects_balance.py | 9 ++ yuyu/local/enabled/_6116_project_balance.py | 10 ++ yuyu/project/balance/__init__.py | 0 yuyu/project/balance/panel.py | 20 +++ yuyu/project/balance/tables.py | 21 +++ .../templates/balance/balance_table.html | 33 ++++ yuyu/project/balance/urls.py | 19 +++ yuyu/project/balance/views.py | 51 +++++++ 32 files changed, 675 insertions(+), 32 deletions(-) create mode 100644 yuyu/admin/projects_balance/__init__.py create mode 100644 yuyu/admin/projects_balance/forms.py create mode 100644 yuyu/admin/projects_balance/panel.py create mode 100644 yuyu/admin/projects_balance/tables.py create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/_form_top_down.html create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/_form_top_up.html create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/balance_projects.html create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/balance_table.html create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/form_top_down.html create mode 100644 yuyu/admin/projects_balance/templates/projects_balance/form_top_up.html create mode 100644 yuyu/admin/projects_balance/urls.py create mode 100644 yuyu/admin/projects_balance/views.py create mode 100644 yuyu/admin/projects_invoice/templates/projects_invoice/manage_invoice_modal.html create mode 100644 yuyu/cases/balance_use_case.py create mode 100644 yuyu/local/enabled/_6107_admin_billing_projects_balance.py create mode 100644 yuyu/local/enabled/_6116_project_balance.py create mode 100644 yuyu/project/balance/__init__.py create mode 100644 yuyu/project/balance/panel.py create mode 100644 yuyu/project/balance/tables.py create mode 100644 yuyu/project/balance/templates/balance/balance_table.html create mode 100644 yuyu/project/balance/urls.py create mode 100644 yuyu/project/balance/views.py diff --git a/remove_yuyu.sh b/remove_yuyu.sh index 1ffd4cd..e5ae3d8 100755 --- a/remove_yuyu.sh +++ b/remove_yuyu.sh @@ -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" \ No newline at end of file diff --git a/setup_yuyu.sh b/setup_yuyu.sh index 8993209..5b49f09 100755 --- a/setup_yuyu.sh +++ b/setup_yuyu.sh @@ -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" \ No newline at end of file diff --git a/yuyu/admin/billing_setting/forms.py b/yuyu/admin/billing_setting/forms.py index 5225696..0e162b3 100644 --- a/yuyu/admin/billing_setting/forms.py +++ b/yuyu/admin/billing_setting/forms.py @@ -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() diff --git a/yuyu/admin/billing_setting/templates/billing_setting/_form_setting.html b/yuyu/admin/billing_setting/templates/billing_setting/_form_setting.html index 856913f..02680bb 100644 --- a/yuyu/admin/billing_setting/templates/billing_setting/_form_setting.html +++ b/yuyu/admin/billing_setting/templates/billing_setting/_form_setting.html @@ -8,5 +8,7 @@

{% trans 'Company Logo : A logo that will be used in invoice' %}

{% trans 'Email Admin : 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' %}

{% trans 'Invoice Tax : Tax that will be calculated for each invoice' %}

+

{% trans 'Invoice Auto Deduct Balance : Automatically deduct project balance and finish invoice every end of the month if project balance is sufficient.' %}

+

{% trans 'How To Top Up : Instruction how to top up project balance that can be seen by project user' %}

{% endblock %} diff --git a/yuyu/admin/projects_balance/__init__.py b/yuyu/admin/projects_balance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/admin/projects_balance/forms.py b/yuyu/admin/projects_balance/forms.py new file mode 100644 index 0000000..6399f04 --- /dev/null +++ b/yuyu/admin/projects_balance/forms.py @@ -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.')) diff --git a/yuyu/admin/projects_balance/panel.py b/yuyu/admin/projects_balance/panel.py new file mode 100644 index 0000000..76acf60 --- /dev/null +++ b/yuyu/admin/projects_balance/panel.py @@ -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" diff --git a/yuyu/admin/projects_balance/tables.py b/yuyu/admin/projects_balance/tables.py new file mode 100644 index 0000000..9dc7c8f --- /dev/null +++ b/yuyu/admin/projects_balance/tables.py @@ -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,) diff --git a/yuyu/admin/projects_balance/templates/projects_balance/_form_top_down.html b/yuyu/admin/projects_balance/templates/projects_balance/_form_top_down.html new file mode 100644 index 0000000..2c1888c --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/_form_top_down.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Top down balance for current project' %}

+{% endblock %} + diff --git a/yuyu/admin/projects_balance/templates/projects_balance/_form_top_up.html b/yuyu/admin/projects_balance/templates/projects_balance/_form_top_up.html new file mode 100644 index 0000000..8b693a9 --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/_form_top_up.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans 'Top up balance for current project' %}

+{% endblock %} + diff --git a/yuyu/admin/projects_balance/templates/projects_balance/balance_projects.html b/yuyu/admin/projects_balance/templates/projects_balance/balance_projects.html new file mode 100644 index 0000000..c5a179f --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/balance_projects.html @@ -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 %} \ No newline at end of file diff --git a/yuyu/admin/projects_balance/templates/projects_balance/balance_table.html b/yuyu/admin/projects_balance/templates/projects_balance/balance_table.html new file mode 100644 index 0000000..4847940 --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/balance_table.html @@ -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 %} +

Current Balance: {{ amount }}

+ {{ table.render }} +{% endblock %} +{% block js %} + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/yuyu/admin/projects_balance/templates/projects_balance/form_top_down.html b/yuyu/admin/projects_balance/templates/projects_balance/form_top_down.html new file mode 100644 index 0000000..bce12f4 --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/form_top_down.html @@ -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 %} diff --git a/yuyu/admin/projects_balance/templates/projects_balance/form_top_up.html b/yuyu/admin/projects_balance/templates/projects_balance/form_top_up.html new file mode 100644 index 0000000..2c64180 --- /dev/null +++ b/yuyu/admin/projects_balance/templates/projects_balance/form_top_up.html @@ -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 %} diff --git a/yuyu/admin/projects_balance/urls.py b/yuyu/admin/projects_balance/urls.py new file mode 100644 index 0000000..977c8ee --- /dev/null +++ b/yuyu/admin/projects_balance/urls.py @@ -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[^/]+)/$', views.DetailBalanceView.as_view(), name='balance_detail'), + url(r'^detail/(?P[^/]+)/topup/$', + views.TopUpView.as_view(), name='top_up'), + url(r'^detail/(?P[^/]+)/top_down/$', + views.TopDownView.as_view(), name='top_down'), +] diff --git a/yuyu/admin/projects_balance/views.py b/yuyu/admin/projects_balance/views.py new file mode 100644 index 0000000..8db1da4 --- /dev/null +++ b/yuyu/admin/projects_balance/views.py @@ -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'] + }) + diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html b/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html index a323ebc..55ee260 100644 --- a/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/cost_tables.html @@ -2,17 +2,7 @@ {% block title %}{{ page_title }}{% endblock %} {% block main %} {% if invoice %} - {% if invoice.state == 2 %} - Set - to Finished - {% endif %} - - {% if invoice.state == 100 %} - Rollback - to Unpaid - {% endif %} + {% include 'admin/projects_invoice/manage_invoice_modal.html' with invoice=invoice project_balance_amount=project_balance_amount %} diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/invoice.html b/yuyu/admin/projects_invoice/templates/projects_invoice/invoice.html index 688a2e9..8401242 100644 --- a/yuyu/admin/projects_invoice/templates/projects_invoice/invoice.html +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/invoice.html @@ -2,19 +2,10 @@ {% block title %}{{ page_title }}{% endblock %} {% block main %} Download PDF - {% if invoice.state == 2 %} - Set to - Finished - {% endif %} + {% include 'admin/projects_invoice/manage_invoice_modal.html' with invoice=invoice project_balance_amount=project_balance_amount %} +
+
- {% if invoice.state == 100 %} - Rollback - to Unpaid - {% endif %} -
-
{% include 'admin/projects_invoice/base_invoice.html' %} {% endblock %} {% block js %} diff --git a/yuyu/admin/projects_invoice/templates/projects_invoice/manage_invoice_modal.html b/yuyu/admin/projects_invoice/templates/projects_invoice/manage_invoice_modal.html new file mode 100644 index 0000000..e880aeb --- /dev/null +++ b/yuyu/admin/projects_invoice/templates/projects_invoice/manage_invoice_modal.html @@ -0,0 +1,51 @@ +{% if invoice.state == 2 %} + +{% endif %} + +{% if invoice.state == 100 %} + +{% endif %} + + + + \ No newline at end of file diff --git a/yuyu/admin/projects_invoice/views.py b/yuyu/admin/projects_invoice/views.py index 83cf84f..118a389 100644 --- a/yuyu/admin/projects_invoice/views.py +++ b/yuyu/admin/projects_invoice/views.py @@ -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) diff --git a/yuyu/cases/balance_use_case.py b/yuyu/cases/balance_use_case.py new file mode 100644 index 0000000..60475ae --- /dev/null +++ b/yuyu/cases/balance_use_case.py @@ -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() diff --git a/yuyu/cases/invoice_use_case.py b/yuyu/cases/invoice_use_case.py index 5177b84..8e7c0d4 100644 --- a/yuyu/cases/invoice_use_case.py +++ b/yuyu/cases/invoice_use_case.py @@ -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 \ No newline at end of file diff --git a/yuyu/cases/setting_use_case.py b/yuyu/cases/setting_use_case.py index 71bb9d4..9790e75 100644 --- a/yuyu/cases/setting_use_case.py +++ b/yuyu/cases/setting_use_case.py @@ -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 diff --git a/yuyu/core/billing_setting/tables.py b/yuyu/core/billing_setting/tables.py index 3974658..9ad6f56 100644 --- a/yuyu/core/billing_setting/tables.py +++ b/yuyu/core/billing_setting/tables.py @@ -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): diff --git a/yuyu/local/enabled/_6107_admin_billing_projects_balance.py b/yuyu/local/enabled/_6107_admin_billing_projects_balance.py new file mode 100644 index 0000000..71901c3 --- /dev/null +++ b/yuyu/local/enabled/_6107_admin_billing_projects_balance.py @@ -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' diff --git a/yuyu/local/enabled/_6116_project_balance.py b/yuyu/local/enabled/_6116_project_balance.py new file mode 100644 index 0000000..da2ab8e --- /dev/null +++ b/yuyu/local/enabled/_6116_project_balance.py @@ -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' diff --git a/yuyu/project/balance/__init__.py b/yuyu/project/balance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yuyu/project/balance/panel.py b/yuyu/project/balance/panel.py new file mode 100644 index 0000000..fcf3840 --- /dev/null +++ b/yuyu/project/balance/panel.py @@ -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" diff --git a/yuyu/project/balance/tables.py b/yuyu/project/balance/tables.py new file mode 100644 index 0000000..7e6e832 --- /dev/null +++ b/yuyu/project/balance/tables.py @@ -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") diff --git a/yuyu/project/balance/templates/balance/balance_table.html b/yuyu/project/balance/templates/balance/balance_table.html new file mode 100644 index 0000000..6d44cb0 --- /dev/null +++ b/yuyu/project/balance/templates/balance/balance_table.html @@ -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 %} +

Current Balance: {{ amount }}

+ + + +
+
+
+

Balance History

+ {{ table.render }} +{% endblock %} +{% block js %} + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/yuyu/project/balance/urls.py b/yuyu/project/balance/urls.py new file mode 100644 index 0000000..f061796 --- /dev/null +++ b/yuyu/project/balance/urls.py @@ -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'), +] diff --git a/yuyu/project/balance/views.py b/yuyu/project/balance/views.py new file mode 100644 index 0000000..844afe8 --- /dev/null +++ b/yuyu/project/balance/views.py @@ -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)