diff --git a/aleksis/apps/tezor/menus.py b/aleksis/apps/tezor/menus.py index 1355051be6de23342f18e317f4382aa68a96452e..30701d462d46ad2f20aeffb60e0e3b5c01a7f05f 100644 --- a/aleksis/apps/tezor/menus.py +++ b/aleksis/apps/tezor/menus.py @@ -6,7 +6,7 @@ MENUS = { "name": _("Payments and Money"), "url": "#", "root": True, - "svg_icon": "mdi:piggy_bank", + "svg_icon": "mdi:piggy-bank", "validators": [ "menu_generator.validators.is_authenticated", "aleksis.core.util.core_helpers.has_person", diff --git a/aleksis/apps/tezor/migrations/0004_client_email.py b/aleksis/apps/tezor/migrations/0004_client_email.py new file mode 100644 index 0000000000000000000000000000000000000000..c6f4a5c7b5106d312538dc16b9a94e5950044be1 --- /dev/null +++ b/aleksis/apps/tezor/migrations/0004_client_email.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.12 on 2022-03-09 20:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tezor', '0003_manual_invoicing'), + ] + + operations = [ + migrations.AddField( + model_name='client', + name='email', + field=models.EmailField(default='', max_length=254, verbose_name='Email'), + preserve_default=False, + ), + ] diff --git a/aleksis/apps/tezor/models/base.py b/aleksis/apps/tezor/models/base.py index 459f9b22b924289f33a4f192e28b2794973184e0..85f85ccf9f3d67cc362c6704852c81602f958101 100644 --- a/aleksis/apps/tezor/models/base.py +++ b/aleksis/apps/tezor/models/base.py @@ -6,6 +6,7 @@ from aleksis.core.mixins import ExtensibleModel class Client(ExtensibleModel): name = models.CharField(verbose_name=_("Name"), max_length=255) + email = models.EmailField(verbose_name=_("Email")) class Meta: constraints = [ diff --git a/aleksis/apps/tezor/models/invoice.py b/aleksis/apps/tezor/models/invoice.py index af5cb872fd1dfa856522c9a7da269d596f586d3c..33a3791f8cb838a9b4431cb7b8b6f47c0c3f11aa 100644 --- a/aleksis/apps/tezor/models/invoice.py +++ b/aleksis/apps/tezor/models/invoice.py @@ -81,6 +81,11 @@ class Invoice(BasePayment, PureDjangoModel): ) items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items")) + class Meta: + permissions = ( + ("send_invoice_email", _("Can send invoice by email")), + ) + @classmethod def get_variant_choices(cls): choices = [] @@ -98,10 +103,12 @@ class Invoice(BasePayment, PureDjangoModel): return self.__class__.STATUS_ICONS[self.status] def get_purchased_items(self): - for item in self.items.all(): - yield item.as_purchased_item() + if self.items.count(): + for item in self.items.all(): + yield item.as_purchased_item() else: - return self.for_object.get_purchased_items() + for item in self.for_object.get_purchased_items(): + yield item def get_person(self): if self.person: @@ -122,6 +129,12 @@ class Invoice(BasePayment, PureDjangoModel): ), ] + def get_billing_email_recipients(self): + if hasattr(self.for_object, "get_billing_email_recipients"): + return self.for_object.get_billing_email_recipients() + else: + return [self.billing_email] + @property def purchased_items_table(self): items = [i._asdict() for i in self.get_purchased_items()] @@ -154,11 +167,14 @@ class Invoice(BasePayment, PureDjangoModel): return TotalsTable(values) - def get_success_url(self): + def get_absolute_url(self): return reverse("invoice_by_token", kwargs={"slug": self.token}) + def get_success_url(self): + return self.get_absolute_url() + def get_failure_url(self): - return reverse("invoice_by_token", kwargs={"slug": self.token}) + return self.get_absolute_url() class InvoiceItem(ExtensibleModel): diff --git a/aleksis/apps/tezor/rules.py b/aleksis/apps/tezor/rules.py index 8f2e4f8916673689f8e773e9d03b04d7660d5ccc..3adb6f722d00cc0090dcb10683f185df73a87ba4 100644 --- a/aleksis/apps/tezor/rules.py +++ b/aleksis/apps/tezor/rules.py @@ -134,3 +134,11 @@ print_invoice_predicate = ( view_invoice_predicate & display_billing_predicate & display_purchased_items_predicate ) rules.add_perm("tezor.print_invoice_rule", print_invoice_predicate) + +# Send invoice email +send_invoice_email_predicate = ( + has_person & is_own_invoice + | has_global_perm("tezor.send_invoice_email") + | has_object_perm("tezor.send_invoice_email") +) +rules.add_perm("tezor.send_invoice_email_rule", send_invoice_email_predicate) diff --git a/aleksis/apps/tezor/tasks.py b/aleksis/apps/tezor/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..3e0569dc58ea178ccd7328a7483ed45b56ff5186 --- /dev/null +++ b/aleksis/apps/tezor/tasks.py @@ -0,0 +1,23 @@ +from aleksis.core.celery import app +from aleksis.core.util.email import send_email +from aleksis.core.util.pdf import generate_pdf_from_template + +from .models.invoice import Invoice + +@app.task +def email_invoice(invoice_token): + context = {} + invoice = Invoice.objects.get(token=invoice_token) + context["invoice"] = invoice + + invoice_pdf, result = generate_pdf_from_template(invoice.group.template_name, context) + result.wait(timeout=30, disable_sync_subtasks=False) + invoice_pdf.refresh_from_db() + + send_email( + template_name="invoice", + from_email=invoice.group.client.email, + recipient_list=invoice.get_billing_email_recipients(), + context=context, + attachments=[(invoice_pdf.file.name, invoice_pdf.file.read(), "application/pdf")], + ) diff --git a/aleksis/apps/tezor/templates/templated_email/invoice.email b/aleksis/apps/tezor/templates/templated_email/invoice.email new file mode 100644 index 0000000000000000000000000000000000000000..8fe6d0572efd6ff1786a71ce771e23e758818b3e --- /dev/null +++ b/aleksis/apps/tezor/templates/templated_email/invoice.email @@ -0,0 +1,21 @@ +{% extends "templated_email/base.email" %} +{% load i18n %} + +{% block subject_content %}{% trans "Invoice" %} {{ invoice.number }}{% endblock %} + +{% block html_content %} +<p> + {% blocktrans with number=invoice.number description=invoice.description %} + Please find attached invoice number {{ number }} for {{ description }}. + Please carefully read the PDF file concerning all payment details. + {% endblocktrans %} +</p> +{% if invoice.status == "waiting" %} +<p> + {% blocktrans %} + Please visit the following link to view and make the payment: + {% endblocktrans %} +</p> +<a href="{{ BASE_URL }}{{ invoice.get_absolute_url }}">{{ BASE_URL }}{{ invoice.get_absolute_url}}</a> +{% endif %} +{% endblock %} diff --git a/aleksis/apps/tezor/templates/tezor/invoice/full.html b/aleksis/apps/tezor/templates/tezor/invoice/full.html index 345aed939f0ef2ad1e2e8a15b58d18286c0c9eae..01576f32b22e02007bb7c4232c0079eb669c8645 100644 --- a/aleksis/apps/tezor/templates/tezor/invoice/full.html +++ b/aleksis/apps/tezor/templates/tezor/invoice/full.html @@ -12,6 +12,7 @@ {% has_perm 'tezor.display_purchased_items_rule' user object as can_view_purchased_items %} {% has_perm 'tezor.display_billing_rule' user object as can_view_billing_information %} {% has_perm 'tezor.print_invoice_rule' user object as can_print_invoice %} + {% has_perm 'tezor.send_invoice_email_rule' user object as can_send_invoice_email %} <h1>{% trans "Invoice" %} {{ object.number }} — {{ object.created.date }}</h1> @@ -21,6 +22,9 @@ {% if can_print_invoice %} <a class="btn colour-primary waves-effect waves-light" href="{% url 'print_invoice' object.token %}">{% trans "Print" %}</a> {% endif %} + {% if can_send_invoice_email %} + <a class="btn colour-primary waves-effect waves-light" href="{% url 'send_invoice_by_token' object.token %}">{% trans "Send Email" %}</a> + {% endif %} <div class="row"> {% if can_view_billing_information %} diff --git a/aleksis/apps/tezor/urls.py b/aleksis/apps/tezor/urls.py index 57b92ac76fc5e533d3f2e4ca8dd7a160a9e3a41f..2bd72955560338d91b87cd4af72f82feeaefb2ea 100644 --- a/aleksis/apps/tezor/urls.py +++ b/aleksis/apps/tezor/urls.py @@ -56,4 +56,9 @@ urlpatterns = [ views.InvoiceDetailView.as_view(), name="invoice_by_token", ), + path( + "invoice/<str:token>/send/", + views.SendInvoiceEmail.as_view(), + name="send_invoice_by_token", + ), ] diff --git a/aleksis/apps/tezor/views.py b/aleksis/apps/tezor/views.py index 33b49a80feb15daf0e18fff683b7bae8f2c8b7f6..2b27b63e1b72145ed7c7f57a1f71a309f719af37 100644 --- a/aleksis/apps/tezor/views.py +++ b/aleksis/apps/tezor/views.py @@ -9,14 +9,18 @@ from django.views.generic.detail import DetailView from django_tables2.views import RequestConfig, SingleTableView from payments import PaymentStatus, RedirectNeeded, get_payment_model from rules.contrib.views import PermissionRequiredMixin +from django_tables2.views import SingleTableView, RequestConfig +from templated_email import InlineImage, send_templated_mail from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView +from aleksis.core.util.pdf import generate_pdf_from_template from aleksis.core.views import RenderPDFView from .forms import EditClientForm, EditInvoiceGroupForm from .models.base import Client from .models.invoice import Invoice, InvoiceGroup from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable +from .tasks import email_invoice class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): @@ -29,7 +33,6 @@ class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): self.template_name = invoice.group.template_name context["invoice"] = invoice - print(invoice.group.__dict__) return context @@ -189,3 +192,17 @@ class InvoiceDetailView(PermissionRequiredMixin, DetailView): slug_field = "token" permission_required = "tezor.view_invoice_rule" template_name = "tezor/invoice/full.html" + + +class SendInvoiceEmail(PermissionRequiredMixin, View): + + permission_required = "tezor.send_invoice_email_rule" + + def get(self, request, token): + email_invoice.delay(token) + + url = request.META.get("HTTP_REFERRER") + if not url: + url = Invoice.objects.get(token=token).get_absolute_url() + + return redirect(url)