Merge branch 'email_notification' into 'main'
Email Notification and Notification Center API See merge request dev/yuyu!4
This commit is contained in:
commit
58464033e9
13 changed files with 395 additions and 25 deletions
|
@ -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']
|
||||
|
|
|
@ -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)),
|
||||
|
|
56
api/views.py
56
api/views.py
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
for handler in self.event_handler:
|
||||
handler.handle(event_type, payload)
|
||||
|
||||
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
|
||||
|
|
|
@ -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,15 +20,23 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **options):
|
||||
print("Processing Invoice")
|
||||
if not get_dynamic_setting(BILLING_ENABLED):
|
||||
return
|
||||
try:
|
||||
if not get_dynamic_setting(BILLING_ENABLED):
|
||||
return
|
||||
|
||||
self.close_date = timezone.now()
|
||||
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
||||
self.close_date = timezone.now()
|
||||
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
||||
|
||||
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
||||
for active_invoice in active_invoices:
|
||||
self.close_active_invoice(active_invoice)
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
|
31
core/migrations/0009_notification.py
Normal file
31
core/migrations/0009_notification.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
24
core/notification.py
Normal 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
105
templates/invoice.html
Normal 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: "Helvetica Neue",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>
|
|
@ -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
|
|
@ -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': [
|
||||
|
|
Loading…
Add table
Reference in a new issue