Skip to content
Snippets Groups Projects
Commit 3b59de15 authored by Tom Teichler's avatar Tom Teichler :beers:
Browse files

Merge branch 'master' into mailing

parents a3dfabd0 b311bcb6
No related branches found
No related tags found
1 merge request!5Mailing
Pipeline #59562 failed
from django.apps import apps from django.apps import apps
from django.db import OperationalError
from aleksis.core.util.apps import AppConfig from aleksis.core.util.apps import AppConfig
from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.core_helpers import get_site_preferences
...@@ -13,15 +14,23 @@ class DefaultConfig(AppConfig): ...@@ -13,15 +14,23 @@ class DefaultConfig(AppConfig):
"Repository": "https://edugit.org/AlekSIS/onboarding//AlekSIS-App-Tezor", "Repository": "https://edugit.org/AlekSIS/onboarding//AlekSIS-App-Tezor",
} }
licence = "EUPL-1.2+" licence = "EUPL-1.2+"
copyright_info = (([2022], "Dominik George", "dominik.george@teckids.org"), ([2022], "Tom Teichler", "tom.teichler@teckids.org"),) copyright_info = (
([2022], "Dominik George", "dominik.george@teckids.org"),
([2022], "Tom Teichler", "tom.teichler@teckids.org"),
)
def ready(self): def ready(self):
from django.conf import settings # noqa from django.conf import settings # noqa
settings.PAYMENT_VARIANTS = {} settings.PAYMENT_VARIANTS = {}
for app_config in apps.app_configs.values(): for app_config in apps.app_configs.values():
if hasattr(app_config, "get_payment_variants"): if hasattr(app_config, "get_payment_variants"):
variants = app_config.get_payment_variants() try:
variants = app_config.get_payment_variants()
except OperationalError:
# Non-fatal, database is not yet ready
continue
for name, config in variants.items(): for name, config in variants.items():
if name not in settings.PAYMENT_VARIANTS: if name not in settings.PAYMENT_VARIANTS:
settings.PAYMENT_VARIANTS[name] = config settings.PAYMENT_VARIANTS[name] = config
...@@ -31,30 +40,37 @@ class DefaultConfig(AppConfig): ...@@ -31,30 +40,37 @@ class DefaultConfig(AppConfig):
variants = {} variants = {}
if prefs["payments__sofort_api_id"]: if prefs["payments__sofort_api_id"]:
variants["sofort"] = ("payments.sofort.SofortProvider", { variants["sofort"] = (
"id": prefs["payments__sofort_api_id"], "payments.sofort.SofortProvider",
"key": prefs["payments__sofort_api_key"], {
"project_id": prefs["payments__sofort_project_id"], "id": prefs["payments__sofort_api_id"],
"endpoint": "https://api.sofort.com/api/xml", "key": prefs["payments__sofort_api_key"],
}) "project_id": prefs["payments__sofort_project_id"],
"endpoint": "https://api.sofort.com/api/xml",
},
)
if prefs["payments__paypal_client_id"]: if prefs["payments__paypal_client_id"]:
variants["paypal"] = ("payments.paypal.PaypalProvider", { variants["paypal"] = (
"client_id": prefs["payments__paypal_client_id"], "payments.paypal.PaypalProvider",
"secret": prefs["payments__paypal_secret"], {
"capture": not prefs["payments__paypal_capture"], "client_id": prefs["payments__paypal_client_id"],
"endpoint": "https://api.paypal.com", "secret": prefs["payments__paypal_secret"],
}) "capture": not prefs["payments__paypal_capture"],
"endpoint": "https://api.paypal.com",
},
)
if prefs["payments__pledge_enabled"]: if prefs["payments__pledge_enabled"]:
variants["pledge"] = ("djp_sepa.providers.PaymentPledgeProvider", { variants["pledge"] = ("djp_sepa.providers.PaymentPledgeProvider", {})
})
if prefs["payments__sdd_creditor_identifier"]: if prefs["payments__sdd_creditor_identifier"]:
variants["sdd"] = ("djp_sepa.providers.DirectDebitProvider", { variants["sdd"] = (
"creditor": prefs["payments__sdd_creditor"], "djp_sepa.providers.DirectDebitProvider",
"creditor_identifier": prefs["payments__sdd_creditor_identifier"], {
}) "creditor": prefs["payments__sdd_creditor"],
"creditor_identifier": prefs["payments__sdd_creditor_identifier"],
},
)
return variants return variants
...@@ -5,6 +5,7 @@ from aleksis.core.mixins import ExtensibleForm ...@@ -5,6 +5,7 @@ from aleksis.core.mixins import ExtensibleForm
from .models.base import Client from .models.base import Client
from .models.invoice import InvoiceGroup from .models.invoice import InvoiceGroup
class EditClientForm(ExtensibleForm): class EditClientForm(ExtensibleForm):
"""Form to create or edit clients.""" """Form to create or edit clients."""
...@@ -17,9 +18,7 @@ class EditClientForm(ExtensibleForm): ...@@ -17,9 +18,7 @@ class EditClientForm(ExtensibleForm):
class EditInvoiceGroupForm(ExtensibleForm): class EditInvoiceGroupForm(ExtensibleForm):
layout = Layout( layout = Layout(Row("name", "template_name"))
Row("name", "template_name")
)
class Meta: class Meta:
model = InvoiceGroup model = InvoiceGroup
......
...@@ -19,15 +19,20 @@ from .base import Client ...@@ -19,15 +19,20 @@ from .base import Client
class InvoiceGroup(ExtensibleModel): class InvoiceGroup(ExtensibleModel):
name = models.CharField(verbose_name=_("Invoice group name"), max_length=255) name = models.CharField(verbose_name=_("Invoice group name"), max_length=255)
client = models.ForeignKey( client = models.ForeignKey(
Client, verbose_name=_("Linked client"), related_name="invoice_groups", on_delete=models.SET_NULL, null=True Client,
verbose_name=_("Linked client"),
related_name="invoice_groups",
on_delete=models.SET_NULL,
null=True,
) )
template_name = models.CharField(verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255) template_name = models.CharField(
verbose_name=_("Template to render invoices with as PDF"), blank=True, max_length=255
)
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client") models.UniqueConstraint(fields=["client", "name"], name="group_uniq_per_client")
...@@ -52,7 +57,11 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -52,7 +57,11 @@ class Invoice(BasePayment, PureDjangoModel):
} }
group = models.ForeignKey( group = models.ForeignKey(
InvoiceGroup, verbose_name=_("Invoice group"), related_name="invoices", on_delete=models.SET_NULL, null=True InvoiceGroup,
verbose_name=_("Invoice group"),
related_name="invoices",
on_delete=models.SET_NULL,
null=True,
) )
number = models.CharField(verbose_name=_("Invoice number"), max_length=255) number = models.CharField(verbose_name=_("Invoice number"), max_length=255)
...@@ -63,7 +72,13 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -63,7 +72,13 @@ class Invoice(BasePayment, PureDjangoModel):
for_object = GenericForeignKey("for_content_type", "for_object_id") for_object = GenericForeignKey("for_content_type", "for_object_id")
# For manual invoicing # For manual invoicing
person = models.ForeignKey(Person, on_delete=models.SET_NULL, verbose_name=_("Invoice recipient (person)"), blank=True, null=True) person = models.ForeignKey(
Person,
on_delete=models.SET_NULL,
verbose_name=_("Invoice recipient (person)"),
blank=True,
null=True,
)
items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items")) items = models.ManyToManyField("InvoiceItem", verbose_name=_("Invoice items"))
@classmethod @classmethod
...@@ -101,7 +116,10 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -101,7 +116,10 @@ class Invoice(BasePayment, PureDjangoModel):
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"), models.UniqueConstraint(fields=["number", "group"], name="number_uniq_per_group"),
models.CheckConstraint(check=(Q(for_object_id__isnull=True) | Q(person__isnull=True)), name="object_or_person"), models.CheckConstraint(
check=(Q(for_object_id__isnull=True) | Q(person__isnull=True)),
name="object_or_person",
),
] ]
def get_billing_email_recipients(self): def get_billing_email_recipients(self):
...@@ -124,17 +142,21 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -124,17 +142,21 @@ class Invoice(BasePayment, PureDjangoModel):
values = [] values = []
for tax_rate, total in tax_amounts.items(): for tax_rate, total in tax_amounts.items():
values.append({ values.append(
"name": _("Included VAT {} %").format(tax_rate), {
"value": total, "name": _("Included VAT {} %").format(tax_rate),
"value": total,
"currency": self.currency,
}
)
values.append(
{
"name": _("Gross total"),
"value": self.total,
"currency": self.currency, "currency": self.currency,
}) }
)
values.append({
"name": _("Gross total"),
"value": self.total,
"currency": self.currency,
})
return TotalsTable(values) return TotalsTable(values)
...@@ -148,9 +170,20 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -148,9 +170,20 @@ class Invoice(BasePayment, PureDjangoModel):
class InvoiceItem(ExtensibleModel): class InvoiceItem(ExtensibleModel):
sku = models.CharField(max_length=255, verbose_name=_("Article no."), blank=True) sku = models.CharField(max_length=255, verbose_name=_("Article no."), blank=True)
description = models.CharField(max_length=255, verbose_name=_("Purchased item")) description = models.CharField(max_length=255, verbose_name=_("Purchased item"))
price = models.DecimalField(verbose_name=_("Item gross price"), max_digits=9, decimal_places=2, default="0.0") price = models.DecimalField(
verbose_name=_("Item gross price"), max_digits=9, decimal_places=2, default="0.0"
)
currency = models.CharField(max_length=10, verbose_name=_("Currency")) currency = models.CharField(max_length=10, verbose_name=_("Currency"))
tax_rate = models.DecimalField(verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0") tax_rate = models.DecimalField(
verbose_name=_("Tax rate"), max_digits=4, decimal_places=1, default="0.0"
)
def as_purchased_item(self): def as_purchased_item(self):
yield PurchasedItem(name=self.description, quantity=1, price=self.price, currency=self.currency, sku=self.sku, tax_rate=self.tax_rate) yield PurchasedItem(
name=self.description,
quantity=1,
price=self.price,
currency=self.currency,
sku=self.sku,
tax_rate=self.tax_rate,
)
...@@ -6,16 +6,19 @@ from .models.invoice import Invoice ...@@ -6,16 +6,19 @@ from .models.invoice import Invoice
User = get_user_model() User = get_user_model()
@predicate @predicate
def is_own_invoice(user: User, obj: Invoice): def is_own_invoice(user: User, obj: Invoice):
"""Predicate which checks if the invoice is linked to the current user.""" """Predicate which checks if the invoice is linked to the current user."""
return obj.get_person() == user.person return obj.get_person() == user.person
@predicate @predicate
def has_no_payment_variant(user: User, obj: Invoice): def has_no_payment_variant(user: User, obj: Invoice):
"""Predicate which checks that the invoice has no payment variant.""" """Predicate which checks that the invoice has no payment variant."""
return not obj.variant return not obj.variant
def is_in_payment_status(status: str): def is_in_payment_status(status: str):
"""Predicate which checks whether the invoice is in a specific state.""" """Predicate which checks whether the invoice is in a specific state."""
......
...@@ -15,7 +15,9 @@ class EnablePledge(BooleanPreference): ...@@ -15,7 +15,9 @@ class EnablePledge(BooleanPreference):
section = payments section = payments
name = "public_payments" name = "public_payments"
verbose_name = _("Public payments") verbose_name = _("Public payments")
help_text = _("Allow anyone (including guests) to make payments. Basic invoice information will be visible to anyone who knows the invoice token.") help_text = _(
"Allow anyone (including guests) to make payments. Basic invoice information will be visible to anyone who knows the invoice token."
)
default = True default = True
required = False required = False
......
import rules import rules
from payments import PaymentStatus from payments import PaymentStatus
from aleksis.core.util.predicates import has_person, has_global_perm, has_any_object, has_object_perm, is_site_preference_set from aleksis.core.util.predicates import (
has_any_object,
has_global_perm,
has_object_perm,
has_person,
is_site_preference_set,
)
from .models.base import Client from .models.base import Client
from .models.invoice import Invoice, InvoiceGroup from .models.invoice import Invoice, InvoiceGroup
from .predicates import has_no_payment_variant, is_own_invoice, is_in_payment_status from .predicates import has_no_payment_variant, is_in_payment_status, is_own_invoice
# View clients # View clients
view_clients_predicate = has_person & ( view_clients_predicate = has_person & (
...@@ -39,7 +45,8 @@ rules.add_perm("tezor.delete_client_rule", delete_client_predicate) ...@@ -39,7 +45,8 @@ rules.add_perm("tezor.delete_client_rule", delete_client_predicate)
# View invoice groups # View invoice groups
view_invoice_groups_predicate = has_person & ( view_invoice_groups_predicate = has_person & (
has_global_perm("tezor.view_invoice_group") | has_any_object("tezor.view_invoice_group", InvoiceGroup) has_global_perm("tezor.view_invoice_group")
| has_any_object("tezor.view_invoice_group", InvoiceGroup)
) )
rules.add_perm("tezor.view_invoice_groups_rule", view_invoice_groups_predicate) rules.add_perm("tezor.view_invoice_groups_rule", view_invoice_groups_predicate)
...@@ -57,35 +64,73 @@ rules.add_perm("tezor.edit_invoice_group_rule", edit_invoice_group_predicate) ...@@ -57,35 +64,73 @@ rules.add_perm("tezor.edit_invoice_group_rule", edit_invoice_group_predicate)
# Create invoice groups # Create invoice groups
create_invoice_groups_predicate = has_person & ( create_invoice_groups_predicate = has_person & (
has_global_perm("tezor.create_invoice_group") | has_any_object("tezor.create_invoice_group", InvoiceGroup) has_global_perm("tezor.create_invoice_group")
| has_any_object("tezor.create_invoice_group", InvoiceGroup)
) )
rules.add_perm("tezor.create_invoice_groups_rule", create_invoice_groups_predicate) rules.add_perm("tezor.create_invoice_groups_rule", create_invoice_groups_predicate)
# Delete invoice groups # Delete invoice groups
delete_invoice_groups_predicate = has_person & ( delete_invoice_groups_predicate = has_person & (
has_global_perm("tezor.delete_invoice_group") | has_any_object("tezor.delete_invoice_group", InvoiceGroup) has_global_perm("tezor.delete_invoice_group")
| has_any_object("tezor.delete_invoice_group", InvoiceGroup)
) )
rules.add_perm("tezor.delete_invoice_groups_rule", delete_invoice_groups_predicate) rules.add_perm("tezor.delete_invoice_groups_rule", delete_invoice_groups_predicate)
# Display invoice billing information # Display invoice billing information
display_billing_predicate = has_person & (is_own_invoice | has_global_perm("tezor.display_billing") | has_object_perm("tezor.display_billing")) display_billing_predicate = has_person & (
is_own_invoice
| has_global_perm("tezor.display_billing")
| has_object_perm("tezor.display_billing")
)
rules.add_perm("tezor.display_billing_rule", display_billing_predicate) rules.add_perm("tezor.display_billing_rule", display_billing_predicate)
# Display invoice purchased items # Display invoice purchased items
display_purchased_items_predicate = has_person & (is_own_invoice | has_global_perm("tezor.display_purchased_items") | has_object_perm("tezor.display_purchased_items")) display_purchased_items_predicate = has_person & (
is_own_invoice
| has_global_perm("tezor.display_purchased_items")
| has_object_perm("tezor.display_purchased_items")
)
rules.add_perm("tezor.display_purchased_items_rule", display_purchased_items_predicate) rules.add_perm("tezor.display_purchased_items_rule", display_purchased_items_predicate)
# Change payment variant # Change payment variant
change_payment_variant_predicate = has_person & is_in_payment_status(PaymentStatus.WAITING) & ((is_own_invoice & has_no_payment_variant) | has_global_perm("tezor.change_payment_variant") | has_object_perm("tezor.change_payment_variant")) change_payment_variant_predicate = (
has_person
& is_in_payment_status(PaymentStatus.WAITING)
& (
(is_own_invoice & has_no_payment_variant)
| has_global_perm("tezor.change_payment_variant")
| has_object_perm("tezor.change_payment_variant")
)
)
rules.add_perm("tezor.change_payment_variant", change_payment_variant_predicate) rules.add_perm("tezor.change_payment_variant", change_payment_variant_predicate)
# Start payment # Start payment
do_payment_predicate = has_person & (is_in_payment_status(PaymentStatus.WAITING) | is_in_payment_status(PaymentStatus.INPUT) | is_in_payment_status(PaymentStatus.ERROR) | is_in_payment_status(PaymentStatus.REJECTED)) & ((is_own_invoice | is_site_preference_set("payments", "public_payments")) | has_global_perm("tezor.do_payment") | has_object_perm("tezor.do_payment")) do_payment_predicate = (
has_person
& (
is_in_payment_status(PaymentStatus.WAITING)
| is_in_payment_status(PaymentStatus.INPUT)
| is_in_payment_status(PaymentStatus.ERROR)
| is_in_payment_status(PaymentStatus.REJECTED)
)
& (
(is_own_invoice | is_site_preference_set("payments", "public_payments"))
| has_global_perm("tezor.do_payment")
| has_object_perm("tezor.do_payment")
)
)
rules.add_perm("tezor.do_payment", do_payment_predicate) rules.add_perm("tezor.do_payment", do_payment_predicate)
# View invoice # View invoice
view_invoice_predicate = has_person & is_own_invoice | is_site_preference_set("payments", "public_payments") | has_global_perm("tezor.view_invoice") | has_object_perm("tezor.view_invoice") view_invoice_predicate = (
has_person & is_own_invoice
| is_site_preference_set("payments", "public_payments")
| has_global_perm("tezor.view_invoice")
| has_object_perm("tezor.view_invoice")
)
rules.add_perm("tezor.view_invoice_rule", view_invoice_predicate) rules.add_perm("tezor.view_invoice_rule", view_invoice_predicate)
print_invoice_predicate = (view_invoice_predicate & display_billing_predicate & display_purchased_items_predicate) print_invoice_predicate = (
view_invoice_predicate & display_billing_predicate & display_purchased_items_predicate
)
rules.add_perm("tezor.print_invoice_rule", print_invoice_predicate) rules.add_perm("tezor.print_invoice_rule", print_invoice_predicate)
...@@ -60,6 +60,7 @@ class ClientsTable(tables.Table): ...@@ -60,6 +60,7 @@ class ClientsTable(tables.Table):
text=_("Delete"), text=_("Delete"),
) )
class InvoiceGroupsTable(tables.Table): class InvoiceGroupsTable(tables.Table):
name = tables.Column() name = tables.Column()
...@@ -83,6 +84,7 @@ class InvoiceGroupsTable(tables.Table): ...@@ -83,6 +84,7 @@ class InvoiceGroupsTable(tables.Table):
text=_("Delete"), text=_("Delete"),
) )
class InvoicesTable(tables.Table): class InvoicesTable(tables.Table):
number = tables.Column() number = tables.Column()
......
from django.shortcuts import redirect, render, get_object_or_404 from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, TemplateView, View
from django.views.generic.detail import DetailView
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, TemplateView, View
from django.views.generic.detail import DetailView
from payments import get_payment_model, PaymentStatus, RedirectNeeded from django_tables2.views import RequestConfig, SingleTableView
from payments import PaymentStatus, RedirectNeeded, get_payment_model
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from django_tables2.views import SingleTableView, RequestConfig from django_tables2.views import SingleTableView, RequestConfig
from templated_email import InlineImage, send_templated_mail from templated_email import InlineImage, send_templated_mail
from aleksis.core.views import RenderPDFView
from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
from aleksis.core.util.pdf import generate_pdf_from_template from aleksis.core.util.pdf import generate_pdf_from_template
from aleksis.core.views import RenderPDFView
from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable
from .forms import EditClientForm, EditInvoiceGroupForm from .forms import EditClientForm, EditInvoiceGroupForm
from .models.base import Client from .models.base import Client
from .models.invoice import Invoice, InvoiceGroup from .models.invoice import Invoice, InvoiceGroup
from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable
class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView): class GetInvoicePDF(PermissionRequiredMixin, RenderPDFView):
...@@ -44,7 +45,11 @@ class DoPaymentView(PermissionRequiredMixin, View): ...@@ -44,7 +45,11 @@ class DoPaymentView(PermissionRequiredMixin, View):
def dispatch(self, request, token): def dispatch(self, request, token):
self.object = get_object_or_404(self.model, token=token) self.object = get_object_or_404(self.model, token=token)
if self.object.status not in [PaymentStatus.WAITING, PaymentStatus.INPUT, PaymentStatus.REJECTED]: if self.object.status not in [
PaymentStatus.WAITING,
PaymentStatus.INPUT,
PaymentStatus.REJECTED,
]:
return redirect(self.object.get_success_url()) return redirect(self.object.get_success_url())
try: try:
...@@ -119,6 +124,7 @@ class ClientDetailView(PermissionRequiredMixin, DetailView): ...@@ -119,6 +124,7 @@ class ClientDetailView(PermissionRequiredMixin, DetailView):
return context return context
class InvoiceGroupDetailView(PermissionRequiredMixin, DetailView): class InvoiceGroupDetailView(PermissionRequiredMixin, DetailView):
model = InvoiceGroup model = InvoiceGroup
...@@ -147,13 +153,17 @@ class InvoiceGroupCreateView(PermissionRequiredMixin, AdvancedCreateView): ...@@ -147,13 +153,17 @@ class InvoiceGroupCreateView(PermissionRequiredMixin, AdvancedCreateView):
success_url = reverse_lazy("clients") success_url = reverse_lazy("clients")
success_message = _("The invoice_group has been created.") success_message = _("The invoice_group has been created.")
def form_valid(self, form): def form_valid(self, form):
client = Client.objects.get(id=self.kwargs["pk"]) 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"]) InvoiceGroup.objects.create(
client=client,
name=form.cleaned_data["name"],
template_name=form.cleaned_data["template_name"],
)
return super().form_valid(form) return super().form_valid(form)
@method_decorator(never_cache, name="dispatch") @method_decorator(never_cache, name="dispatch")
class InvoiceGroupEditView(PermissionRequiredMixin, AdvancedEditView): class InvoiceGroupEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for invoice_groups.""" """Edit view for invoice_groups."""
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment