diff --git a/aleksis/apps/tezor/forms.py b/aleksis/apps/tezor/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..6f35f89f3c3d69baa88bc6c0794e2723b3de3926 --- /dev/null +++ b/aleksis/apps/tezor/forms.py @@ -0,0 +1,26 @@ +from material import Layout, Row + +from aleksis.core.mixins import ExtensibleForm + +from .models.base import Client +from .models.invoice import InvoiceGroup + +class EditClientForm(ExtensibleForm): + """Form to create or edit clients.""" + + layout = Layout("name") + + class Meta: + model = Client + exclude = [] + + +class EditInvoiceGroupForm(ExtensibleForm): + + layout = Layout( + Row("name", "template_name") + ) + + class Meta: + model = InvoiceGroup + exclude = ["client"] diff --git a/aleksis/apps/tezor/menus.py b/aleksis/apps/tezor/menus.py new file mode 100644 index 0000000000000000000000000000000000000000..49c9644d489921913cd07a84aeaf217008bfa661 --- /dev/null +++ b/aleksis/apps/tezor/menus.py @@ -0,0 +1,29 @@ +from django.utils.translation import gettext_lazy as _ + +MENUS = { + "NAV_MENU_CORE": [ + { + "name": _("Payments"), + "url": "#", + "root": True, + "icon": "price_check", + "validators": [ + "menu_generator.validators.is_authenticated", + "aleksis.core.util.core_helpers.has_person", + ], + "submenu": [ + { + "name": _("Clients"), + "url": "clients", + "icon": "account_balance", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "tezor.can_view_clients", + ) + ], + }, + ], + } + ] +} diff --git a/aleksis/apps/tezor/models/base.py b/aleksis/apps/tezor/models/base.py index 7c1815365e65a9801aad6c19ce567514f37472e3..459f9b22b924289f33a4f192e28b2794973184e0 100644 --- a/aleksis/apps/tezor/models/base.py +++ b/aleksis/apps/tezor/models/base.py @@ -11,3 +11,6 @@ class Client(ExtensibleModel): constraints = [ models.UniqueConstraint(fields=["name", "site"], name="uniq_client_per_site") ] + + def __str__(self) -> str: + return self.name diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py index 448f470b9293fe62909ed3d6d727ce3b3c119ee7..fd6be73de84a1e3113009a4d38351adc9d7db4c2 100644 --- a/aleksis/apps/tezor/models/invoice.py +++ b/aleksis/apps/tezor/models/invoice.py @@ -21,6 +21,10 @@ class InvoiceGroup(ExtensibleModel): template_name = models.CharField(verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255) + def __str__(self) -> str: + return self.name + + class Meta: constraints = [ models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client") diff --git a/aleksis/apps/tezor/tables.py b/aleksis/apps/tezor/tables.py index 3cf48132ba0ceab99588815c898defa8b69ce2ee..d9001895cd8d4232f3ff24b25ea847b2747077db 100644 --- a/aleksis/apps/tezor/tables.py +++ b/aleksis/apps/tezor/tables.py @@ -1,6 +1,7 @@ from django.utils.translation import gettext_lazy as _ import django_tables2 as tables +from django_tables2.utils import A class PurchasedItemsTable(tables.Table): @@ -32,3 +33,73 @@ class TotalsTable(tables.Table): class Meta: show_header = False orderable = False + + +class ClientsTable(tables.Table): + class Meta: + attrs = {"class": "responsive-table highlight"} + + name = tables.Column() + + view = tables.LinkColumn( + "client_by_pk", + args=[A("id")], + verbose_name=_("View"), + text=_("View"), + ) + edit = tables.LinkColumn( + "edit_client_by_pk", + args=[A("id")], + verbose_name=_("Edit"), + text=_("Edit"), + ) + delete = tables.LinkColumn( + "delete_client_by_pk", + args=[A("id")], + verbose_name=_("Delete"), + text=_("Delete"), + ) + +class InvoiceGroupsTable(tables.Table): + + name = tables.Column() + template_name = tables.Column() + view = tables.LinkColumn( + "invoice_group_by_pk", + args=[A("id")], + verbose_name=_("View"), + text=_("View"), + ) + edit = tables.LinkColumn( + "edit_invoice_group_by_pk", + args=[A("id")], + verbose_name=_("Edit"), + text=_("Edit"), + ) + delete = tables.LinkColumn( + "delete_invoice_group_by_pk", + args=[A("id")], + verbose_name=_("Delete"), + text=_("Delete"), + ) + +class InvoicesTable(tables.Table): + + transaction_id = tables.Column() + status = tables.Column() + created = tables.DateColumn() + billing_first_name = tables.Column() + billing_last_name = tables.Column() + total = tables.Column() + view = tables.LinkColumn( + "invoice_by_pk", + args=[A("id")], + verbose_name=_("View"), + text=_("View"), + ) + print = tables.LinkColumn( + "get_invoice_by_pk", + args=[A("id")], + verbose_name=_("Print"), + text=_("Print"), + ) diff --git a/aleksis/apps/tezor/templates/tezor/client/create.html b/aleksis/apps/tezor/templates/tezor/client/create.html new file mode 100644 index 0000000000000000000000000000000000000000..634500f5ff3e0352f07a46cc073f19ad382c14e4 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/client/create.html @@ -0,0 +1,19 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Create client{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Create client{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/client/edit.html b/aleksis/apps/tezor/templates/tezor/client/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..3c193950bc685dc5224f9f2248fbbd60a8639827 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/client/edit.html @@ -0,0 +1,18 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Edit client{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Edit client{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/client/full.html b/aleksis/apps/tezor/templates/tezor/client/full.html new file mode 100644 index 0000000000000000000000000000000000000000..19c4ab002a7c1ab3fc4ded90ee286c3d27083260 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/client/full.html @@ -0,0 +1,14 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% load render_table from django_tables2 %} + +{% block page_title %}{% blocktrans %}Client{% endblocktrans %} {{ client }}{% endblock %} +{% block browser_title %}{% blocktrans %}Client{% endblocktrans %} {{ client }}{% endblock %} + +{% block content %} + + <a class="btn colour-primary waves-effect waves-light" href="{% url 'create_invoice_group' client.id %}">{% trans "Add invoice group" %}</a> + {% render_table invoice_groups_table %} + +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/client/list.html b/aleksis/apps/tezor/templates/tezor/client/list.html new file mode 100644 index 0000000000000000000000000000000000000000..e8b32263958ec250023248fcb08b75022ca05c79 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/client/list.html @@ -0,0 +1,14 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% load render_table from django_tables2 %} + +{% block page_title %}{% blocktrans %}Clients{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Clients{% endblocktrans %}{% endblock %} + +{% block content %} + + <a class="btn colour-primary waves-effect waves-light" href="{% url 'create_client' %}">{% trans "Create client" %}</a> + {% render_table table %} + +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/empty.html b/aleksis/apps/tezor/templates/tezor/empty.html deleted file mode 100644 index 2fb4415f4772cd40a8eb40e290d50f912c4f219f..0000000000000000000000000000000000000000 --- a/aleksis/apps/tezor/templates/tezor/empty.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends 'core/base.html' %} -{% load i18n %} - -{% block content %} - <p class="flow-text"> - {% blocktrans %}Tezor (account and payment system){% endblocktrans %} - </p> -{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/invoice/full.html b/aleksis/apps/tezor/templates/tezor/invoice/full.html new file mode 100644 index 0000000000000000000000000000000000000000..add38a90fa88f33578f98b3282ae1f315d05a031 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/invoice/full.html @@ -0,0 +1,16 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% load render_table from django_tables2 %} + +{% block browser_title %}{{ object.transaction_id }}{% endblock %} + +{% block content %} + + <h1>{% trans "Invoice" %} {{ object.transaction_id }} — {{ object.created.date }}</h1> + <a class="btn colour-primary waves-effect waves-light" href="{% url 'invoice_group_by_pk' object.group.pk %}">{% trans "Back" %}</a> + + {% render_table object.purchased_items_table %} + {% render_table object.totals_table %} + +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/invoice_group/create.html b/aleksis/apps/tezor/templates/tezor/invoice_group/create.html new file mode 100644 index 0000000000000000000000000000000000000000..fa24a828cf0dd07141cab8f8bcd9ca5a50d0343d --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/invoice_group/create.html @@ -0,0 +1,19 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Create invoice group{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Create invoice group{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/invoice_group/edit.html b/aleksis/apps/tezor/templates/tezor/invoice_group/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..284a3ec0197f0e31d972e1c0c14e589685bd73ab --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/invoice_group/edit.html @@ -0,0 +1,18 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% block page_title %}{% blocktrans %}Edit invoice group{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Edit invoice group{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/invoice_group/full.html b/aleksis/apps/tezor/templates/tezor/invoice_group/full.html new file mode 100644 index 0000000000000000000000000000000000000000..6fecbff61a76f4176bc8e979d731b11a8c403a73 --- /dev/null +++ b/aleksis/apps/tezor/templates/tezor/invoice_group/full.html @@ -0,0 +1,14 @@ +{% extends "core/base.html" %} +{% load material_form i18n %} + +{% load render_table from django_tables2 %} + +{% block page_title %}{{ object }}{% endblock %} +{% block browser_title %}{{ object }}{% endblock %} + +{% block content %} + + <a class="btn colour-primary waves-effect waves-light" href="{% url 'client_by_pk' object.client.pk %}">{% trans "Back" %}</a> + {% render_table invoices_table %} + +{% endblock %} diff --git a/aleksis/apps/tezor/urls.py b/aleksis/apps/tezor/urls.py index 571809cf71d6d08182a261b2aa26cde2155b6844..627d64f756e6bd336acfe3be02a244a15ae069de 100644 --- a/aleksis/apps/tezor/urls.py +++ b/aleksis/apps/tezor/urls.py @@ -4,5 +4,55 @@ from . import views urlpatterns = [ path("payments/", include("payments.urls")), - path("invoice/<int:pk>/print", views.GetInvoicePDF.as_view(), name="get_invoice_by_pk") + path("invoice/<int:pk>/print", views.GetInvoicePDF.as_view(), name="get_invoice_by_pk"), + path( + "clients/list", + views.ClientListView.as_view(), + name="clients", + ), + path( + "clients/create", + views.ClientCreateView.as_view(), + name="create_client", + ), + path( + "clients/<int:pk>/edit", + views.ClientEditView.as_view(), + name="edit_client_by_pk", + ), + path( + "clients/<int:pk>/delete", + views.ClientDeleteView.as_view(), + name="delete_client_by_pk", + ), + path( + "clients/<int:pk>/", + views.ClientDetailView.as_view(), + name="client_by_pk", + ), + path( + "client/<int:pk>/invoice_groups/create", + views.InvoiceGroupCreateView.as_view(), + name="create_invoice_group", + ), + path( + "invoice_groups/<int:pk>/edit", + views.InvoiceGroupEditView.as_view(), + name="edit_invoice_group_by_pk", + ), + path( + "invoice_groups/<int:pk>/", + views.InvoiceGroupDetailView.as_view(), + name="invoice_group_by_pk", + ), + path( + "invoice_groups/<int:pk>/delete", + views.InvoiceGroupDeleteView.as_view(), + name="delete_invoice_group_by_pk", + ), + path( + "invoice/<int:pk>/", + views.InvoiceDetailView.as_view(), + name="invoice_by_pk", + ), ] diff --git a/aleksis/apps/tezor/views.py b/aleksis/apps/tezor/views.py index 22fe10830ab05dd5688ae8bf2a498a4e406ec45b..27939605b54e85cf57ce36abaad31d147e8132da 100644 --- a/aleksis/apps/tezor/views.py +++ b/aleksis/apps/tezor/views.py @@ -1,11 +1,22 @@ from django.views.generic import View from django.shortcuts import render +from django.views.decorators.cache import never_cache +from django.utils.decorators import method_decorator +from django.views.generic import FormView, TemplateView +from django.utils.translation import ugettext as _ +from django.urls import reverse, reverse_lazy +from django.views.generic.detail import DetailView from rules.contrib.views import PermissionRequiredMixin +from django_tables2.views import SingleTableView, RequestConfig from aleksis.core.views import RenderPDFView +from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView -from .models.invoice import Invoice +from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable +from .forms import EditClientForm, EditInvoiceGroupForm +from .models.base import Client +from .models.invoice import Invoice, InvoiceGroup class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): @@ -20,3 +31,125 @@ class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): print(invoice.group.__dict__) return context + +class ClientListView(PermissionRequiredMixin, SingleTableView): + """Table of all clients.""" + + model = Client + table_class = ClientsTable + permission_required = "tezor.view_clients" + template_name = "tezor/client/list.html" + + +@method_decorator(never_cache, name="dispatch") +class ClientCreateView(PermissionRequiredMixin, AdvancedCreateView): + """Create view for clients.""" + + model = Client + form_class = EditClientForm + permission_required = "tezor.add_clients" + template_name = "tezor/client/create.html" + success_url = reverse_lazy("clients") + success_message = _("The client has been created.") + + +@method_decorator(never_cache, name="dispatch") +class ClientEditView(PermissionRequiredMixin, AdvancedEditView): + """Edit view for clients.""" + + model = Client + form_class = EditClientForm + permission_required = "tezor.edit_clients" + template_name = "tezor/client/edit.html" + success_url = reverse_lazy("clients") + success_message = _("The client has been saved.") + + +class ClientDeleteView(PermissionRequiredMixin, AdvancedDeleteView): + """Delete view for client.""" + + model = Client + permission_required = "tezor.delete_client" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("clients") + success_message = _("The client has been deleted.") + + +class ClientDetailView(PermissionRequiredMixin, DetailView): + + model = Client + permission_required = "tezor.view_client" + template_name = "tezor/client/full.html" + + def get_context_data(self, object): + context = super().get_context_data() + + invoice_groups = object.invoice_groups.all() + invoice_groups_table = InvoiceGroupsTable(invoice_groups) + RequestConfig(self.request).configure(invoice_groups_table) + context["invoice_groups_table"] = invoice_groups_table + + return context + +class InvoiceGroupDetailView(PermissionRequiredMixin, DetailView): + + model = InvoiceGroup + permission_required = "tezor.view_invoice_group" + template_name = "tezor/invoice_group/full.html" + + def get_context_data(self, object): + context = super().get_context_data() + + invoices = object.invoices.all() + invoices_table = InvoicesTable(invoices) + RequestConfig(self.request).configure(invoices_table) + context["invoices_table"] = invoices_table + + return context + + +@method_decorator(never_cache, name="dispatch") +class InvoiceGroupCreateView(PermissionRequiredMixin, AdvancedCreateView): + """Create view for invoice_groups.""" + + model = InvoiceGroup + form_class = EditInvoiceGroupForm + permission_required = "tezor.add_invoice_groups" + template_name = "tezor/invoice_group/create.html" + success_url = reverse_lazy("clients") + success_message = _("The invoice_group has been created.") + + + def form_valid(self, form): + client = Client.objects.get(id=self.kwargs["pk"]) + InvoiceGroup.objects.create(client=client, name=form.cleaned_data["name"], template_name=form.cleaned_data["template_name"]) + + return super().form_valid(form) + +@method_decorator(never_cache, name="dispatch") +class InvoiceGroupEditView(PermissionRequiredMixin, AdvancedEditView): + """Edit view for invoice_groups.""" + + model = InvoiceGroup + form_class = EditInvoiceGroupForm + permission_required = "tezor.edit_invoice_groups" + template_name = "tezor/invoice_group/edit.html" + success_url = reverse_lazy("invoice_groups") + success_message = _("The invoice_group has been saved.") + + +class InvoiceGroupDeleteView(PermissionRequiredMixin, AdvancedDeleteView): + """Delete view for invoice_group.""" + + model = InvoiceGroup + permission_required = "tezor.delete_invoice_group" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("invoice_groups") + success_message = _("The invoice_group has been deleted.") + + +class InvoiceDetailView(PermissionRequiredMixin, DetailView): + + model = Invoice + permission_required = "tezor.view_invoice" + template_name = "tezor/invoice/full.html"