Email Notification and Notification Center API

This commit is contained in:
Setyo Nugroho 2022-07-08 07:43:26 +00:00
parent 6702c5e590
commit 7013b53e09
13 changed files with 395 additions and 25 deletions

View file

@ -1,7 +1,7 @@
from djmoney.contrib.django_rest_framework import MoneyField
from rest_framework import serializers
from core.models import Invoice, BillingProject
from core.models import Invoice, BillingProject, Notification
from core.component import component
@ -63,3 +63,11 @@ class BillingProjectSerializer(serializers.ModelSerializer):
class Meta:
model = BillingProject
fields = ['tenant_id', 'email_notification']
class NotificationSerializer(serializers.ModelSerializer):
project = BillingProjectSerializer()
class Meta:
model = Notification
fields = ['id', 'project', 'title', 'short_description', 'content', 'sent_status', 'is_read']

View file

@ -12,6 +12,7 @@ router.register(r'settings', views.DynamicSettingViewSet, basename='settings')
router.register(r'invoice', views.InvoiceViewSet, basename='invoice')
router.register(r'admin_overview', views.AdminOverviewViewSet, basename='admin_overview')
router.register(r'project_overview', views.ProjectOverviewViewSet, basename='project_overview')
router.register(r'notification', views.NotificationViewSet, basename='notification')
urlpatterns = [
path('', include(router.urls)),

View file

@ -8,14 +8,16 @@ from rest_framework import viewsets, serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer, BillingProjectSerializer, NotificationSerializer
from core.component import component, labels
from core.component.component import INVOICE_COMPONENT_MODEL
from core.exception import PriceNotFound
from core.models import Invoice, BillingProject
from core.models import Invoice, BillingProject, Notification
from core.notification import send_notification_from_template
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED, \
INVOICE_TAX
INVOICE_TAX, COMPANY_NAME, COMPANY_LOGO, COMPANY_ADDRESS
from core.utils.model_utils import InvoiceComponentMixin
from yuyu import settings
def get_generic_model_view_set(model):
@ -175,6 +177,19 @@ class InvoiceViewSet(viewsets.ModelViewSet):
if invoice.state == Invoice.InvoiceState.UNPAID:
invoice.finish()
send_notification_from_template(
project=invoice.project,
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} has been Paid',
short_description=f'Invoice is paid with total of {invoice.total}',
template='invoice.html',
context={
'invoice': invoice,
'company_name': get_dynamic_setting(COMPANY_NAME),
'logo': get_dynamic_setting(COMPANY_LOGO),
'address': get_dynamic_setting(COMPANY_ADDRESS),
}
)
serializer = InvoiceSerializer(invoice)
return Response(serializer.data)
@ -184,6 +199,19 @@ class InvoiceViewSet(viewsets.ModelViewSet):
if invoice.state == Invoice.InvoiceState.FINISHED:
invoice.rollback_to_unpaid()
send_notification_from_template(
project=invoice.project,
title=settings.EMAIL_TAG + f' Invoice #{invoice.id} in Unpaid',
short_description=f'Invoice is Unpaid with total of {invoice.total}',
template='invoice.html',
context={
'invoice': invoice,
'company_name': get_dynamic_setting(COMPANY_NAME),
'logo': get_dynamic_setting(COMPANY_LOGO),
'address': get_dynamic_setting(COMPANY_ADDRESS),
}
)
serializer = InvoiceSerializer(invoice)
return Response(serializer.data)
@ -339,3 +367,25 @@ class ProjectOverviewViewSet(viewsets.ViewSet):
data['data'].append(sum_of_price)
return Response(data)
class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = NotificationSerializer
def get_queryset(self):
tenant_id = self.request.query_params.get('tenant_id', None)
if tenant_id is None:
return Notification.objects.order_by('-created_at')
if tenant_id == 0:
return Notification.objects.filter(project=None).order_by('-created_at')
return Notification.objects.filter(project__tenant_id=tenant_id).order_by('-created_at')
@action(detail=True, methods=['GET'])
def resend(self, request, pk):
notification = Notification.objects.filter(id=pk).first()
notification.send()
serializer = NotificationSerializer(notification)
return Response(serializer.data)

View file

@ -2,7 +2,7 @@ from django.contrib import admin
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, BillingProject, Invoice, InvoiceVolume, \
InvoiceFloatingIp, InvoiceInstance, DynamicSetting, InvoiceImage, ImagePrice, SnapshotPrice, RouterPrice, \
InvoiceSnapshot, InvoiceRouter
InvoiceSnapshot, InvoiceRouter, Notification
@admin.register(DynamicSetting)
@ -34,6 +34,7 @@ class RouterPriceAdmin(admin.ModelAdmin):
class SnapshotPriceAdmin(admin.ModelAdmin):
list_display = ('hourly_price', 'monthly_price')
@admin.register(ImagePrice)
class ImagePriceAdmin(admin.ModelAdmin):
list_display = ('hourly_price', 'monthly_price')
@ -73,7 +74,12 @@ class InvoiceRouterAdmin(admin.ModelAdmin):
class InvoiceSnapshotAdmin(admin.ModelAdmin):
list_display = ('snapshot_id', 'name', 'space_allocation_gb')
@admin.register(InvoiceImage)
class InvoiceImageAdmin(admin.ModelAdmin):
list_display = ('image_id', 'name', 'space_allocation_gb')
@admin.register(Notification)
class InvoiceImageAdmin(admin.ModelAdmin):
list_display = ('project', 'title', 'short_description', 'sent_status')

View file

@ -6,6 +6,7 @@ from djmoney.money import Money
from core.exception import PriceNotFound
from core.models import InvoiceComponentMixin, PriceMixin
from core.notification import send_notification
class InvoiceHandler(metaclass=abc.ABCMeta):
@ -25,7 +26,14 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
price = self.get_price(payload)
if price is None:
raise PriceNotFound()
except PriceNotFound:
except PriceNotFound as e:
send_notification(
project=None,
title=f'{settings.EMAIL_TAG} [Error] Price not found when create invoice',
short_description=f'Price not found for {e.identifier} with payload {payload}',
content=f'Price not found or {e.identifier} with payload {payload}. Will use fallback price as 0.',
)
if fallback_price:
price = PriceMixin()
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
@ -74,15 +82,25 @@ class InvoiceHandler(metaclass=abc.ABCMeta):
price = self.get_price(self.get_price_dependency_from_instance(instance))
if price is None:
raise PriceNotFound()
except PriceNotFound:
hourly_price = price.hourly_price
monthly_price = price.monthly_price
except PriceNotFound as e:
send_notification(
project=None,
title=f'{settings.EMAIL_TAG} [Error] Price not found when rolling invoice',
short_description=f'Please check your Price configuration. Error on ID {getattr(instance, self.KEY_FIELD)}',
content=f'Please check your Price configuration fro {e.identifier}. Error on ID {getattr(instance, self.KEY_FIELD)} for '
f'invoice {getattr(instance, "invoice").id}',
)
if fallback_price:
price = PriceMixin()
price.hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
price.monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
hourly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
monthly_price = Money(amount=0, currency=settings.DEFAULT_CURRENCY)
else:
raise
instance.hourly_price = price.hourly_price
instance.monthly_price = price.monthly_price
instance.hourly_price = hourly_price
instance.monthly_price = monthly_price
instance.save()
return instance

View file

@ -1,9 +1,12 @@
import logging
import traceback
from oslo_messaging import NotificationResult
from core.component import component
from core.notification import send_notification
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
from yuyu import settings
LOG = logging.getLogger("yuyu_notification")
@ -23,7 +26,14 @@ class EventEndpoint(object):
return NotificationResult.HANDLED
# TODO: Error Handling
try:
for handler in self.event_handler:
handler.handle(event_type, payload)
except Exception:
send_notification(
project=None,
title=f'{settings.EMAIL_TAG} [Error] Error when handling OpenStack Notification',
short_description=f'There is an error when handling OpenStack Notification',
content=f'There is an error when handling OpenStack Notification \n {traceback.format_exc()}',
)
return NotificationResult.HANDLED

View file

@ -1,4 +1,5 @@
import logging
import traceback
from typing import Mapping, Dict, Iterable
from django.core.management import BaseCommand
@ -6,7 +7,10 @@ from django.utils import timezone
from core.models import Invoice, InvoiceComponentMixin
from core.component import component, labels
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX
from core.notification import send_notification_from_template, send_notification
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX, COMPANY_LOGO, COMPANY_NAME, \
COMPANY_ADDRESS
from yuyu import settings
LOG = logging.getLogger("yuyu_new_invoice")
@ -16,6 +20,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
print("Processing Invoice")
try:
if not get_dynamic_setting(BILLING_ENABLED):
return
@ -25,6 +30,13 @@ class Command(BaseCommand):
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
for active_invoice in active_invoices:
self.close_active_invoice(active_invoice)
except Exception:
send_notification(
project=None,
title='[Error] Error when processing Invoice',
short_description=f'There is an error when Processing Invoice',
content=f'There is an error when handling Processing Invoice \n {traceback.format_exc()}',
)
print("Processing Done")
def close_active_invoice(self, active_invoice: Invoice):
@ -55,3 +67,16 @@ class Command(BaseCommand):
handler.roll(active_component, self.close_date, update_payload={
"invoice": new_invoice
}, fallback_price=True)
send_notification_from_template(
project=active_invoice.project,
title=settings.EMAIL_TAG + f' Your Invoice #{active_invoice.id} is Ready',
short_description=f'Invoice is ready with total of {active_invoice.total}',
template='invoice.html',
context={
'invoice': active_invoice,
'company_name': get_dynamic_setting(COMPANY_NAME),
'logo': get_dynamic_setting(COMPANY_LOGO),
'address': get_dynamic_setting(COMPANY_ADDRESS),
}
)

View file

@ -0,0 +1,31 @@
# Generated by Django 3.2.6 on 2022-07-08 04:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20220621_2253'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=256)),
('short_description', models.CharField(max_length=524)),
('content', models.TextField()),
('sent_status', models.BooleanField()),
('is_read', models.BooleanField()),
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.billingproject')),
],
options={
'abstract': False,
},
),
]

View file

@ -1,12 +1,18 @@
import re
import math
from django.conf import settings
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.html import strip_tags
from djmoney.models.fields import MoneyField
from djmoney.money import Money
from core.component import labels
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
LABEL_VOLUMES
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
@ -103,6 +109,17 @@ class Invoice(BaseModel, TimestampMixin):
return total
@property
def state_str(self):
if self.state == Invoice.InvoiceState.IN_PROGRESS:
return 'In Progress'
if self.state == Invoice.InvoiceState.UNPAID:
return 'Unpaid'
if self.state == Invoice.InvoiceState.FINISHED:
return 'Finished'
def close(self, date, tax_percentage):
self.state = Invoice.InvoiceState.UNPAID
self.end_date = date
@ -120,6 +137,37 @@ class Invoice(BaseModel, TimestampMixin):
self.finish_date = None
self.save()
# Price for individual key
@property
def instance_price(self):
relation_all_row = getattr(self, LABEL_INSTANCES).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
@property
def volume_price(self):
relation_all_row = getattr(self, LABEL_VOLUMES).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
@property
def fip_price(self):
relation_all_row = getattr(self, LABEL_FLOATING_IPS).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
@property
def router_price(self):
relation_all_row = getattr(self, LABEL_ROUTERS).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
@property
def snapshot_price(self):
relation_all_row = getattr(self, LABEL_SNAPSHOTS).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
@property
def images_price(self):
relation_all_row = getattr(self, LABEL_IMAGES).all()
return sum(map(lambda x: x.price_charged, relation_all_row))
# end region
@ -204,4 +252,38 @@ class InvoiceImage(BaseModel, InvoiceComponentMixin):
def price_charged(self):
price_without_allocation = super().price_charged
return price_without_allocation * math.ceil(self.space_allocation_gb)
# end region
class Notification(BaseModel, TimestampMixin):
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE, blank=True, null=True)
title = models.CharField(max_length=256)
short_description = models.CharField(max_length=524)
content = models.TextField()
sent_status = models.BooleanField()
is_read = models.BooleanField()
def send(self):
from core.utils.dynamic_setting import get_dynamic_setting, EMAIL_ADMIN
def textify(html):
# Remove html tags and continuous whitespaces
text_only = re.sub('[ \t]+', ' ', strip_tags(html))
# Strip single spaces in the beginning of each line
return text_only.replace('\n ', '\n').strip()
recipient = [get_dynamic_setting(EMAIL_ADMIN)]
if self.project is not None:
recipient.append(self.project.email_notification)
send_mail(
subject=self.title,
message=textify(self.content),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipient,
html_message=self.content,
)
self.sent_status = True
self.save()

24
core/notification.py Normal file
View file

@ -0,0 +1,24 @@
from django.template.loader import render_to_string
from core.models import BillingProject, Notification
def send_notification(project, title: str, short_description: str, content: str):
notification = Notification(
project=project,
title=title,
short_description=short_description,
content=content,
sent_status=False,
is_read=False,
)
notification.save()
notification.send()
def send_notification_from_template(project: BillingProject, title: str, short_description: str, template: str,
context):
msg_html = render_to_string(template, context=context)
send_notification(project=project, title=title, short_description=short_description, content=msg_html)

105
templates/invoice.html Normal file
View file

@ -0,0 +1,105 @@
<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-family: sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;font-size: 10px;-webkit-tap-highlight-color: rgba(0,0,0,0);">
<head style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
</head>
<body style="margin-top: 20px;background: #eee;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin: 0;font-family: &quot;Helvetica Neue&quot;,Helvetica,Arial,sans-serif;font-size: 14px;line-height: 1.42857143;color: #333;background-color: #fff;">
<div class="container" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding-right: 15px;padding-left: 15px;margin-right: auto;margin-left: auto;">
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
<!-- BEGIN INVOICE -->
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
<div class="grid invoice" style="padding: 30px;position: relative;width: 100%;background: #fff;color: #666666;border-radius: 2px;margin-bottom: 25px;box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1);-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<div class="grid-body" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<div class="invoice-title" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
<img src="{{ logo }}" alt="" height="50" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;border: 0;vertical-align: middle;page-break-inside: avoid;max-width: 100%!important;">
<!-- <img src="https://www.seekpng.com/png/detail/246-2468199_logo-placeholder-png-white.png" alt="" height="35"> -->
</div>
</div>
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
<div class="col-xs-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 100%;">
<h2 style="margin-top: 0px;line-height: 0.8em;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;orphans: 3;widows: 3;page-break-after: avoid;font-family: inherit;font-weight: 500;color: inherit;margin-bottom: 10px;font-size: 30px;">invoice<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<span class="small" style="font-weight: 300;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-size: 65%;line-height: 1;color: #777;">order #{{ invoice.id }}</span></h2>
</div>
</div>
</div>
<hr style="margin-top: 10px;border-color: #ddd;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;height: 0;margin-bottom: 20px;border: 0;border-top: 1px solid #eee;">
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
<div class="col-xs-6" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 50%;">
<address style="max-width: 30%;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-bottom: 20px;font-style: normal;line-height: 1.42857143;">
{{ address }}
</address>
</div>
<div class="col-xs-6 text-right" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;text-align: right;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;float: left;width: 50%;">
<address style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-bottom: 20px;font-style: normal;line-height: 1.42857143;">
<strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Invoice Month:</strong><br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
{{ invoice.start_date | date:"M Y" }}
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Invoice State:</strong><br style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
{{ invoice.state_str }}
</address>
</div>
</div>
<div class="row" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;margin-right: -15px;margin-left: -15px;">
<div class="col-md-12" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;position: relative;min-height: 1px;padding-right: 15px;padding-left: 15px;width: 100%;">
<h3 style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;orphans: 3;widows: 3;page-break-after: avoid;font-family: inherit;font-weight: 500;line-height: 1.1;color: inherit;margin-top: 20px;margin-bottom: 10px;font-size: 24px;">ORDER SUMMARY</h3>
<table class="table table-striped" style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;border-spacing: 0;border-collapse: collapse!important;background-color: transparent;width: 100%;max-width: 100%;margin-bottom: 20px;">
<thead style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;display: table-header-group;">
<tr class="line" style="border-bottom: 1px solid #ccc;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">#</strong></td>
<td width="70%" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">COMPONENT</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">TOTAL COST</strong></td>
</tr>
</thead>
<tbody style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;">
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">1</td>
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Instance</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.instance_price }}</td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">2</td>
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Volume</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.volume_price }}</td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">3</td>
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Floating IP</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.fip_price }}</td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">4</td>
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Router</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.router_price }}</td>
</tr>
<tr class="line" style="border-bottom: 1px solid #ccc;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">5</td>
<td style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Snapshot</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;">{{ invoice.snapshot_price }}</td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Subtotal</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.subtotal }}</strong></td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Tax</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.tax }}</strong></td>
</tr>
<tr style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;page-break-inside: avoid;">
<td colspan="2" class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">Total</strong></td>
<td class="text-right" style="border: none;-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;padding: 8px;text-align: right;line-height: 1.42857143;vertical-align: top;border-top: 1px solid #ddd;background-color: #fff!important;"><strong style="-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;font-weight: 700;">{{ invoice.total }}</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- END INVOICE -->
</div>
</div>
</body>
</html>

View file

@ -5,3 +5,14 @@ YUYU_NOTIFICATION_TOPICS = ["notifications"]
CURRENCIES = ('IDR', 'USD')
DEFAULT_CURRENCY = "IDR"
# Email Setting
EMAIL_TAG = '[YUYU]'
DEFAULT_FROM_EMAIL = 'no-reply@btech.id'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = '<paste your gmail account here>'
EMAIL_HOST_PASSWORD = '<paste Google password or app password here>'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

View file

@ -58,8 +58,7 @@ ROOT_URLCONF = 'yuyu.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [