initial commit
This commit is contained in:
commit
8fbcba3976
97 changed files with 3570 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
.DS_Store
|
||||||
|
*/CACHE/*
|
||||||
|
static
|
||||||
|
build/*
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.so
|
||||||
|
*~
|
||||||
|
*.db
|
||||||
|
local_settings.py
|
||||||
|
*.jpg.*
|
||||||
|
*.tmproj
|
||||||
|
*.sqlite
|
||||||
|
media/*
|
||||||
|
env/*
|
||||||
|
logs/*
|
||||||
|
|
||||||
|
*.sqlite3
|
||||||
|
projects/logs/*
|
||||||
|
|
||||||
|
# IDE Setting
|
||||||
|
.vscode/*
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
deploy_log/*
|
||||||
|
**.ipynb
|
||||||
|
.vscode
|
||||||
|
.vim
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
hosts
|
31
README.md
Normal file
31
README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Yuyu
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure you know where Horizon is located
|
||||||
|
|
||||||
|
Run this command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup_yuyu.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter horizon location and press ENTER.
|
||||||
|
|
||||||
|
Activate Horzon Virtual Environment if exist.
|
||||||
|
|
||||||
|
Install Yuyu Dashboard Depencencies with
|
||||||
|
|
||||||
|
```
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this config to your horizon `local_settings.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
YUYU_URL="http://yuyu_server_url:8182"
|
||||||
|
CURRENCIES = ('IDR', 'USD')
|
||||||
|
DEFAULT_CURRENCY = "IDR"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart Horizon.
|
21
remove_yuyu.sh
Executable file
21
remove_yuyu.sh
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Specify Horizon Location (ex: /etc/horizon): "
|
||||||
|
read horizon_path
|
||||||
|
root=`pwd -P`
|
||||||
|
|
||||||
|
echo "Removing yuyu Symlink"
|
||||||
|
rm $horizon_path/openstack_dashboard/dashboards/yuyu
|
||||||
|
rm $horizon_path/openstack_dashboard/local/enabled/_6100_yuyu.py
|
||||||
|
rm $horizon_path/openstack_dashboard/local/enabled/_6101_admin_billing_panel_group.py
|
||||||
|
rm $horizon_path/openstack_dashboard/local/enabled/_6102_admin_billing_overview.py
|
||||||
|
rm $horizon_path/openstack_dashboard/local/enabled/_6103_admin_billing_price_configuration.py
|
||||||
|
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/_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
|
||||||
|
echo "Yuyu Removal Done"
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
django-money==2.0.1
|
||||||
|
python-dateutil==2.8.2
|
23
setup_yuyu.sh
Executable file
23
setup_yuyu.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Specify Horizon Location (ex: /etc/horizon): "
|
||||||
|
read horizon_path
|
||||||
|
root=`pwd -P`
|
||||||
|
|
||||||
|
echo "Creating Symlink"
|
||||||
|
ln -sf $root/yuyu $horizon_path/openstack_dashboard/dashboards
|
||||||
|
ln -sf $root/yuyu/local/enabled/_6100_yuyu.py $horizon_path/openstack_dashboard/local/enabled/_6100_yuyu.py
|
||||||
|
ln -sf $root/yuyu/local/enabled/_6101_admin_billing_panel_group.py $horizon_path/openstack_dashboard/local/enabled/_6101_admin_billing_panel_group.py
|
||||||
|
ln -sf $root/yuyu/local/enabled/_6102_admin_billing_overview.py $horizon_path/openstack_dashboard/local/enabled/_6102_admin_billing_overview.py
|
||||||
|
ln -sf $root/yuyu/local/enabled/_6103_admin_billing_price_configuration.py $horizon_path/openstack_dashboard/local/enabled/_6103_admin_billing_price_configuration.py
|
||||||
|
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/_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/_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
|
||||||
|
|
||||||
|
echo "Symlink Creation Done"
|
||||||
|
echo "Now you can configure and yuyu dashboard"
|
0
yuyu/__init__.py
Normal file
0
yuyu/__init__.py
Normal file
0
yuyu/admin/billing_overview/__init__.py
Normal file
0
yuyu/admin/billing_overview/__init__.py
Normal file
20
yuyu/admin/billing_overview/panel.py
Normal file
20
yuyu/admin/billing_overview/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 BillingOverview(horizon.Panel):
|
||||||
|
name = _("Billing Overview")
|
||||||
|
slug = "billing_overview"
|
|
@ -0,0 +1,132 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Billing Setting" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">This Month Total Resource Allocation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="totalResourceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Active Resource Allocation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="activeResourceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">This Month All Resource Price Charged</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="totalResourcePriceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Active Resource Price Charged</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="activeResourcePriceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"
|
||||||
|
integrity="sha512-TW5s0IT/IppJtu76UbysrBH9Hy/5X41OTAbQuffZFU6lQ1rdcLHzpU5BzVvr/YFykoiMYZVWlr/PX1mDcfM9Qg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function showBarChart(id, title, data) {
|
||||||
|
const ctx = document.getElementById(id).getContext('2d');
|
||||||
|
const myChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.label,
|
||||||
|
datasets: [{
|
||||||
|
label: title,
|
||||||
|
data: data.data,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 99, 132, 1)',
|
||||||
|
'rgba(54, 162, 235, 1)',
|
||||||
|
'rgba(255, 206, 86, 1)',
|
||||||
|
'rgba(75, 192, 192, 1)',
|
||||||
|
'rgba(153, 102, 255, 1)',
|
||||||
|
'rgba(255, 159, 64, 1)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPieChart(id, title, data) {
|
||||||
|
const ctx = document.getElementById(id).getContext('2d');
|
||||||
|
const myChart = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: data.label,
|
||||||
|
datasets: [{
|
||||||
|
label: title,
|
||||||
|
data: data.data,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 99, 132, 1)',
|
||||||
|
'rgba(54, 162, 235, 1)',
|
||||||
|
'rgba(255, 206, 86, 1)',
|
||||||
|
'rgba(75, 192, 192, 1)',
|
||||||
|
'rgba(153, 102, 255, 1)',
|
||||||
|
'rgba(255, 159, 64, 1)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showBarChart('totalResourceChart', 'This Month Total Resource Allocation', JSON.parse('{{ total_resource_json | safe }}'));
|
||||||
|
showPieChart('activeResourceChart', 'Active Resource Allocation', JSON.parse('{{ active_resource_json | safe }}'));
|
||||||
|
showBarChart('totalResourcePriceChart', 'This Month All Resource Price Charged', JSON.parse('{{ price_total_resource_json | safe }}'));
|
||||||
|
showPieChart('activeResourcePriceChart', 'Active Resource Price Charged', JSON.parse('{{ price_active_resource_json | safe }}'));
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
19
yuyu/admin/billing_overview/urls.py
Normal file
19
yuyu/admin/billing_overview/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 . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
]
|
34
yuyu/admin/billing_overview/views.py
Normal file
34
yuyu/admin/billing_overview/views.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
from django import shortcuts
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import views
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.admin_overview_use_case import AdminOverviewUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(views.APIView):
|
||||||
|
page_title = _("Billing Overview")
|
||||||
|
template_name = "admin/billing_overview/index.html"
|
||||||
|
|
||||||
|
overview_uc = AdminOverviewUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['total_resource_json'] = json.dumps(self.overview_uc.total_resource(self.request))
|
||||||
|
context['active_resource_json'] = json.dumps(self.overview_uc.active_resource(self.request))
|
||||||
|
context['price_total_resource_json'] = json.dumps(self.overview_uc.price_total_resource(self.request))
|
||||||
|
context['price_active_resource_json'] = json.dumps(self.overview_uc.price_active_resource(self.request))
|
||||||
|
|
||||||
|
return context
|
0
yuyu/admin/billing_setting/__init__.py
Normal file
0
yuyu/admin/billing_setting/__init__.py
Normal file
20
yuyu/admin/billing_setting/panel.py
Normal file
20
yuyu/admin/billing_setting/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 BillingSetting(horizon.Panel):
|
||||||
|
name = _("Billing Setting")
|
||||||
|
slug = "billing_setting"
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Billing Setting" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/missing_prices.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{% if setting.billing_enabled %}
|
||||||
|
<h1>Billing Enabled</h1> <br/>
|
||||||
|
<a href="{% url 'horizon:admin:billing_setting:disable_billing' %}" class="btn btn-primary">Disable Billing</a>
|
||||||
|
{% else %}
|
||||||
|
<h1>Billing Disabled</h1> <br/>
|
||||||
|
<p>Please make sure all price is already configured before enable billing</p>
|
||||||
|
|
||||||
|
<a href="{% url 'horizon:admin:billing_setting:enable_billing' %}" class="btn btn-primary {% if missing_price.has_missing %} disabled {% endif %}">Enable</a>
|
||||||
|
<a href="{% url 'horizon:admin:billing_setting:reset_billing' %}" class="btn btn-danger">Reset Billing Data</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
23
yuyu/admin/billing_setting/urls.py
Normal file
23
yuyu/admin/billing_setting/urls.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# 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 . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^enable_billing$', views.EnableBillingView.as_view(), name='enable_billing'),
|
||||||
|
url(r'^disable_billing$', views.DisableBillingView.as_view(), name='disable_billing'),
|
||||||
|
url(r'^reset_billing$', views.ResetBillingView.as_view(), name='reset_billing'),
|
||||||
|
]
|
||||||
|
|
68
yuyu/admin/billing_setting/views.py
Normal file
68
yuyu/admin/billing_setting/views.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# 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 import shortcuts
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import views, exceptions, messages
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.setting_use_case import SettingUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core.utils.price_checker import has_missing_price
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(views.APIView):
|
||||||
|
page_title = _("Setting")
|
||||||
|
template_name = "admin/billing_setting/index.html"
|
||||||
|
|
||||||
|
setting_uc = SettingUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['setting'] = self.setting_uc.get_settings(self.request)
|
||||||
|
context['missing_price'] = has_missing_price(self.request)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EnableBillingView(views.APIView):
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.invoice_uc.enable_billing(request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to enable billing, Please check your price configuration"))
|
||||||
|
return shortcuts.redirect("horizon:admin:billing_setting:index")
|
||||||
|
|
||||||
|
|
||||||
|
class DisableBillingView(views.APIView):
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.invoice_uc.disable_billing(request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to disable billing"))
|
||||||
|
return shortcuts.redirect("horizon:admin:billing_setting:index")
|
||||||
|
|
||||||
|
|
||||||
|
class ResetBillingView(views.APIView):
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.invoice_uc.reset_billing(request)
|
||||||
|
messages.success(request, _("Data successfully Reset."))
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to reset billing"))
|
||||||
|
return shortcuts.redirect("horizon:admin:billing_setting:index")
|
0
yuyu/admin/price_configuration/__init__.py
Normal file
0
yuyu/admin/price_configuration/__init__.py
Normal file
65
yuyu/admin/price_configuration/forms.py
Normal file
65
yuyu/admin/price_configuration/forms.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import forms
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core.pricing_admin.forms import BasePriceForm
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorPriceForm(BasePriceForm):
|
||||||
|
flavor = forms.ThemableChoiceField(label=_("Flavor"))
|
||||||
|
|
||||||
|
USE_CASE = FlavorPriceUseCase()
|
||||||
|
NAME = "Flavor Price"
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
flavor_list = kwargs.get('initial', {}).get('flavor_list', [])
|
||||||
|
self.fields['flavor'].choices = flavor_list
|
||||||
|
|
||||||
|
def to_payload(self, data):
|
||||||
|
payload = super().to_payload(data)
|
||||||
|
payload["flavor_id"] = data["flavor"]
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePriceForm(BasePriceForm):
|
||||||
|
volume_type = forms.ThemableChoiceField(label=_("Volume Type"))
|
||||||
|
|
||||||
|
USE_CASE = VolumePriceUseCase()
|
||||||
|
NAME = "Volume Price"
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
volume_type_list = kwargs.get('initial', {}).get('volume_type_list', [])
|
||||||
|
self.fields['volume_type'].choices = volume_type_list
|
||||||
|
|
||||||
|
def to_payload(self, data):
|
||||||
|
payload = super().to_payload(data)
|
||||||
|
payload["volume_type_id"] = data["volume_type"]
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpPriceForm(BasePriceForm):
|
||||||
|
USE_CASE = FloatingIpPriceUseCase()
|
||||||
|
NAME = "Floating IP Price"
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPriceForm(BasePriceForm):
|
||||||
|
USE_CASE = RouterPriceUseCase()
|
||||||
|
NAME = "Router Price"
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotPriceForm(BasePriceForm):
|
||||||
|
USE_CASE = SnapshotPriceUseCase()
|
||||||
|
NAME = "Snapshot Price"
|
||||||
|
|
||||||
|
class ImagePriceForm(BasePriceForm):
|
||||||
|
USE_CASE = ImagePriceUseCase()
|
||||||
|
NAME = "Image Price"
|
20
yuyu/admin/price_configuration/panel.py
Normal file
20
yuyu/admin/price_configuration/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 PriceConfiguration(horizon.Panel):
|
||||||
|
name = _("Price Configuration")
|
||||||
|
slug = "price_configuration"
|
245
yuyu/admin/price_configuration/tables.py
Normal file
245
yuyu/admin/price_configuration/tables.py
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import tables
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core.pricing_admin.tables import BasePriceTable, BaseCreatePrice, \
|
||||||
|
BaseEditPrice, BaseDeletePrice
|
||||||
|
|
||||||
|
|
||||||
|
def create_create_action(type_name, **kwargs):
|
||||||
|
return type(type_name, (BaseCreatePrice,), kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_filter_action(type_name, **kwargs):
|
||||||
|
return type(type_name, (tables.FilterAction,), kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_edit_action(type_name, **kwargs):
|
||||||
|
return type(type_name, (BaseEditPrice,), kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_delete_action(type_name, **kwargs):
|
||||||
|
return type(type_name, (BaseDeletePrice,), kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorPriceTable(BasePriceTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Flavor Name'))
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "flavor_price"
|
||||||
|
verbose_name = _("Flavor Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="FlavorPriceFilter",
|
||||||
|
name="flavor_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="FlavorPriceCreate",
|
||||||
|
name="flavor_price_create",
|
||||||
|
verbose_name=_("Create Flavor Price"),
|
||||||
|
url="horizon:admin:price_configuration:flavor_price_create"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="FlavorPriceUpdate",
|
||||||
|
name="flavor_price_edit",
|
||||||
|
verbose_name=_("Edit Flavor Price"),
|
||||||
|
url="horizon:admin:price_configuration:flavor_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="FlavorPriceDelete",
|
||||||
|
use_case=FlavorPriceUseCase(),
|
||||||
|
single_action_label="Flavor Price",
|
||||||
|
plural_action_label="Flavor Prices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePriceTable(BasePriceTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Volume Type Name'))
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "volume_price"
|
||||||
|
verbose_name = _("Volume Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="VolumePriceFilter",
|
||||||
|
name="volume_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="VolumePriceCreate",
|
||||||
|
name="create_volume_price",
|
||||||
|
verbose_name=_("Create Volume Price"),
|
||||||
|
url="horizon:admin:price_configuration:volume_price_create"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="VolumePriceEdit",
|
||||||
|
name="update_volume_price",
|
||||||
|
verbose_name=_("Edit Volume Price"),
|
||||||
|
url="horizon:admin:price_configuration:volume_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="VolumePriceDelete",
|
||||||
|
use_case=VolumePriceUseCase(),
|
||||||
|
single_action_label="Volume Price",
|
||||||
|
plural_action_label="Volume Prices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpPriceTable(BasePriceTable):
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "floating_ip_price"
|
||||||
|
verbose_name = _("Floating IP Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="FloatingIpPriceFilter",
|
||||||
|
name="floating_ip_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="FloatingIpPriceCreate",
|
||||||
|
name="create_floating_ip_price",
|
||||||
|
verbose_name=_("Create Floating IP Price"),
|
||||||
|
url="horizon:admin:price_configuration:floating_ip_price_create",
|
||||||
|
single_data=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="FloatingIpPriceEdit",
|
||||||
|
name="update_floating_ip_price",
|
||||||
|
verbose_name=_("Edit Floating IP Price"),
|
||||||
|
url="horizon:admin:price_configuration:floating_ip_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="FloatingIpPriceDelete",
|
||||||
|
use_case=FloatingIpPriceUseCase(),
|
||||||
|
single_action_label="Floating IP Price",
|
||||||
|
plural_action_label="Floating IP Prices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPriceTable(BasePriceTable):
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "router_price"
|
||||||
|
verbose_name = _("Router Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="RouterPriceFilter",
|
||||||
|
name="router_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="RouterPriceCreate",
|
||||||
|
name="create_router_price",
|
||||||
|
verbose_name=_("Create Router Price"),
|
||||||
|
url="horizon:admin:price_configuration:router_price_create",
|
||||||
|
single_data=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="RouterPriceEdit",
|
||||||
|
name="update_router_price",
|
||||||
|
verbose_name=_("Edit Router Price"),
|
||||||
|
url="horizon:admin:price_configuration:router_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="RouterPriceDelete",
|
||||||
|
use_case=RouterPriceUseCase(),
|
||||||
|
single_action_label="Router Price",
|
||||||
|
plural_action_label="Router Prices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotPriceTable(BasePriceTable):
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "snapshot_price"
|
||||||
|
verbose_name = _("Snapshot Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="SnapshotFilter",
|
||||||
|
name="snapshot_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="SnapshotCreate",
|
||||||
|
name="create_snapshot_price",
|
||||||
|
verbose_name=_("Create Snapshot Price"),
|
||||||
|
url="horizon:admin:price_configuration:snapshot_price_create",
|
||||||
|
single_data=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="SnapshotEdit",
|
||||||
|
name="update_snapshot_price",
|
||||||
|
verbose_name=_("Edit Snapshot Price"),
|
||||||
|
url="horizon:admin:price_configuration:snapshot_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="SnapshotDelete",
|
||||||
|
use_case=SnapshotPriceUseCase(),
|
||||||
|
single_action_label="Snapshot Price",
|
||||||
|
plural_action_label="Snapshot Prices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePriceTable(BasePriceTable):
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "image_price"
|
||||||
|
verbose_name = _("Image Price")
|
||||||
|
table_actions = (
|
||||||
|
create_filter_action(
|
||||||
|
type_name="ImageFilter",
|
||||||
|
name="image_price_filter"
|
||||||
|
),
|
||||||
|
create_create_action(
|
||||||
|
type_name="ImageCreate",
|
||||||
|
name="create_image_price",
|
||||||
|
verbose_name=_("Create Image Price"),
|
||||||
|
url="horizon:admin:price_configuration:image_price_create",
|
||||||
|
single_data=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row_actions = (
|
||||||
|
create_edit_action(
|
||||||
|
type_name="ImageEdit",
|
||||||
|
name="update_image_price",
|
||||||
|
verbose_name=_("Edit Image Price"),
|
||||||
|
url="horizon:admin:price_configuration:image_price_update"
|
||||||
|
),
|
||||||
|
create_delete_action(
|
||||||
|
type_name="ImageDelete",
|
||||||
|
use_case=ImagePriceUseCase(),
|
||||||
|
single_action_label="Image Price",
|
||||||
|
plural_action_label="Image Prices"
|
||||||
|
)
|
||||||
|
)
|
130
yuyu/admin/price_configuration/tabs.py
Normal file
130
yuyu/admin/price_configuration/tabs.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
from horizon import exceptions, tables, tabs
|
||||||
|
from openstack_dashboard.dashboards.yuyu.admin.price_configuration.tables import FlavorPriceTable, VolumePriceTable, \
|
||||||
|
FloatingIpPriceTable, RouterPriceTable, SnapshotPriceTable, ImagePriceTable
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorTab(tabs.TableTab):
|
||||||
|
table_classes = (FlavorPriceTable,)
|
||||||
|
name = _("Flavor")
|
||||||
|
slug = "flavor"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
flavor_price_uc = FlavorPriceUseCase()
|
||||||
|
|
||||||
|
def get_flavor_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.flavor_price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get flavor prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeTab(tabs.TableTab):
|
||||||
|
table_classes = (VolumePriceTable,)
|
||||||
|
name = _("Volume")
|
||||||
|
slug = "volume"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
volume_price_uc = VolumePriceUseCase()
|
||||||
|
|
||||||
|
def get_volume_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.volume_price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get volume prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpTab(tabs.TableTab):
|
||||||
|
table_classes = (FloatingIpPriceTable,)
|
||||||
|
name = _("Floating IP")
|
||||||
|
slug = "floating_ip"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
fip_price_uc = FloatingIpPriceUseCase()
|
||||||
|
|
||||||
|
def get_floating_ip_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.fip_price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get floating IP prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class RouterTab(tabs.TableTab):
|
||||||
|
table_classes = (RouterPriceTable,)
|
||||||
|
name = _("Router")
|
||||||
|
slug = "router"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
price_uc = RouterPriceUseCase()
|
||||||
|
|
||||||
|
def get_router_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get router prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotTab(tabs.TableTab):
|
||||||
|
table_classes = (SnapshotPriceTable,)
|
||||||
|
name = _("Snapshot")
|
||||||
|
slug = "snapshot"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
price_uc = SnapshotPriceUseCase()
|
||||||
|
|
||||||
|
def get_snapshot_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get snapshot prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
class ImageTab(tabs.TableTab):
|
||||||
|
table_classes = (ImagePriceTable,)
|
||||||
|
name = _("Image")
|
||||||
|
slug = "image"
|
||||||
|
template_name = 'horizon/common/_detail_table.html'
|
||||||
|
|
||||||
|
price_uc = ImagePriceUseCase()
|
||||||
|
|
||||||
|
def get_image_price_data(self):
|
||||||
|
try:
|
||||||
|
data = self.price_uc.list(self.request)
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get image prices')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class PriceConfigurationTabs(tabs.TabGroup):
|
||||||
|
slug = "price_config"
|
||||||
|
tabs = (FlavorTab, VolumeTab, FloatingIpTab, RouterTab, SnapshotTab, ImageTab, )
|
||||||
|
sticky = True
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a flavor price.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a floating IP price for each allocation.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a image price for each 1GB of space.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a router price for each allocation that have external network.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a snapshot price for each 1GB of space.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Create/update a volume price for each 1GB of space.' %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Flavor Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_flavor.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Floating IP Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_floating_ip.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Image Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_image.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Router Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_router.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Snapshot Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_snapshot.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Create/Update Volume Price" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/_create_volume.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Price Configuration" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/price_configuration/missing_prices.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
{{ tab_group.render }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,70 @@
|
||||||
|
{% if missing_price.flavor %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Flavor price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if missing_price.volume %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Volume price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_price.fip %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Floating IP price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_price.router %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Router IP price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_price.snapshot %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Snapshot price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_price.image %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
Image price not complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
35
yuyu/admin/price_configuration/urls.py
Normal file
35
yuyu/admin/price_configuration/urls.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# 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 . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^flavor_price/create/$', views.FlavorPriceAddFormView.as_view(), name='flavor_price_create'),
|
||||||
|
url(r'^flavor_price/update/(?P<id>[^/]+)/$', views.FlavorPriceUpdateFormView.as_view(), name='flavor_price_update'),
|
||||||
|
url(r'^volume_price/create/$', views.VolumePriceAddFormView.as_view(), name='volume_price_create'),
|
||||||
|
url(r'^volume_price/update/(?P<id>[^/]+)/$', views.VolumePriceUpdateFormView.as_view(), name='volume_price_update'),
|
||||||
|
url(r'^floating_ip_price/create/$', views.FloatingIpPriceAddFormView.as_view(), name='floating_ip_price_create'),
|
||||||
|
url(r'^floating_ip_price/update/(?P<id>[^/]+)/$', views.FloatingIpPriceUpdateFormView.as_view(),
|
||||||
|
name='floating_ip_price_update'),
|
||||||
|
url(r'^router_price/create/$', views.RouterPriceAddFormView.as_view(), name='router_price_create'),
|
||||||
|
url(r'^router_price/update/(?P<id>[^/]+)/$', views.RouterPriceUpdateFormView.as_view(),
|
||||||
|
name='router_price_update'),
|
||||||
|
url(r'^snapshot_price/create/$', views.SnapshotPriceAddFormView.as_view(), name='snapshot_price_create'),
|
||||||
|
url(r'^snapshot_price/update/(?P<id>[^/]+)/$', views.SnapshotPriceUpdateFormView.as_view(),
|
||||||
|
name='snapshot_price_update'),
|
||||||
|
url(r'^image_price/create/$', views.ImagePriceAddFormView.as_view(), name='image_price_create'),
|
||||||
|
url(r'^image_price/update/(?P<id>[^/]+)/$', views.ImagePriceUpdateFormView.as_view(),
|
||||||
|
name='image_price_update'),
|
||||||
|
]
|
378
yuyu/admin/price_configuration/views.py
Normal file
378
yuyu/admin/price_configuration/views.py
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
# 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.urls import reverse_lazy, reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from neutronclient.common import exceptions as neutron_exc
|
||||||
|
|
||||||
|
from horizon import exceptions, tabs
|
||||||
|
from horizon import forms
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.snapshot_price_use_case import SnapshotPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
|
||||||
|
from .forms import FlavorPriceForm, VolumePriceForm, FloatingIpPriceForm, RouterPriceForm, SnapshotPriceForm, ImagePriceForm
|
||||||
|
from .tabs import PriceConfigurationTabs
|
||||||
|
from ...core.utils.price_checker import has_missing_price
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tabs.TabbedTableView):
|
||||||
|
|
||||||
|
tab_group_class = PriceConfigurationTabs
|
||||||
|
page_title = _("Price Configuration")
|
||||||
|
template_name = "admin/price_configuration/index.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(IndexView, self).get_context_data(**kwargs)
|
||||||
|
context['missing_price'] = has_missing_price(self.request)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorPriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = FlavorPriceForm
|
||||||
|
form_id = "flavor_price_form"
|
||||||
|
page_title = _("Create Flavor Price")
|
||||||
|
submit_label = _("Create Flavor Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:flavor_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_flavor.html'
|
||||||
|
|
||||||
|
flavor_price_uc = FlavorPriceUseCase()
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.flavor_id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
added_ids = []
|
||||||
|
flavors = []
|
||||||
|
try:
|
||||||
|
added_ids = map(lambda x: x['flavor_id'], self.flavor_price_uc.list(self.request))
|
||||||
|
flavors = api.nova.flavor_list(self.request)
|
||||||
|
except neutron_exc.ConnectionFailed:
|
||||||
|
exceptions.handle(self.request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve flavors."))
|
||||||
|
|
||||||
|
flavor_list = []
|
||||||
|
for flavor in flavors:
|
||||||
|
if flavor.id not in added_ids:
|
||||||
|
flavor_list.append((flavor.id, flavor.name))
|
||||||
|
|
||||||
|
if not flavor_list:
|
||||||
|
flavor_list = [(None, _("No flavors available"))]
|
||||||
|
return {'flavor_list': flavor_list}
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorPriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = FlavorPriceForm
|
||||||
|
form_id = "flavor_price_form_update"
|
||||||
|
page_title = _("Flavor Price")
|
||||||
|
submit_label = _("Update Flavor Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:flavor_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_flavor.html'
|
||||||
|
|
||||||
|
flavor_price_uc = FlavorPriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.flavor_id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
flavor_price = self.flavor_price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
flavor_price = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve flavor price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': flavor_price['id'],
|
||||||
|
'flavor_list': [(flavor_price['flavor_id'], flavor_price['name'])],
|
||||||
|
'hourly_price': flavor_price['hourly_price'],
|
||||||
|
'monthly_price': flavor_price['monthly_price']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = VolumePriceForm
|
||||||
|
form_id = "volume_price_form"
|
||||||
|
page_title = _("Create Volume Price")
|
||||||
|
submit_label = _("Create Volume Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:volume_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_volume.html'
|
||||||
|
|
||||||
|
volume_price_uc = VolumePriceUseCase()
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.volume_type_id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
added_ids = []
|
||||||
|
volumes = []
|
||||||
|
try:
|
||||||
|
added_ids = map(lambda x: x['volume_type_id'], self.volume_price_uc.list(self.request))
|
||||||
|
volumes = api.cinder.volume_type_list(self.request)
|
||||||
|
except neutron_exc.ConnectionFailed:
|
||||||
|
exceptions.handle(self.request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve volumes."))
|
||||||
|
|
||||||
|
volume_list = []
|
||||||
|
for d in volumes:
|
||||||
|
if d.id not in added_ids:
|
||||||
|
volume_list.append((d.id, d.name))
|
||||||
|
|
||||||
|
if not volume_list:
|
||||||
|
volume_list = [(None, _("No volume type available"))]
|
||||||
|
return {'volume_type_list': volume_list}
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = VolumePriceForm
|
||||||
|
form_id = "volume_price_form_update"
|
||||||
|
page_title = _("Volume Price")
|
||||||
|
submit_label = _("Update Volume Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:volume_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_volume.html'
|
||||||
|
|
||||||
|
volume_price_uc = VolumePriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.volume_type_id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
volume = self.volume_price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve volume price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': volume['id'],
|
||||||
|
'volume_type_list': [(volume['volume_type_id'], volume['name'])],
|
||||||
|
'hourly_price': volume['hourly_price'],
|
||||||
|
'monthly_price': volume['monthly_price']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpPriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = FloatingIpPriceForm
|
||||||
|
form_id = "floating_ip_price_form"
|
||||||
|
page_title = _("Create Floating IP Price")
|
||||||
|
submit_label = _("Create Floating IP Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:floating_ip_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_floating_ip.html'
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpPriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = FloatingIpPriceForm
|
||||||
|
form_id = "floating_ip_price_form_update"
|
||||||
|
page_title = _("Floating IP Price")
|
||||||
|
submit_label = _("Update Floating IP Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:floating_ip_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_floating_ip.html'
|
||||||
|
|
||||||
|
fip_price_uc = FloatingIpPriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
volume = self.fip_price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve floating ip price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': volume['id'],
|
||||||
|
'hourly_price': volume['hourly_price'],
|
||||||
|
'monthly_price': volume['monthly_price']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = RouterPriceForm
|
||||||
|
form_id = "router_price_form"
|
||||||
|
page_title = _("Create Router Price")
|
||||||
|
submit_label = _("Create Router Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:router_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_router.html'
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = RouterPriceForm
|
||||||
|
form_id = "router_price_form_update"
|
||||||
|
page_title = _("Router Price")
|
||||||
|
submit_label = _("Update Router Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:router_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_router.html'
|
||||||
|
|
||||||
|
price_uc = RouterPriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
volume = self.price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve router price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': volume['id'],
|
||||||
|
'hourly_price': volume['hourly_price'],
|
||||||
|
'monthly_price': volume['monthly_price']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotPriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = SnapshotPriceForm
|
||||||
|
form_id = "snapshot_form"
|
||||||
|
page_title = _("Create Snapshot Price")
|
||||||
|
submit_label = _("Create Snapshot Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:snapshot_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_snapshot.html'
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotPriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = SnapshotPriceForm
|
||||||
|
form_id = "snapshot_form_update"
|
||||||
|
page_title = _("Snapshot Price")
|
||||||
|
submit_label = _("Update Snapshot Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:snapshot_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_snapshot.html'
|
||||||
|
|
||||||
|
price_uc = SnapshotPriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
volume = self.price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve snapshot price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': volume['id'],
|
||||||
|
'hourly_price': volume['hourly_price'],
|
||||||
|
'monthly_price': volume['monthly_price']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePriceAddFormView(forms.ModalFormView):
|
||||||
|
form_class = ImagePriceForm
|
||||||
|
form_id = "snapshot_form"
|
||||||
|
page_title = _("Create Image Price")
|
||||||
|
submit_label = _("Create Image Price")
|
||||||
|
submit_url = reverse_lazy("horizon:admin:price_configuration:image_price_create")
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_image.html'
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePriceUpdateFormView(forms.ModalFormView):
|
||||||
|
form_class = ImagePriceForm
|
||||||
|
form_id = "image_form_update"
|
||||||
|
page_title = _("Image Price")
|
||||||
|
submit_label = _("Update Image Price")
|
||||||
|
submit_url = "horizon:admin:price_configuration:image_price_update"
|
||||||
|
success_url = reverse_lazy("horizon:admin:price_configuration:index")
|
||||||
|
template_name = 'admin/price_configuration/create_image.html'
|
||||||
|
|
||||||
|
price_uc = ImagePriceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
args = (self.kwargs['id'],)
|
||||||
|
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_object_display(self, obj):
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
try:
|
||||||
|
volume = self.price_uc.get(self.request, self.kwargs['id'])
|
||||||
|
except Exception:
|
||||||
|
volume = None
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve image price."))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'model_id': volume['id'],
|
||||||
|
'hourly_price': volume['hourly_price'],
|
||||||
|
'monthly_price': volume['monthly_price']
|
||||||
|
}
|
0
yuyu/admin/projects_invoice/__init__.py
Normal file
0
yuyu/admin/projects_invoice/__init__.py
Normal file
20
yuyu/admin/projects_invoice/panel.py
Normal file
20
yuyu/admin/projects_invoice/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 ProjectsInvoice(horizon.Panel):
|
||||||
|
name = _("Projects Invoice")
|
||||||
|
slug = "projects_invoice"
|
44
yuyu/admin/projects_invoice/tables.py
Normal file
44
yuyu/admin/projects_invoice/tables.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceAction(tables.LinkAction):
|
||||||
|
name = "invoice"
|
||||||
|
verbose_name = "Invoice"
|
||||||
|
|
||||||
|
def get_link_url(self, datum=None, *args, **kwargs):
|
||||||
|
return reverse("horizon:admin:projects_invoice:download_pdf", kwargs={
|
||||||
|
"id": datum['id'],
|
||||||
|
"project_id": datum['project_id'],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class UsageCostAction(tables.LinkAction):
|
||||||
|
name = "usage_cost"
|
||||||
|
verbose_name = "Usage Cost"
|
||||||
|
|
||||||
|
def get_link_url(self, datum=None, *args, **kwargs):
|
||||||
|
return reverse("horizon:admin:projects_invoice:usage_cost", kwargs={
|
||||||
|
"id": datum['id'],
|
||||||
|
"project_id": datum['project_id'],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceTable(tables.DataTable):
|
||||||
|
date = tables.WrappingColumn('date', verbose_name=_('Date'))
|
||||||
|
state = tables.WrappingColumn('state', verbose_name=_('State'))
|
||||||
|
total = tables.Column('total', verbose_name=_('Total'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['date']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "invoice_table"
|
||||||
|
hidden_title = True
|
||||||
|
verbose_name = _("Invoice")
|
||||||
|
row_actions = (InvoiceAction, UsageCostAction)
|
|
@ -0,0 +1,66 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% 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 %}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<div>
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Invoice Month</dt>
|
||||||
|
<dd>{{ invoice.start_date|date:"M Y" }}</dd>
|
||||||
|
<dt>Invoice State</dt>
|
||||||
|
<dd>{{ invoice.state_text }}</dd>
|
||||||
|
<dt>Subtotal</dt>
|
||||||
|
<dd>{{ invoice.subtotal_money }}</dd>
|
||||||
|
{% if invoice.state != 1 %}
|
||||||
|
<dt>Tax</dt>
|
||||||
|
<dd>{{ invoice.tax_money }}</dd>
|
||||||
|
<dt>Total</dt>
|
||||||
|
<dd>{{ invoice.total_money }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="instance-cost">
|
||||||
|
{{ instance_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="volume-cost">
|
||||||
|
{{ volume_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="floating-ip-cost">
|
||||||
|
{{ floating_ip_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="router-cost">
|
||||||
|
{{ router_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="snapshot-cost">
|
||||||
|
{{ snapshot_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="image-cost">
|
||||||
|
{{ image_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h1>Billing not enabled or you don't have any usage yet</h1> <br/>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function onInvoiceChange(val) {
|
||||||
|
var search = "?invoice_id=" + val;
|
||||||
|
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,123 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<button onclick="javascript:downloadPdf();" class="btn btn-default">Download PDF</button>
|
||||||
|
{% 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 %}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="invoice">
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Invoice Month</dt>
|
||||||
|
<dd>
|
||||||
|
<h3>{{ invoice.start_date|date:"M Y" }}</h3>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<h5>Invoice State: {{ invoice.state_text }}</h5>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Total Cost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Instance</td>
|
||||||
|
<td>{{ instance_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Volume</td>
|
||||||
|
<td>{{ volume_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Floating IP</td>
|
||||||
|
<td>{{ fip_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Router</td>
|
||||||
|
<td>{{ router_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Snapshot</td>
|
||||||
|
<td>{{ snapshot_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Image</td>
|
||||||
|
<td>{{ image_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Subtotal:</b> {{ invoice.subtotal_money }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if invoice.state != 1 %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Tax:</b> {{ invoice.tax_money }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Total:</b> {{ invoice.total_money }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function downloadPdf() {
|
||||||
|
var pdf = new jsPDF('p', 'pt', 'letter');
|
||||||
|
// source can be HTML-formatted string, or a reference
|
||||||
|
// to an actual DOM element from which the text will be scraped.
|
||||||
|
source = $('#invoice')[0];
|
||||||
|
|
||||||
|
// we support special element handlers. Register them with jQuery-style
|
||||||
|
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
|
||||||
|
// There is no support for any other type of selectors
|
||||||
|
// (class, of compound) at this time.
|
||||||
|
specialElementHandlers = {
|
||||||
|
// element with id of "bypass" - jQuery style selector
|
||||||
|
'#bypassme': function (element, renderer) {
|
||||||
|
// true = "handled elsewhere, bypass text extraction"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
margins = {
|
||||||
|
top: 80,
|
||||||
|
bottom: 60,
|
||||||
|
left: 40,
|
||||||
|
width: 522
|
||||||
|
};
|
||||||
|
// all coords and widths are in jsPDF instance's declared units
|
||||||
|
// 'inches' in this case
|
||||||
|
pdf.fromHTML(
|
||||||
|
source, // HTML string or DOM elem ref.
|
||||||
|
margins.left, // x coord
|
||||||
|
margins.top, { // y coord
|
||||||
|
'width': margins.width, // max width of content on PDF
|
||||||
|
'elementHandlers': specialElementHandlers
|
||||||
|
},
|
||||||
|
|
||||||
|
function (dispose) {
|
||||||
|
// dispose: object with X, Y of the last line add to the PDF
|
||||||
|
// this allow the insertion of new lines after html
|
||||||
|
pdf.save('invoice.pdf');
|
||||||
|
}, margins);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% load horizon %}
|
||||||
|
{% jstemplate %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Price_Volume_Panel" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endjstemplate %}
|
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div>
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Project: </dt>
|
||||||
|
<dd>
|
||||||
|
<select id="project_select" onchange="onProjectSelect(this.value)">
|
||||||
|
{% for i in project_list %}
|
||||||
|
<option
|
||||||
|
value='{"project_id": "{{ i.id }}", "project_name": "{{ i.name }}"}'
|
||||||
|
{% if i.id == current_project_id %}selected {% endif %}
|
||||||
|
>{{ i.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{{ table.render }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function onProjectSelect(val) {
|
||||||
|
console.log(val)
|
||||||
|
const data = JSON.parse(val);
|
||||||
|
var search = "?project_id=" + data.project_id + "&project_name=" + data.project_name;
|
||||||
|
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
24
yuyu/admin/projects_invoice/urls.py
Normal file
24
yuyu/admin/projects_invoice/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_invoice import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^invoice/pdf/(?P<project_id>[^/]+)/(?P<id>[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'),
|
||||||
|
url(r'^invoice/usage/(?P<project_id>[^/]+)/(?P<id>[^/]+)/$', views.UsageCostView.as_view(), name='usage_cost'),
|
||||||
|
url(r'^invoice/finish/(?P<id>[^/]+)/$', views.FinishInvoice.as_view(), name='finish_invoice'),
|
||||||
|
url(r'^invoice/rollback_to_unpaid/(?P<id>[^/]+)/$', views.RollbackToUnpaidInvoice.as_view(),
|
||||||
|
name='rollback_to_unpaid'),
|
||||||
|
]
|
252
yuyu/admin/projects_invoice/views.py
Normal file
252
yuyu/admin/projects_invoice/views.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
# 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 datetime
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
from dateutil.tz import tzutc
|
||||||
|
from django import shortcuts
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timesince import timesince
|
||||||
|
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 horizon import views
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
|
||||||
|
from .tables import InvoiceTable
|
||||||
|
from ...core.usage_cost.tables import InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, \
|
||||||
|
SnapshotCostTable, ImageCostTable
|
||||||
|
from ...core.utils.invoice_utils import state_to_text
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tables.DataTableView):
|
||||||
|
table_class = InvoiceTable
|
||||||
|
page_title = _("Invoice")
|
||||||
|
template_name = "admin/projects_invoice/invoice_table.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
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['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)
|
||||||
|
print(context['project_list'])
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
project_id = self.request.GET.get('project_id', self.request.user.project_id)
|
||||||
|
try:
|
||||||
|
data = []
|
||||||
|
for d in self.invoice_uc.get_simple_list(self.request, project_id):
|
||||||
|
data.append({
|
||||||
|
'id': d['id'],
|
||||||
|
'project_id': project_id,
|
||||||
|
'date': formats.date_format(d['start_date'], 'M Y'),
|
||||||
|
'state': state_to_text(d['state']),
|
||||||
|
'total': d['total_money'] or d['subtotal_money']
|
||||||
|
})
|
||||||
|
return list(data)
|
||||||
|
except Exception as e:
|
||||||
|
error_message = _('Unable to get invoice')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceView(views.APIView):
|
||||||
|
page_title = _("Invoice")
|
||||||
|
template_name = "admin/projects_invoice/download_pdf.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
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'])
|
||||||
|
context['invoice'] = invoice
|
||||||
|
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')
|
||||||
|
context['router_cost'] = self.get_sum_price(invoice, 'routers')
|
||||||
|
context['snapshot_cost'] = self.get_sum_price(invoice, 'snapshots')
|
||||||
|
context['image_cost'] = self.get_sum_price(invoice, 'images')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_sum_price(self, invoice, key):
|
||||||
|
instance_prices = map(
|
||||||
|
lambda x: Money(amount=x['price_charged'], currency=x['price_charged_currency']),
|
||||||
|
invoice.get(key, [])
|
||||||
|
)
|
||||||
|
|
||||||
|
return sum(instance_prices)
|
||||||
|
|
||||||
|
|
||||||
|
class UsageCostView(tables.MultiTableView):
|
||||||
|
table_classes = (
|
||||||
|
InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable)
|
||||||
|
page_title = _("Usage Cost")
|
||||||
|
template_name = "admin/projects_invoice/cost_tables.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
request.invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'],
|
||||||
|
tenant_id=self.kwargs['project_id'])
|
||||||
|
return super(UsageCostView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['invoice'] = self.request.invoice
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_instance_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
"flavor": api.nova.flavor_get(self.request, x['flavor_id']).name,
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('instances', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get instance cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_volume_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['volume_name'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
'type': api.cinder.volume_type_get(self.request, x['volume_type_id']).name,
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('volumes', []))
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get volume cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_floating_ip_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['ip'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('floating_ips', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get floating ip cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_router_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('routers', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get router cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_snapshot_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('snapshots', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get snapshot cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_image_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('images', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get images cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class FinishInvoice(views.APIView):
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.invoice_uc.finish_invoice(request, kwargs['id'])
|
||||||
|
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
|
||||||
|
return HttpResponseRedirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
class RollbackToUnpaidInvoice(views.APIView):
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.invoice_uc.rollback_to_unpaid_invoice(request, kwargs['id'])
|
||||||
|
next_url = request.GET.get('next', reverse('horizon:admin:projects_invoice:index'))
|
||||||
|
return HttpResponseRedirect(next_url)
|
0
yuyu/cases/__init__.py
Normal file
0
yuyu/cases/__init__.py
Normal file
19
yuyu/cases/admin_overview_use_case.py
Normal file
19
yuyu/cases/admin_overview_use_case.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
|
||||||
|
|
||||||
|
|
||||||
|
class AdminOverviewUseCase:
|
||||||
|
def total_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"admin_overview/total_resource/")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def active_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"admin_overview/active_resource/")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def price_total_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"admin_overview/price_total_resource/")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def price_active_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"admin_overview/price_active_resource/")
|
||||||
|
return response.json()
|
38
yuyu/cases/flavor_price_use_case.py
Normal file
38
yuyu/cases/flavor_price_use_case.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorPriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "flavor"
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
data = list(super().list(request))
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
try:
|
||||||
|
flavor = api.nova.flavor_get(request, d['flavor_id'])
|
||||||
|
d["name"] = flavor.name
|
||||||
|
except Exception:
|
||||||
|
d["name"] = 'Invalid Flavor'
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get(self, request, id):
|
||||||
|
data = super().get(request, id)
|
||||||
|
try:
|
||||||
|
flavor = api.nova.flavor_get(request, d['flavor_id'])
|
||||||
|
data["name"] = flavor.name
|
||||||
|
except Exception:
|
||||||
|
data["name"] = 'Invalid Flavor'
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def has_missing_price(self, request):
|
||||||
|
server_data_prices = list(map(lambda d: d['flavor_id'], super().list(request)))
|
||||||
|
flavor_list = api.nova.flavor_list(request)
|
||||||
|
|
||||||
|
for f in flavor_list:
|
||||||
|
if f.id not in server_data_prices:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
5
yuyu/cases/floating_ip_price_use_case.py
Normal file
5
yuyu/cases/floating_ip_price_use_case.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpPriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "floating_ip"
|
5
yuyu/cases/image_price_use_case.py
Normal file
5
yuyu/cases/image_price_use_case.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "image"
|
122
yuyu/cases/invoice_use_case.py
Normal file
122
yuyu/cases/invoice_use_case.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import dateutil.parser
|
||||||
|
import requests
|
||||||
|
from djmoney.money import Money
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core.utils.invoice_utils import state_to_text
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceUseCase:
|
||||||
|
def get_simple_list(self, request, tenant_id=None):
|
||||||
|
if not tenant_id:
|
||||||
|
tenant_id = request.user.project_id
|
||||||
|
response = yuyu_client.get(request, f"invoice/simple_list/?tenant_id={tenant_id}")
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
for d in data:
|
||||||
|
zero_money = Money(amount=0, currency=d['subtotal_currency'])
|
||||||
|
d['start_date'] = dateutil.parser.isoparse(d['start_date'])
|
||||||
|
d['subtotal_money'] = Money(amount=d['subtotal'], currency=d['subtotal_currency'])
|
||||||
|
d['total_money'] = Money(amount=d['total'], currency=d['total_currency']) if d['total'] else zero_money
|
||||||
|
d['state_text'] = state_to_text(d['state'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_invoice(self, request, id, tenant_id=None):
|
||||||
|
if not tenant_id:
|
||||||
|
tenant_id = request.user.project_id
|
||||||
|
|
||||||
|
response = yuyu_client.get(request, f"invoice/{id}/?tenant_id={tenant_id}")
|
||||||
|
data = response.json()
|
||||||
|
data['subtotal_money'] = Money(amount=data['subtotal'], currency=data['subtotal_currency'])
|
||||||
|
|
||||||
|
zero_money = Money(amount=0, currency=data['subtotal_currency'])
|
||||||
|
|
||||||
|
data['tax_money'] = Money(amount=data['tax'], currency=data['tax_currency']) if data['tax'] else zero_money
|
||||||
|
data['total_money'] = Money(amount=data['total'], currency=data['total_currency']) if data[
|
||||||
|
'total'] else zero_money
|
||||||
|
|
||||||
|
data['start_date'] = dateutil.parser.isoparse(data['start_date'])
|
||||||
|
data['state_text'] = state_to_text(data['state'])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def enable_billing(self, request):
|
||||||
|
volume_type_list = api.cinder.volume_type_list(request)
|
||||||
|
volume_type_name_to_id = {
|
||||||
|
v.name: v.id
|
||||||
|
for v in volume_type_list
|
||||||
|
}
|
||||||
|
|
||||||
|
router_with_ext = filter(lambda r: bool(r.external_gateway_info), api.neutron.router_list(request))
|
||||||
|
instances = api.nova.server_list(request, search_opts={'all_tenants': True})[0]
|
||||||
|
volumes = api.cinder.volume_list(request, search_opts={"all_tenants": 1})
|
||||||
|
floating_ips = api.neutron.tenant_floating_ip_list(request, all_tenants=True)
|
||||||
|
snapshots = api.cinder.volume_snapshot_list(request, search_opts={'all_tenants': True})
|
||||||
|
images = api.glance.image_list_detailed(request, filters={'is_public': None})[0]
|
||||||
|
|
||||||
|
# Note!: When initializing router, we don't know when external network was added to router
|
||||||
|
payload = {
|
||||||
|
"instances": list(map(lambda s: {
|
||||||
|
"tenant_id": s.tenant_id,
|
||||||
|
"instance_id": s.id,
|
||||||
|
"name": s.name,
|
||||||
|
"flavor_id": s.flavor['id'],
|
||||||
|
"start_date": s.created
|
||||||
|
}, instances)),
|
||||||
|
"volumes": list(map(lambda v: {
|
||||||
|
"tenant_id": v.tenant_id,
|
||||||
|
"volume_id": v.id,
|
||||||
|
"volume_name": v.name,
|
||||||
|
"volume_type_id": volume_type_name_to_id[v.volume_type],
|
||||||
|
"space_allocation_gb": v.size,
|
||||||
|
"start_date": v.created_at
|
||||||
|
}, volumes)),
|
||||||
|
"floating_ips": list(map(lambda f: {
|
||||||
|
"tenant_id": f.tenant_id,
|
||||||
|
"fip_id": f.id,
|
||||||
|
"ip": f.ip,
|
||||||
|
"start_date": f.created_at
|
||||||
|
}, floating_ips)),
|
||||||
|
"routers": list(map(lambda r: {
|
||||||
|
"tenant_id": r.tenant_id,
|
||||||
|
"router_id": r.id,
|
||||||
|
"name": r.name,
|
||||||
|
"start_date": r.created_at
|
||||||
|
}, router_with_ext)),
|
||||||
|
"snapshots": list(map(lambda s: {
|
||||||
|
"tenant_id": s.project_id,
|
||||||
|
"snapshot_id": s.id,
|
||||||
|
"name": s.name,
|
||||||
|
"space_allocation_gb": s.size,
|
||||||
|
"start_date": s.created_at
|
||||||
|
}, snapshots)),
|
||||||
|
"images": list(map(lambda i: {
|
||||||
|
"tenant_id": i.owner,
|
||||||
|
"image_id": i.id,
|
||||||
|
"name": i.name,
|
||||||
|
"space_allocation_gb": i.size / 1024 / 1024 / 1024,
|
||||||
|
"start_date": i.created_at
|
||||||
|
}, images))
|
||||||
|
}
|
||||||
|
|
||||||
|
response = yuyu_client.post(request, f"invoice/enable_billing/", payload)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return True
|
||||||
|
raise Exception('Unable to enable billing')
|
||||||
|
|
||||||
|
def disable_billing(self, request):
|
||||||
|
yuyu_client.post(request, f"invoice/disable_billing/", {})
|
||||||
|
|
||||||
|
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/")
|
||||||
|
data = response.json()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def rollback_to_unpaid_invoice(self, request, id):
|
||||||
|
response = yuyu_client.get(request, f"invoice/{id}/rollback_to_unpaid/")
|
||||||
|
data = response.json()
|
||||||
|
return data
|
41
yuyu/cases/pricing_use_case.py
Normal file
41
yuyu/cases/pricing_use_case.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from djmoney.money import Money
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
|
||||||
|
|
||||||
|
|
||||||
|
class PricingUseCase:
|
||||||
|
pricing_name = ""
|
||||||
|
|
||||||
|
def clean_price_response(self, response_data):
|
||||||
|
response_data["hourly_price"] = Money(amount=response_data['hourly_price'],
|
||||||
|
currency=response_data['hourly_price_currency'])
|
||||||
|
if response_data['monthly_price'] and response_data['monthly_price_currency']:
|
||||||
|
response_data["monthly_price"] = Money(amount=response_data['monthly_price'],
|
||||||
|
currency=response_data['monthly_price_currency'])
|
||||||
|
else:
|
||||||
|
response_data["monthly_price"] = Money(currency=settings.DEFAULT_CURRENCY)
|
||||||
|
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def create(self, request, payload):
|
||||||
|
return yuyu_client.post(request, f"price/{self.pricing_name}/", payload).json()
|
||||||
|
|
||||||
|
def update(self, request, id, payload):
|
||||||
|
return yuyu_client.patch(request, f"price/{self.pricing_name}/{id}/", payload).json()
|
||||||
|
|
||||||
|
def delete(self, request, id):
|
||||||
|
return yuyu_client.delete(request, f"price/{self.pricing_name}/{id}/")
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
response = yuyu_client.get(request, f"price/{self.pricing_name}/")
|
||||||
|
data = list(map(lambda f: self.clean_price_response(f), response.json()))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get(self, request, id):
|
||||||
|
data = yuyu_client.get(request, f"price/{self.pricing_name}/{id}/").json()
|
||||||
|
data = self.clean_price_response(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def has_missing_price(self, request):
|
||||||
|
return len(self.list(request)) == 0
|
19
yuyu/cases/project_overview_use_case.py
Normal file
19
yuyu/cases/project_overview_use_case.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectOverviewUseCase:
|
||||||
|
def total_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"project_overview/total_resource/?tenant_id={request.user.tenant_id}")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def active_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"project_overview/active_resource/?tenant_id={request.user.tenant_id}")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def price_total_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"project_overview/price_total_resource/?tenant_id={request.user.tenant_id}")
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def price_active_resource(self, request):
|
||||||
|
response = yuyu_client.get(request, f"project_overview/price_active_resource/?tenant_id={request.user.tenant_id}")
|
||||||
|
return response.json()
|
5
yuyu/cases/router_price_use_case.py
Normal file
5
yuyu/cases/router_price_use_case.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class RouterPriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "router"
|
11
yuyu/cases/setting_use_case.py
Normal file
11
yuyu/cases/setting_use_case.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core import yuyu_client
|
||||||
|
|
||||||
|
|
||||||
|
class SettingUseCase:
|
||||||
|
def get_settings(self, request):
|
||||||
|
return yuyu_client.get(request, "settings/").json()
|
||||||
|
|
||||||
|
def set_setting(self, request, key, value):
|
||||||
|
return yuyu_client.patch(request, f"settings/{key}/", {
|
||||||
|
"value": value
|
||||||
|
}).json()
|
5
yuyu/cases/snapshot_price_use_case.py
Normal file
5
yuyu/cases/snapshot_price_use_case.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotPriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "snapshot"
|
29
yuyu/cases/volume_price_use_case.py
Normal file
29
yuyu/cases/volume_price_use_case.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePriceUseCase(PricingUseCase):
|
||||||
|
pricing_name = "volume"
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
data = list(super().list(request))
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
d["name"] = api.cinder.volume_type_get(request, d['volume_type_id']).name
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get(self, request, id):
|
||||||
|
data = super().get(request, id)
|
||||||
|
data["name"] = api.cinder.volume_type_get(request, data['volume_type_id']).name
|
||||||
|
return data
|
||||||
|
|
||||||
|
def has_missing_price(self, request):
|
||||||
|
server_data_prices = list(map(lambda d: d['volume_type_id'], super().list(request)))
|
||||||
|
volume_types = api.cinder.volume_type_list(request)
|
||||||
|
|
||||||
|
for f in volume_types:
|
||||||
|
if f.id not in server_data_prices:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
52
yuyu/core/pricing_admin/forms.py
Normal file
52
yuyu/core/pricing_admin/forms.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from djmoney.forms import MoneyField
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import messages
|
||||||
|
|
||||||
|
|
||||||
|
class BasePriceForm(forms.SelfHandlingForm):
|
||||||
|
hourly_price = MoneyField(label=_("Hourly Price"))
|
||||||
|
monthly_price = MoneyField(label=_("Monthly Price"), required=False)
|
||||||
|
|
||||||
|
USE_CASE: PricingUseCase = None
|
||||||
|
NAME = ""
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
self.model_id = kwargs.get('initial', {}).get('model_id', None)
|
||||||
|
|
||||||
|
def to_payload(self, data):
|
||||||
|
payload = {
|
||||||
|
"hourly_price": data['hourly_price'].amount,
|
||||||
|
"hourly_price_currency": data['hourly_price'].currency.code
|
||||||
|
}
|
||||||
|
|
||||||
|
if data['monthly_price']:
|
||||||
|
payload['monthly_price'] = data['monthly_price'].amount
|
||||||
|
payload['monthly_price_currency'] = data['monthly_price'].currency.code
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
if self.model_id:
|
||||||
|
result = self.USE_CASE.update(
|
||||||
|
request=request,
|
||||||
|
id=self.model_id,
|
||||||
|
payload=self.to_payload(data)
|
||||||
|
)
|
||||||
|
messages.success(request, _(f"Successfully update {self.NAME}"))
|
||||||
|
else:
|
||||||
|
result = self.USE_CASE.create(
|
||||||
|
request=request,
|
||||||
|
payload=self.to_payload(data)
|
||||||
|
)
|
||||||
|
messages.success(request, _(f"Successfully create {self.NAME}"))
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception:
|
||||||
|
mode_str = "update" if self.model_id else "create"
|
||||||
|
exceptions.handle(request, _(f'Unable to {mode_str} {self.NAME}.'))
|
60
yuyu/core/pricing_admin/tables.py
Normal file
60
yuyu/core/pricing_admin/tables.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _, ungettext_lazy
|
||||||
|
|
||||||
|
from horizon import tables, exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCreatePrice(tables.LinkAction):
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "link"
|
||||||
|
single_data = False
|
||||||
|
|
||||||
|
def allowed(self, request, datum):
|
||||||
|
if self.single_data and len(self.table.data) >= 1:
|
||||||
|
self.classes = [c for c in self.classes] + ['hidden']
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEditPrice(tables.LinkAction):
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "pencil"
|
||||||
|
|
||||||
|
def get_link_url(self, datum=None):
|
||||||
|
instance_id = self.table.get_object_id(datum)
|
||||||
|
return reverse(self.url, args=[instance_id])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDeletePrice(tables.DeleteAction):
|
||||||
|
use_case = None
|
||||||
|
single_action_label = None
|
||||||
|
plural_action_label = None
|
||||||
|
|
||||||
|
def action_present(self, count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
"Delete " + self.single_action_label,
|
||||||
|
"Delete " + self.plural_action_label,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_past(self, count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
"Deleted " + self.single_action_label,
|
||||||
|
"Deleted " + self.plural_action_label,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, request, obj_id):
|
||||||
|
try:
|
||||||
|
self.use_case.delete(request, obj_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Exception", e)
|
||||||
|
exceptions.handle(request, e)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePriceTable(tables.DataTable):
|
||||||
|
hourly_price = tables.Column('hourly_price', verbose_name=_('Hourly Price'))
|
||||||
|
monthly_price = tables.Column('monthly_price', verbose_name=_('Monthly Price'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
24
yuyu/core/pricing_admin/views.py
Normal file
24
yuyu/core/pricing_admin/views.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import tables
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.pricing_use_case import PricingUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class BasePriceIndexView(tables.DataTableView):
|
||||||
|
USE_CASE: PricingUseCase = None
|
||||||
|
|
||||||
|
def has_more_data(self, table):
|
||||||
|
return self._has_more
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
try:
|
||||||
|
datas = self.USE_CASE.list(self.request)
|
||||||
|
self._has_more = False # TODO: Pagination
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
self._has_more = False
|
||||||
|
error_message = _('Unable to get data')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
0
yuyu/core/usage_cost/__init__.py
Normal file
0
yuyu/core/usage_cost/__init__.py
Normal file
110
yuyu/core/usage_cost/tables.py
Normal file
110
yuyu/core/usage_cost/tables.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
flavor = tables.Column('flavor', verbose_name=_('Flavor'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "instance_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Instance")
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
type = tables.Column('type', verbose_name=_('Type'))
|
||||||
|
size = tables.Column('size', verbose_name=_('Size'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "volume_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Volume")
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIpCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "floating_ip_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Floating IP")
|
||||||
|
|
||||||
|
|
||||||
|
class RouterCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "router_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Router")
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
size = tables.Column('size', verbose_name=_('Size'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "snapshot_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Snapshot")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageCostTable(tables.DataTable):
|
||||||
|
name = tables.WrappingColumn('name', verbose_name=_('Name'))
|
||||||
|
size = tables.Column('size', verbose_name=_('Size'))
|
||||||
|
usage = tables.Column('usage', verbose_name=_('Usage'))
|
||||||
|
cost = tables.Column('cost', verbose_name=_('Cost'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['name']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "image_cost"
|
||||||
|
hidden_title = False
|
||||||
|
verbose_name = _("Image")
|
0
yuyu/core/utils/__init__.py
Normal file
0
yuyu/core/utils/__init__.py
Normal file
7
yuyu/core/utils/invoice_utils.py
Normal file
7
yuyu/core/utils/invoice_utils.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
def state_to_text(state):
|
||||||
|
if state == 1:
|
||||||
|
return 'In Progress'
|
||||||
|
if state == 2:
|
||||||
|
return 'Unpaid'
|
||||||
|
if state == 100:
|
||||||
|
return 'Finished'
|
28
yuyu/core/utils/price_checker.py
Normal file
28
yuyu/core/utils/price_checker.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.flavor_price_use_case import FlavorPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.floating_ip_price_use_case import FloatingIpPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.image_price_use_case import ImagePriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.router_price_use_case import RouterPriceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.volume_price_use_case import VolumePriceUseCase
|
||||||
|
|
||||||
|
|
||||||
|
def has_missing_price(request):
|
||||||
|
flavor_price_uc = FlavorPriceUseCase()
|
||||||
|
volume_price_uc = VolumePriceUseCase()
|
||||||
|
fip_price_uc = FloatingIpPriceUseCase()
|
||||||
|
router_price_uc = RouterPriceUseCase()
|
||||||
|
snapshot_price_uc = RouterPriceUseCase()
|
||||||
|
image_price_uc = ImagePriceUseCase()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'flavor': flavor_price_uc.has_missing_price(request),
|
||||||
|
'volume': volume_price_uc.has_missing_price(request),
|
||||||
|
'fip': fip_price_uc.has_missing_price(request),
|
||||||
|
'router': router_price_uc.has_missing_price(request),
|
||||||
|
'snapshot': snapshot_price_uc.has_missing_price(request),
|
||||||
|
'image': image_price_uc.has_missing_price(request),
|
||||||
|
}
|
||||||
|
|
||||||
|
context['has_missing'] = context['flavor'] or context['volume'] or context['fip'] or context['router'] or context[
|
||||||
|
'snapshot'] or context['image']
|
||||||
|
|
||||||
|
return context
|
26
yuyu/core/yuyu_client.py
Normal file
26
yuyu/core/yuyu_client.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _get_header(request):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _yuyu_url(request, path):
|
||||||
|
return settings.YUYU_URL + "/api/" + path
|
||||||
|
|
||||||
|
|
||||||
|
def get(request, path):
|
||||||
|
return requests.get(_yuyu_url(request, path), headers=_get_header(request))
|
||||||
|
|
||||||
|
|
||||||
|
def post(request, path, payload):
|
||||||
|
return requests.post(_yuyu_url(request, path), headers=_get_header(request), json=payload)
|
||||||
|
|
||||||
|
|
||||||
|
def patch(request, path, payload):
|
||||||
|
return requests.patch(_yuyu_url(request, path), headers=_get_header(request), json=payload)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(request, path):
|
||||||
|
return requests.delete(_yuyu_url(request, path), headers=_get_header(request))
|
4
yuyu/local/enabled/_6100_yuyu.py
Normal file
4
yuyu/local/enabled/_6100_yuyu.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
ADD_INSTALLED_APPS = [
|
||||||
|
'djmoney',
|
||||||
|
'openstack_dashboard.dashboards.yuyu',
|
||||||
|
]
|
8
yuyu/local/enabled/_6101_admin_billing_panel_group.py
Normal file
8
yuyu/local/enabled/_6101_admin_billing_panel_group.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL_GROUP = 'billing'
|
||||||
|
# The display name of the PANEL_GROUP. Required.
|
||||||
|
PANEL_GROUP_NAME = _('Billing')
|
||||||
|
# The slug of the dashboard the PANEL_GROUP associated with. Required.
|
||||||
|
PANEL_GROUP_DASHBOARD = 'admin'
|
9
yuyu/local/enabled/_6102_admin_billing_overview.py
Normal file
9
yuyu/local/enabled/_6102_admin_billing_overview.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'billing_overview'
|
||||||
|
# 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.billing_overview.panel.BillingOverview'
|
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'price_configuration'
|
||||||
|
# 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.price_configuration.panel.PriceConfiguration'
|
9
yuyu/local/enabled/_6104_admin_billing_setting.py
Normal file
9
yuyu/local/enabled/_6104_admin_billing_setting.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'billing_setting'
|
||||||
|
# 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.billing_setting.panel.BillingSetting'
|
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'projects_invoice'
|
||||||
|
# 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_invoice.panel.ProjectsInvoice'
|
8
yuyu/local/enabled/_6111_project_billing_panel_group.py
Normal file
8
yuyu/local/enabled/_6111_project_billing_panel_group.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL_GROUP = 'billing'
|
||||||
|
# The display name of the PANEL_GROUP. Required.
|
||||||
|
PANEL_GROUP_NAME = _('Billing')
|
||||||
|
# The slug of the dashboard the PANEL_GROUP associated with. Required.
|
||||||
|
PANEL_GROUP_DASHBOARD = 'project'
|
9
yuyu/local/enabled/_6112_project_billing_overview.py
Normal file
9
yuyu/local/enabled/_6112_project_billing_overview.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'project_billing_overview'
|
||||||
|
# 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.project_billing_overview.panel.ProjectBillingOverview'
|
9
yuyu/local/enabled/_6113_project_billing_usage_cost.py
Normal file
9
yuyu/local/enabled/_6113_project_billing_usage_cost.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'usage_cost'
|
||||||
|
# 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.usage_cost.panel.UsageCost'
|
9
yuyu/local/enabled/_6114_project_billing_invoice.py
Normal file
9
yuyu/local/enabled/_6114_project_billing_invoice.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'invoice'
|
||||||
|
# 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.invoice.panel.Invoice'
|
0
yuyu/project/invoice/__init__.py
Normal file
0
yuyu/project/invoice/__init__.py
Normal file
20
yuyu/project/invoice/panel.py
Normal file
20
yuyu/project/invoice/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 Invoice(horizon.Panel):
|
||||||
|
name = _("Invoices")
|
||||||
|
slug = "invoice"
|
31
yuyu/project/invoice/tables.py
Normal file
31
yuyu/project/invoice/tables.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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, *args, **kwargs):
|
||||||
|
print(datum, args, kwargs)
|
||||||
|
return reverse("horizon:project:invoice:download_pdf", kwargs={"id": datum['id']})
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceTable(tables.DataTable):
|
||||||
|
date = tables.WrappingColumn('date', verbose_name=_('Date'))
|
||||||
|
state = tables.WrappingColumn('state', verbose_name=_('State'))
|
||||||
|
total = tables.Column('total', verbose_name=_('Total'))
|
||||||
|
|
||||||
|
def get_object_id(self, datum):
|
||||||
|
return datum['id']
|
||||||
|
|
||||||
|
def get_object_display(self, datum):
|
||||||
|
return datum['date']
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "invoice_table"
|
||||||
|
hidden_title = True
|
||||||
|
verbose_name = _("Invoice")
|
||||||
|
row_actions = (DetailAction, )
|
116
yuyu/project/invoice/templates/invoice/download_pdf.html
Normal file
116
yuyu/project/invoice/templates/invoice/download_pdf.html
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<button onclick="javascript:downloadPdf();" class="btn btn-default">Download PDF</button>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div id="invoice">
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<dt>Invoice Month</dt>
|
||||||
|
<dd>
|
||||||
|
<h3>{{ invoice.start_date|date:"M Y" }}</h3>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<h5>Invoice State: {{ invoice.state_text }}</h5>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Total Cost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Instance</td>
|
||||||
|
<td>{{ instance_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Volume</td>
|
||||||
|
<td>{{ volume_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Floating IP</td>
|
||||||
|
<td>{{ fip_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Router</td>
|
||||||
|
<td>{{ router_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Snapshot</td>
|
||||||
|
<td>{{ snapshot_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Image</td>
|
||||||
|
<td>{{ image_cost }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Subtotal:</b> {{ invoice.subtotal_money }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if invoice.state != 1 %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Tax:</b> {{ invoice.tax_money }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><b>Total:</b> {{ invoice.total_money }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function downloadPdf() {
|
||||||
|
var pdf = new jsPDF('p', 'pt', 'letter');
|
||||||
|
// source can be HTML-formatted string, or a reference
|
||||||
|
// to an actual DOM element from which the text will be scraped.
|
||||||
|
source = $('#invoice')[0];
|
||||||
|
|
||||||
|
// we support special element handlers. Register them with jQuery-style
|
||||||
|
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
|
||||||
|
// There is no support for any other type of selectors
|
||||||
|
// (class, of compound) at this time.
|
||||||
|
specialElementHandlers = {
|
||||||
|
// element with id of "bypass" - jQuery style selector
|
||||||
|
'#bypassme': function (element, renderer) {
|
||||||
|
// true = "handled elsewhere, bypass text extraction"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
margins = {
|
||||||
|
top: 80,
|
||||||
|
bottom: 60,
|
||||||
|
left: 40,
|
||||||
|
width: 522
|
||||||
|
};
|
||||||
|
// all coords and widths are in jsPDF instance's declared units
|
||||||
|
// 'inches' in this case
|
||||||
|
pdf.fromHTML(
|
||||||
|
source, // HTML string or DOM elem ref.
|
||||||
|
margins.left, // x coord
|
||||||
|
margins.top, { // y coord
|
||||||
|
'width': margins.width, // max width of content on PDF
|
||||||
|
'elementHandlers': specialElementHandlers
|
||||||
|
},
|
||||||
|
|
||||||
|
function (dispose) {
|
||||||
|
// dispose: object with X, Y of the last line add to the PDF
|
||||||
|
// this allow the insertion of new lines after html
|
||||||
|
pdf.save('invoice.pdf');
|
||||||
|
}, margins);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
17
yuyu/project/invoice/templates/invoice/index.html
Normal file
17
yuyu/project/invoice/templates/invoice/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% load horizon %}
|
||||||
|
{% jstemplate %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Price_Volume_Panel" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endjstemplate %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
{{ table.render }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.2/jspdf.min.js"></script>
|
||||||
|
{% endblock %}
|
21
yuyu/project/invoice/urls.py
Normal file
21
yuyu/project/invoice/urls.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# 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.invoice import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^invoice/pdf/(?P<id>[^/]+)/$', views.InvoiceView.as_view(), name='download_pdf'),
|
||||||
|
]
|
82
yuyu/project/invoice/views.py
Normal file
82
yuyu/project/invoice/views.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# 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 datetime
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
from dateutil.tz import tzutc
|
||||||
|
from django.template.defaultfilters import date
|
||||||
|
from django.utils.timesince import timesince
|
||||||
|
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 horizon import views
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
|
||||||
|
from .tables import InvoiceTable
|
||||||
|
from ...core.utils.invoice_utils import state_to_text
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tables.DataTableView):
|
||||||
|
table_class = InvoiceTable
|
||||||
|
page_title = _("Invoice")
|
||||||
|
template_name = "project/invoice/invoice_table.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
try:
|
||||||
|
data = []
|
||||||
|
for d in self.invoice_uc.get_simple_list(self.request):
|
||||||
|
data.append({
|
||||||
|
'id': d['id'],
|
||||||
|
'date': formats.date_format(d['start_date'], 'M Y'),
|
||||||
|
'state': state_to_text(d['state']),
|
||||||
|
'total': d['total_money'] or d['subtotal_money']
|
||||||
|
})
|
||||||
|
return list(data)
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get invoice')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceView(views.APIView):
|
||||||
|
page_title = _("Invoice")
|
||||||
|
template_name = "project/invoice/download_pdf.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
invoice = self.invoice_uc.get_invoice(self.request, self.kwargs['id'])
|
||||||
|
context['invoice'] = invoice
|
||||||
|
|
||||||
|
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')
|
||||||
|
context['router_cost'] = self.get_sum_price(invoice, 'routers')
|
||||||
|
context['snapshot_cost'] = self.get_sum_price(invoice, 'snapshots')
|
||||||
|
context['image_cost'] = self.get_sum_price(invoice, 'images')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_sum_price(self, invoice, key):
|
||||||
|
instance_prices = map(
|
||||||
|
lambda x: Money(amount=x['price_charged'], currency=x['price_charged_currency']),
|
||||||
|
invoice.get(key, [])
|
||||||
|
)
|
||||||
|
|
||||||
|
return sum(instance_prices)
|
0
yuyu/project/project_billing_overview/__init__.py
Normal file
0
yuyu/project/project_billing_overview/__init__.py
Normal file
20
yuyu/project/project_billing_overview/panel.py
Normal file
20
yuyu/project/project_billing_overview/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 ProjectBillingOverview(horizon.Panel):
|
||||||
|
name = _("Billing Overview")
|
||||||
|
slug = "project_billing_overview"
|
|
@ -0,0 +1,132 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Billing Setting" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">This Month Total Resource Allocation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="totalResourceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Active Resource Allocation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="activeResourceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">This Month All Resource Price Charged</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="totalResourcePriceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Active Resource Price Charged</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<canvas id="activeResourcePriceChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"
|
||||||
|
integrity="sha512-TW5s0IT/IppJtu76UbysrBH9Hy/5X41OTAbQuffZFU6lQ1rdcLHzpU5BzVvr/YFykoiMYZVWlr/PX1mDcfM9Qg=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function showBarChart(id, title, data) {
|
||||||
|
const ctx = document.getElementById(id).getContext('2d');
|
||||||
|
const myChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.label,
|
||||||
|
datasets: [{
|
||||||
|
label: title,
|
||||||
|
data: data.data,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 99, 132, 1)',
|
||||||
|
'rgba(54, 162, 235, 1)',
|
||||||
|
'rgba(255, 206, 86, 1)',
|
||||||
|
'rgba(75, 192, 192, 1)',
|
||||||
|
'rgba(153, 102, 255, 1)',
|
||||||
|
'rgba(255, 159, 64, 1)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPieChart(id, title, data) {
|
||||||
|
const ctx = document.getElementById(id).getContext('2d');
|
||||||
|
const myChart = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: data.label,
|
||||||
|
datasets: [{
|
||||||
|
label: title,
|
||||||
|
data: data.data,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(255, 99, 132, 0.2)',
|
||||||
|
'rgba(54, 162, 235, 0.2)',
|
||||||
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
'rgba(75, 192, 192, 0.2)',
|
||||||
|
'rgba(153, 102, 255, 0.2)',
|
||||||
|
'rgba(255, 159, 64, 0.2)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(255, 99, 132, 1)',
|
||||||
|
'rgba(54, 162, 235, 1)',
|
||||||
|
'rgba(255, 206, 86, 1)',
|
||||||
|
'rgba(75, 192, 192, 1)',
|
||||||
|
'rgba(153, 102, 255, 1)',
|
||||||
|
'rgba(255, 159, 64, 1)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showBarChart('totalResourceChart', 'This Month Total Resource Allocation', JSON.parse('{{ total_resource_json | safe }}'));
|
||||||
|
showPieChart('activeResourceChart', 'Active Resource Allocation', JSON.parse('{{ active_resource_json | safe }}'));
|
||||||
|
showBarChart('totalResourcePriceChart', 'This Month All Resource Price Charged', JSON.parse('{{ price_total_resource_json | safe }}'));
|
||||||
|
showPieChart('activeResourcePriceChart', 'Active Resource Price Charged', JSON.parse('{{ price_active_resource_json | safe }}'));
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
19
yuyu/project/project_billing_overview/urls.py
Normal file
19
yuyu/project/project_billing_overview/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 . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
]
|
34
yuyu/project/project_billing_overview/views.py
Normal file
34
yuyu/project/project_billing_overview/views.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
from django import shortcuts
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import views
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.project_overview_use_case import ProjectOverviewUseCase
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(views.APIView):
|
||||||
|
page_title = _("Billing Overview")
|
||||||
|
template_name = "project/project_billing_overview/index.html"
|
||||||
|
|
||||||
|
overview_uc = ProjectOverviewUseCase()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['total_resource_json'] = json.dumps(self.overview_uc.total_resource(self.request))
|
||||||
|
context['active_resource_json'] = json.dumps(self.overview_uc.active_resource(self.request))
|
||||||
|
context['price_total_resource_json'] = json.dumps(self.overview_uc.price_total_resource(self.request))
|
||||||
|
context['price_active_resource_json'] = json.dumps(self.overview_uc.price_active_resource(self.request))
|
||||||
|
|
||||||
|
return context
|
0
yuyu/project/usage_cost/__init__.py
Normal file
0
yuyu/project/usage_cost/__init__.py
Normal file
20
yuyu/project/usage_cost/panel.py
Normal file
20
yuyu/project/usage_cost/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 UsageCost(horizon.Panel):
|
||||||
|
name = _("Usage Cost")
|
||||||
|
slug = "usage_cost"
|
|
@ -0,0 +1,60 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{{ page_title }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
{% if invoice %}
|
||||||
|
<div>
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Invoice Month</dt>
|
||||||
|
<dd>
|
||||||
|
<select id="invoice_select" onchange="onInvoiceChange(this.value)">
|
||||||
|
{% for i in invoice_list %}
|
||||||
|
<option value="{{ i.id }}" {% if i.id == invoice.id %}
|
||||||
|
selected {% endif %}>{{ i.start_date|date:"M Y" }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</dd>
|
||||||
|
<dt>Invoice State</dt>
|
||||||
|
<dd>{{ invoice.state_text }}</dd>
|
||||||
|
<dt>Subtotal</dt>
|
||||||
|
<dd>{{ invoice.subtotal_money }}</dd>
|
||||||
|
{% if invoice.state != 1 %}
|
||||||
|
<dt>Tax</dt>
|
||||||
|
<dd>{{ invoice.tax_money }}</dd>
|
||||||
|
<dt>Total</dt>
|
||||||
|
<dd>{{ invoice.total_money }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="instance-cost">
|
||||||
|
{{ instance_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="volume-cost">
|
||||||
|
{{ volume_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="floating-ip-cost">
|
||||||
|
{{ floating_ip_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="router-cost">
|
||||||
|
{{ router_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="snapshot-cost">
|
||||||
|
{{ snapshot_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="image-cost">
|
||||||
|
{{ image_cost_table.render }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h1>Billing not enabled or you don't have any usage yet</h1> <br/>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
function onInvoiceChange(val) {
|
||||||
|
var search = "?invoice_id=" + val;
|
||||||
|
window.location.href = window.location.protocol + "//" + window.location.host + window.location.pathname + search;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
17
yuyu/project/usage_cost/templates/usage_cost/index.html
Normal file
17
yuyu/project/usage_cost/templates/usage_cost/index.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% load horizon %}
|
||||||
|
{% jstemplate %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Price_Volume_Panel" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Price_Volume_Panel") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endjstemplate %}
|
19
yuyu/project/usage_cost/tests.py
Normal file
19
yuyu/project/usage_cost/tests.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 horizon.test import helpers as test
|
||||||
|
|
||||||
|
|
||||||
|
class Price_Volume_PanelTests(test.TestCase):
|
||||||
|
# Unit tests for price_volume_panel.
|
||||||
|
def test_me(self):
|
||||||
|
self.assertTrue(1 + 1 == 2)
|
20
yuyu/project/usage_cost/urls.py
Normal file
20
yuyu/project/usage_cost/urls.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.conf.urls import url
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.yuyu.project.usage_cost import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
]
|
169
yuyu/project/usage_cost/views.py
Normal file
169
yuyu/project/usage_cost/views.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
# 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.timesince import timesince
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from djmoney.money import Money
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import tables
|
||||||
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.dashboards.yuyu.cases.invoice_use_case import InvoiceUseCase
|
||||||
|
from openstack_dashboard.dashboards.yuyu.core.usage_cost.tables import InstanceCostTable, VolumeCostTable, \
|
||||||
|
FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tables.MultiTableView):
|
||||||
|
table_classes = (
|
||||||
|
InstanceCostTable, VolumeCostTable, FloatingIpCostTable, RouterCostTable, SnapshotCostTable, ImageCostTable)
|
||||||
|
page_title = _("Usage Cost")
|
||||||
|
template_name = "project/usage_cost/cost_tables.html"
|
||||||
|
|
||||||
|
invoice_uc = InvoiceUseCase()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
request.invoice_list = self.invoice_uc.get_simple_list(self.request)
|
||||||
|
request.has_invoice = len(request.invoice_list) != 0
|
||||||
|
|
||||||
|
invoice_id = self.request.GET.get('invoice_id', None)
|
||||||
|
if not invoice_id and request.has_invoice:
|
||||||
|
invoice_id = request.invoice_list[0]['id']
|
||||||
|
|
||||||
|
request.invoice = self.invoice_uc.get_invoice(self.request, invoice_id) if request.has_invoice else {}
|
||||||
|
return super(IndexView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['invoice_list'] = self.request.invoice_list
|
||||||
|
context['invoice'] = self.request.invoice
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_instance_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
"flavor": api.nova.flavor_get(self.request, x['flavor_id']).name,
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('instances', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get instance cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_volume_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['volume_name'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
'type': api.cinder.volume_type_get(self.request, x['volume_type_id']).name,
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('volumes', []))
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get volume cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_floating_ip_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['ip'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('floating_ips', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get floating ip cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_router_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('routers', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get router cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_snapshot_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('snapshots', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get snapshot cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_image_cost_data(self):
|
||||||
|
try:
|
||||||
|
datas = map(lambda x: {
|
||||||
|
"id": x['id'],
|
||||||
|
"name": x['name'],
|
||||||
|
'size': x['space_allocation_gb'],
|
||||||
|
"usage": timesince(
|
||||||
|
dateutil.parser.isoparse(x['start_date']),
|
||||||
|
dateutil.parser.isoparse(x['adjusted_end_date'])
|
||||||
|
),
|
||||||
|
"cost": Money(amount=x['price_charged'], currency=x['price_charged_currency'])
|
||||||
|
}, self.request.invoice.get('images', []))
|
||||||
|
|
||||||
|
return datas
|
||||||
|
except Exception:
|
||||||
|
error_message = _('Unable to get images cost')
|
||||||
|
exceptions.handle(self.request, error_message)
|
||||||
|
|
||||||
|
return []
|
Loading…
Add table
Reference in a new issue