Add currency management

Fix color picker
Add exchange rate updater submodule
Fix logging in celery
A few invoicing fixes
HTTP request class
Navigation/URL updates
This commit is contained in:
Kumi 2020-06-03 12:35:23 +02:00
parent 2e8bbdd8a7
commit 31b0eed298
29 changed files with 397 additions and 50 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ expephalon/custom_settings.py
__pycache__/
migrations/
*.pid
.vscode/

3
.gitmodules vendored
View file

@ -16,3 +16,6 @@
[submodule "totp"]
path = totp
url = git@kumig.it:kumisystems/expephalon-totp.git
[submodule "ratesapi"]
path = ratesapi
url = git@kumig.it:kumisystems/expephalon-ratesapi.git

13
core/classes/http.py Normal file
View file

@ -0,0 +1,13 @@
from urllib.request import Request, urlopen
from core.helpers.expephalon import version_string
class HTTP:
def __init__(self, url, no_user_agent=False, *args, **kwargs):
self.request = Request(url, *args, **kwargs)
if not no_user_agent:
self.request.add_header("User-Agent", f"Expephalon/{version_string} (https://kumi.systems/)")
self.fetch()
def fetch(self):
self.data = urlopen(self.request).read()

View file

@ -19,7 +19,7 @@ class NavItem:
return self.__path if (self.__path.startswith("/") or "://" in self.__path) else reverse(self.__path)
class NavSection:
def __init__(self, name: str, icon: str):
def __init__(self, name: str, icon: str = ""):
self.__items = []
self.__name = name
self.__icon = icon

View file

@ -0,0 +1,10 @@
from django.conf import settings
from git import Repo
def version_string():
repo = Repo(settings["BASE_DIR"])
try:
return repo.head.commit.hexsha
except:
return "unknown"

View file

@ -5,7 +5,7 @@ from django.conf import settings
# Dashboard Section
dashboard_section = NavSection("Dashboard", "")
dashboard_section = NavSection("Dashboard")
dashboard_item = NavItem("Dashboard", "fa-rocket", "dashboard")
@ -15,7 +15,7 @@ navigations["backend_main"].add_section(dashboard_section)
# Clients Section
clients_section = NavSection("Clients", "")
clients_section = NavSection("Clients")
client_list_item = NavItem("List Clients", "fa-user-tag", "clients")
client_add_item = NavItem("Add Client", "fa-user-edit", "clients_create")
@ -31,7 +31,7 @@ navigations["backend_main"].add_section(clients_section)
# Quotes Section
quotes_section = NavSection("Quotes", "")
quotes_section = NavSection("Quotes")
quote_list_item = NavItem("List Quotes", "fa-file-invoice-dollar", "backendni")
quote_create_item = NavItem("Create Quote", "fa-plus-square", "backendni")
@ -43,7 +43,7 @@ navigations["backend_main"].add_section(quotes_section)
# Billing Section
billing_section = NavSection("Billing", "")
billing_section = NavSection("Billing")
invoice_list_item = NavItem("List Invoices", "fa-file-invoice-dollar", "invoices")
invoice_create_item = NavItem("Create Invoice", "fa-plus-square", "invoices_create")
@ -61,7 +61,7 @@ navigations["backend_main"].add_section(billing_section)
# Support Section
support_section = NavSection("Support", "")
support_section = NavSection("Support")
ticket_view_item = NavItem("View Tickets", "fa-life-ring", "backendni")
ticket_add_item = NavItem("Add Ticket", "fa-plus-square", "backendni")
@ -75,19 +75,32 @@ navigations["backend_main"].add_section(support_section)
# Reports Section
reports_section = NavSection("Reports", "")
reports_section = NavSection("Reports")
report_period_item = NavItem("Income by period", "fa-chart-bar", "backendni")
report_forecast_item = NavItem("Income Forecast", "fa-chart-area", "backendni")
report_more_item = NavItem("More reports...", "fa-chalkboard", "backendni")
reports_section.add_item(report_period_item)
reports_section.add_item(report_forecast_item)
navigations["backend_main"].add_section(reports_section)
# Products Section
products_section = NavSection("Products")
product_administration_item = NavItem("Products", "fa-cube", "backendni")
pgroup_administration_item = NavItem("Product Groups", "fa-cubes", "backendni")
products_section.add_item(product_administration_item)
products_section.add_item(pgroup_administration_item)
navigations["backend_main"].add_section(products_section)
# Administration Section
administration_section = NavSection("Administration", "")
administration_section = NavSection("Administration")
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins")
brand_administration_item = NavItem("Brands", "fa-code-branch", "brands")
@ -95,9 +108,9 @@ ratelimit_administration_item = NavItem("Firewall", "fa-shield-alt", "ratelimits
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni")
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni")
backup_administration_item = NavItem("Backups", "fa-shield-alt", "backendni")
product_administration_item = NavItem("Products", "fa-cube", "backendni")
pgroup_administration_item = NavItem("Product Groups", "fa-cubes", "backendni")
payment_administration_item = NavItem("Payment Gateways", "fa-credit-card", "backendni")
currency_administration_item = NavItem("Currencies", "fa-coins", "currencies")
tax_administration_item = NavItem("Tax Settings", "fa-handshake", "backendni")
dbsettings_item = NavItem("Database Settings", "fa-database", "dbsettings")
administration_section.add_item(user_administration_item)
@ -106,11 +119,9 @@ administration_section.add_item(ratelimit_administration_item)
administration_section.add_item(sms_administration_item)
administration_section.add_item(otp_administration_item)
administration_section.add_item(backup_administration_item)
administration_section.add_item(product_administration_item)
administration_section.add_item(pgroup_administration_item)
administration_section.add_item(payment_administration_item)
if "dbsettings" in settings.INSTALLED_APPS:
administration_section.add_item(currency_administration_item)
administration_section.add_item(tax_administration_item)
administration_section.add_item(dbsettings_item)
navigations["backend_main"].add_section(administration_section)

View file

@ -8,8 +8,9 @@ from core.urls.backend.brands import urlpatterns as brandpatterns
from core.urls.backend.firewall import urlpatterns as firewallpatterns
from core.urls.backend.invoices import urlpatterns as invoicepatterns
from core.urls.backend.clientgroups import urlpatterns as clientgrouppatterns
from core.urls.backend.currencies import urlpatterns as currencypatterns
urlpatterns = adminpatterns + clientpatterns + dbsettingspatterns + brandpatterns + firewallpatterns + invoicepatterns + clientgrouppatterns
urlpatterns = adminpatterns + clientpatterns + dbsettingspatterns + brandpatterns + firewallpatterns + invoicepatterns + clientgrouppatterns + currencypatterns
urlpatterns.append(path('admin/', DashboardView.as_view(), name="dashboard"))
urlpatterns.append(path('admin/oops/', BackendNotImplementedView.as_view(), name="backendni"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.currencies import CurrencyCreateView, CurrencyDeleteView, CurrencyEditView, CurrencyListView
urlpatterns = []
urlpatterns.append(path('admin/currencies/', CurrencyListView.as_view(), name="currencies"))
urlpatterns.append(path("admin/currencies/<pk>/delete/", CurrencyDeleteView.as_view(), name="currencies_delete"))
urlpatterns.append(path("admin/currencies/<pk>/edit/", CurrencyEditView.as_view(), name="currencies_edit"))
urlpatterns.append(path("admin/currencies/create/", CurrencyCreateView.as_view(), name="currencies_create"))

View file

@ -0,0 +1,46 @@
from django.conf import settings
from django.urls import reverse_lazy
from core.models.local import Currency
from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView
class CurrencyListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/currencies/index.html"
model = Currency
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Currency Settings"
return context
class CurrencyEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/currencies/update.html"
model = Currency
success_url = reverse_lazy("currencies")
fields = ["name", "code", "symbol", "base", "rate"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Currency"
return context
class CurrencyDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/currencies/delete.html"
model = Currency
success_url = reverse_lazy("currencies")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Currency"
return context
class CurrencyCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/currencies/create.html"
model = Currency
success_url = reverse_lazy("currencies")
fields = ["name", "code", "symbol", "base", "rate"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Currency"
return context

View file

@ -10,37 +10,37 @@ class InvoiceListView(BackendListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Brand Settings"
context["title"] = "Invoice Settings"
return context
class InvoiceEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/update.html"
model = Invoice
success_url = reverse_lazy("brands")
success_url = reverse_lazy("invoices")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Brand"
context["title"] = "Edit Invoice"
return context
class InvoiceDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/delete.html"
model = Invoice
success_url = reverse_lazy("brands")
success_url = reverse_lazy("invoices")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Brand"
context["title"] = "Delete Invoice"
return context
class InvoiceCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/create.html"
model = Invoice
success_url = reverse_lazy("brands")
success_url = reverse_lazy("invoices")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Brand"
context["title"] = "Create Invoice"
return context

View file

@ -1,19 +1,8 @@
from django.forms import TextInput
from django.conf import settings
from django.templatetags.static import static
from django.utils.safestring import mark_safe
class ColorPickerWidget(TextInput):
class Media:
css = {
"all": (
static("backend/css/colorPicker.css")
)
}
js = (
static("backend/scripts/jquery.colorPicker.js")
)
def render(self, name, value, attrs=None, renderer=None):
rendered = super().render(name, value, attrs=attrs, renderer=renderer)
return rendered + mark_safe(f'<script type="text/javascript">$("#id_{name}").colorPicker();</script>')
def __init__(self, attrs={}):
attrs["class"] = "colorpicker"
super().__init__(attrs)

View file

@ -1,2 +1,3 @@
rabbitmq-server
memcached
git

View file

@ -52,5 +52,7 @@ RABBITMQ_USER = "guest"
RABBITMQ_PASS = "guest"
# Logging
# Set log level to something higher in production - debug might leak sensitive information
LOG_DIRECTORY = "/var/log/expephalon/"
LOGLEVEL = "DEBUG"

View file

@ -149,6 +149,7 @@ CELERY_CACHE_BACKEND = 'django-cache'
CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_LOCATION}/{RABBITMQ_VHOST}"
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_TASK_RESULT_EXPIRES = 12 * 60 * 60
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
# Auth URLs
@ -165,7 +166,7 @@ except:
try:
with open(os.path.join(LOG_DIRECTORY, "expephalon.log"), "a") as logfile:
logfile.write("\n----------\n")
logfile.write("\n----------\n\nStarting Expephalon...\n")
except:
raise Exception(f"Unable to write to log file {os.path.join(LOG_DIRECTORY, 'expephalon.log')}, please make sure the Expephalon user account has sufficient privileges to write to the {LOG_DIRECTORY} directory and all files within it.")
@ -182,7 +183,7 @@ LOGGING = {
},
'handlers': {
'expephalon': {
'level': 'DEBUG',
'level': LOGLEVEL,
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_DIRECTORY, 'expephalon.log'),
'when': 'midnight',
@ -198,3 +199,9 @@ LOGGING = {
},
},
}
try:
with open(os.path.join(LOG_DIRECTORY, "expephacron.log"), "a") as logfile:
pass
except:
raise Exception(f"Unable to write to log file {os.path.join(LOG_DIRECTORY, 'expephacron.log')}, please make sure the Expephalon user account has sufficient privileges to write to the {LOG_DIRECTORY} directory and all files within it.")

1
ratesapi Submodule

@ -0,0 +1 @@
Subproject commit 0d2d9a491a1649a70107a588857e69e551c019a1

View file

@ -17,3 +17,4 @@ pyuca
git+https://kumig.it/kumisystems/parse_crontab.git
django-internationalflavor
suds-jurko
GitPython

View file

@ -1 +1,3 @@
#!/bin/bash
celery -A expephalon worker --loglevel=info

View file

@ -1 +1,3 @@
#!/bin/bash
celery -A expephalon beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler

View file

@ -0,0 +1 @@
$(".colorpicker").colorPicker({pickerDefault: "58D062"});

File diff suppressed because one or more lines are too long

View file

@ -349,4 +349,4 @@
showHexField: true
};
})(jQuery);
})($);

View file

@ -26,6 +26,8 @@
-->
<link href="{% static "backend/css/main.css" %}" rel="stylesheet">
<link href="https://fa.kumi.systems/css/all.css" rel="stylesheet">
{% block css %}{% endblock %}
<script src="{% static "backend/scripts/jquery-3.5.1.min.js" %}"></script>"
</head>
<body>
<div class="app-container app-theme-white body-tabs-shadow fixed-sidebar fixed-header">
@ -476,5 +478,6 @@
</div>
</div>
<script type="text/javascript" src="{% static "backend/scripts/main.js" %}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View file

@ -1,5 +1,9 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% load static %}
{% block css %}
<link href="{% static "backend/css/colorPicker.css" %}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
@ -51,7 +55,8 @@
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="{% static "backend/scripts/jquery.colorPicker.js" %}"></script>
<script type="text/javascript" src="{% static "backend/scripts/custom.colorPicker.js" %}"></script>
{% endblock %}

View file

@ -43,7 +43,7 @@
<tbody>
{% for group in object_list %}
<tr>
<td>{{ group.name }}</td>
<td style="color:{{ group.color }};">{{ group.name }}</td>
<td><a href="{% url "clientgroups_edit" group.id %}"><i class="fas fa-edit" title="Edit Client Group"></i></a> <a href="{% url "clientgroups_delete" group.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Client Group"></i></a></td>
</tr>
{% endfor %}

View file

@ -1,5 +1,9 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% load static %}
{% block css %}
<link href="{% static "backend/css/colorPicker.css" %}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
@ -51,7 +55,8 @@
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="{% static "backend/scripts/jquery.colorPicker.js" %}"></script>
<script type="text/javascript" src="{% static "backend/scripts/custom.colorPicker.js" %}"></script>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Currencies - Create Currency
<div class="page-title-subheading">Create a new currency
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Currency" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Currency
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Create Currency
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "currencies" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-database">
</i>
</div>
<div>Currencies - Delete Currency
<div class="page-title-subheading">Delete a currency from the system
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Currency
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting {{ object.name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete {{ object.name }}? Please note that this will fail if the currency has already been used in an invoice.
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "currencies" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,63 @@
{% extends "backend/base.html" %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Currencies
<div class="page-title-subheading">Create, edit and delete currencies
</div>
</div>
</div>
<div class="page-title-actions">
<a href="{% url "currencies_create" %}" type="button" data-toggle="tooltip" title="New Currency" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Currency
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Active Currencies
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
<table class="mb-0 table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Code</th>
<th>Symbol</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for currency in object_list %}
<tr>
<td>{{ currency.name }}</td>
<td>{{ currency.code }}</td>
<td>{{ currency.symbol }}</td>
<td><a href="{% url "currencies_edit" currency.id %}"><i class="fas fa-edit" title="Edit Currency"></i></a> <a href="{% url "currencies_delete" currency.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Currency"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,54 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Currencies - Edit Currency
<div class="page-title-subheading">Edit currency properties
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Currency" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Currency
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Editing {{ object.name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "currencies" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}