Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/onboarding/AlekSIS-App-Tezor
  • sunweaver/AlekSIS-App-Tezor
  • 3lisvequii/AlekSIS-App-Tezor
3 results
Show changes
Commits on Source (4)
Showing with 256 additions and 153 deletions
...@@ -2,8 +2,8 @@ from django_filters import ChoiceFilter, FilterSet ...@@ -2,8 +2,8 @@ from django_filters import ChoiceFilter, FilterSet
from material import Layout, Row from material import Layout, Row
from payments import PaymentStatus from payments import PaymentStatus
from .models.base import PaymentVariant
from .models.invoice import Invoice from .models.invoice import Invoice
from .models.payment_variant import PaymentVariant
class InvoicesFilter(FilterSet): class InvoicesFilter(FilterSet):
......
...@@ -100,6 +100,52 @@ export default { ...@@ -100,6 +100,52 @@ export default {
}, },
name: "tezor.deleteInvoiceGroupByPk", name: "tezor.deleteInvoiceGroupByPk",
}, },
{
path: "payment_variants/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "tezor.payment_variants",
meta: {
inMenu: true,
titleKey: "tezor.payment_variants.menu_title",
icon: "mdi-cash-register",
permission: "tezor.view_paymentvariants_rule",
},
},
{
path: "payment_variant/create/:model/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "tezor.createPaymentVariant",
},
{
path: "payment_variant/:pk/edit/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "tezor.editPaymentVariantByPk",
},
{
path: "payment_variant/:pk/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "tezor.deletePaymentVariantByPk",
},
{
path: "payment_variant/:pk/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "tezor.paymentVariantByPk",
},
{ {
path: "invoices/my/", path: "invoices/my/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
......
{ {
"tezor": { "tezor": {
"menu_title": "Payments and Money", "menu_title": "Payments",
"clients": { "clients": {
"menu_title": "Manage clients" "menu_title": "Clients"
}, },
"personal_invoices": { "personal_invoices": {
"menu_title": "My invoices" "menu_title": "My Invoices"
},
"payment_variants": {
"menu_title": "Payment Variants"
} }
} }
} }
# Generated by Django 5.1.7 on 2025-03-10 10:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tezor', '0012_paymentvariant'),
]
operations = [
migrations.AddField(
model_name='invoiceitem',
name='quantity',
field=models.PositiveIntegerField(default=1, verbose_name='Quantity'),
),
]
from typing import ClassVar
from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.functional import classproperty
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from localflavor.generic.models import BICField, IBANField from aleksis.core.mixins import ExtensibleModel
from payments.core import BasicProvider
from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel, RegistryObject
class Client(ExtensibleModel): class Client(ExtensibleModel):
name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True) name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True)
email = models.EmailField(verbose_name=_("Email")) email = models.EmailField(verbose_name=_("Email"))
def get_payment_variant_choices(self):
return [(v.name, v.verbose_name) for v in self.payment_variants.all()]
def __str__(self): def __str__(self):
return self.name return self.name
class Meta: class Meta:
verbose_name = _("Client") verbose_name = _("Client")
verbose_name_plural = _("Clients") verbose_name_plural = _("Clients")
class PaymentVariant(RegistryObject, ExtensiblePolymorphicModel):
"""A single, configured payment variant."""
icon: ClassVar[str] = None
verbose_name: ClassVar[str] = None
description = models.CharField(max_length=255, verbose_name=_("Description"))
client = models.ForeignKey(Client, on_delete=models.CASCADE, verbose_name=_("Client"), related_name="payment_variants")
def __str__(self) -> str:
return self.description
def get_provider(self) -> BasicProvider:
"""Get django-payments provider for this payment variant."""
raise NotImplementedError()
@classproperty
def as_choices(cls) -> list[tuple[str, str]]:
"""Get payment variants as choices."""
return [(variant.name, variant.verbose_name) for variant in cls.registered_objects_list]
class Meta:
verbose_name = _("Payment variant")
verbose_name_plural = _("Payment variants")
class SofortPaymentVariant(PaymentVariant):
icon = "simple-icons:klarna"
verbose_name = _("Klarna/Sofort")
name = "sofort"
api_id = models.CharField(verbose_name=_("API ID"), max_length=255)
api_key = models.CharField(verbose_name=_("API key"), max_length=255)
project_id = models.CharField(verbose_name=_("Project ID"), max_length=255)
def get_provider(self):
from payments.sofort import SofortProvider # noqa
provider = SofortProvider(
key=self.api_key, id=self.api_sid, project_id=self.project_id
)
return provider
class Meta:
verbose_name = _("Sofort/Klarna payment variant")
verbose_name_plural = _("Sofort/Klarna payment variants")
class PaypalPaymentVariant(PaymentVariant):
icon = "logos:paypal"
verbose_name = _("PayPal")
name = "paypal"
api_client_id = models.CharField(verbose_name=_("Client ID"), max_length=255)
secret = models.CharField(verbose_name=_("Secret"), max_length=255)
capture = models.BooleanField(
verbose_name=_("Use PayPal Authorize & Capture"), default=False
)
endpoint = models.URLField(verbose_name=_("Endpoint"), default="https://api.paypal.com")
def get_provider(self):
from payments.paypal import PaypalProvider # noqa
provider = PaypalProvider(
client_id=self.client_id,
secret=self.secret,
capture=self.capture,
endpoint=self.endpoint,
)
return provider
class Meta:
verbose_name = _("PayPal payment variant")
verbose_name_plural = _("PayPal payment variants")
class SEPADirectDebitPaymentVariant(PaymentVariant):
icon = "mdi:bank-transfer"
verbose_name = _("SEPA Direct Debit")
name = "sdd"
creditor = models.CharField(verbose_name=_("Creditor name"), max_length=255)
creditor_identifier = models.CharField(
verbose_name=_("Creditor identifier"),
max_length=35,
validators=[RegexValidator("^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,31}$")],
)
iban = IBANField(verbose_name=_("IBAN of bank account"))
bic = BICField(verbose_name=_("BIC/SWIFT code of bank"))
def get_provider(self):
from djp_sepa.providers import DirectDebitProvider # noqa
provider = DirectDebitProvider(
creditor=self.creditor,
creditor_identifier=self.creditor_identifier,
iban=self.iban,
bic=self.bic,
)
return provider
class Meta:
verbose_name = _("SEPA Direct Debit payment variant")
verbose_name_plural = _("SEPA Direct Debit payment variants")
class PledgePaymentVariant(PaymentVariant):
icon = "mdi:hand-coin"
verbose_name = _("Payment pledge/Manual payment")
name = "pledge"
def get_provider(self):
from djp_sepa.providers import PaymentPledgeProvider # noqa
provider = PaymentPledgeProvider()
return provider
class Meta:
verbose_name = _("Pledge payment variant")
verbose_name_plural = _("Pledge payment variants")
from collections.abc import Iterator
from decimal import Decimal
from typing import NamedTuple
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from payments import PaymentStatus, PurchasedItem from payments import PaymentStatus
from payments.models import BasePayment from payments.models import BasePayment
from aleksis.core.mixins import ExtensibleModel, PureDjangoModel from aleksis.core.mixins import ExtensibleModel, PureDjangoModel
from aleksis.core.models import Person from aleksis.core.models import Person
from ..tables import PurchasedItemsTable, TotalsTable from ..tables import PurchasedItemsTable, TotalsTable
from .base import Client, PaymentVariant from .base import Client
from .payment_variant import PaymentVariant
class CustomPurchasedItem(NamedTuple):
"""A single item in a purchase."""
name: str
quantity: int
unit_price: Decimal
price: Decimal
currency: str
sku: str
tax_rate: Decimal | None = None
class InvoiceGroup(ExtensibleModel): class InvoiceGroup(ExtensibleModel):
...@@ -110,7 +127,7 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -110,7 +127,7 @@ class Invoice(BasePayment, PureDjangoModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_variant(self): def get_variant(self):
variants = [v for v in self.group.client.payment_variants.all() if v.label == self.variant] variants = [v for v in self.group.client.payment_variants.all() if v.name == self.variant]
return variants[0] if variants else None return variants[0] if variants else None
def get_absolute_url(self): def get_absolute_url(self):
...@@ -125,7 +142,7 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -125,7 +142,7 @@ class Invoice(BasePayment, PureDjangoModel):
def get_status_icon(self): def get_status_icon(self):
return self.STATUS_ICONS[self.status] return self.STATUS_ICONS[self.status]
def get_purchased_items(self): def get_purchased_items(self) -> Iterator[CustomPurchasedItem]:
if self.items.count(): if self.items.count():
for item in self.items.all(): for item in self.items.all():
yield item.as_purchased_item() yield item.as_purchased_item()
...@@ -192,6 +209,7 @@ class Invoice(BasePayment, PureDjangoModel): ...@@ -192,6 +209,7 @@ 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"))
quantity = models.PositiveIntegerField(verbose_name=_("Quantity"), default=1)
price = models.DecimalField( price = models.DecimalField(
verbose_name=_("Item net price"), max_digits=9, decimal_places=2, default="0.0" verbose_name=_("Item net price"), max_digits=9, decimal_places=2, default="0.0"
) )
...@@ -207,11 +225,16 @@ class InvoiceItem(ExtensibleModel): ...@@ -207,11 +225,16 @@ class InvoiceItem(ExtensibleModel):
def __str__(self): def __str__(self):
return f"{self.sku}: {self.description}" return f"{self.sku}: {self.description}"
def as_purchased_item(self): @property
return PurchasedItem( def total_price(self):
return self.price * self.quantity
def as_purchased_item(self) -> CustomPurchasedItem:
return CustomPurchasedItem(
name=self.description, name=self.description,
quantity=1, quantity=self.quantity,
price=self.price, unit_price=self.price,
price=self.total_price,
currency=self.currency, currency=self.currency,
sku=self.sku, sku=self.sku,
tax_rate=self.tax_rate, tax_rate=self.tax_rate,
......
from typing import ClassVar
from django.core.validators import RegexValidator
from django.db import models
from django.utils.functional import classproperty
from django.utils.translation import gettext_lazy as _
from localflavor.generic.models import BICField, IBANField
from payments.core import BasicProvider
from aleksis.core.mixins import ExtensiblePolymorphicModel, RegistryObject
from .base import Client
class PaymentVariant(RegistryObject, ExtensiblePolymorphicModel):
"""A single, configured payment variant."""
icon: ClassVar[str] = None
verbose_name: ClassVar[str] = None
description = models.CharField(max_length=255, verbose_name=_("Description"))
client = models.ForeignKey(
Client, on_delete=models.CASCADE, verbose_name=_("Client"), related_name="payment_variants"
)
def __str__(self) -> str:
return self.description
def get_provider(self) -> BasicProvider:
"""Get django-payments provider for this payment variant."""
raise NotImplementedError()
@classproperty
def as_choices(cls) -> list[tuple[str, str]]:
"""Get payment variants as choices."""
return [(variant.name, variant.verbose_name) for variant in cls.registered_objects_list]
class Meta:
verbose_name = _("Payment variant")
verbose_name_plural = _("Payment variants")
class SofortPaymentVariant(PaymentVariant):
icon = "simple-icons:klarna"
verbose_name = _("Klarna/Sofort")
name = "sofort"
api_id = models.CharField(verbose_name=_("API ID"), max_length=255)
api_key = models.CharField(verbose_name=_("API key"), max_length=255)
project_id = models.CharField(verbose_name=_("Project ID"), max_length=255)
def get_provider(self):
from payments.sofort import SofortProvider # noqa
provider = SofortProvider(key=self.api_key, id=self.api_sid, project_id=self.project_id)
return provider
class Meta:
verbose_name = _("Sofort/Klarna payment variant")
verbose_name_plural = _("Sofort/Klarna payment variants")
class PaypalPaymentVariant(PaymentVariant):
icon = "logos:paypal"
verbose_name = _("PayPal")
name = "paypal"
api_client_id = models.CharField(verbose_name=_("Client ID"), max_length=255)
secret = models.CharField(verbose_name=_("Secret"), max_length=255)
capture = models.BooleanField(verbose_name=_("Use PayPal Authorize & Capture"), default=False)
endpoint = models.URLField(verbose_name=_("Endpoint"), default="https://api.paypal.com")
def get_provider(self):
from payments.paypal import PaypalProvider # noqa
provider = PaypalProvider(
client_id=self.client_id,
secret=self.secret,
capture=self.capture,
endpoint=self.endpoint,
)
return provider
class Meta:
verbose_name = _("PayPal payment variant")
verbose_name_plural = _("PayPal payment variants")
class SEPADirectDebitPaymentVariant(PaymentVariant):
icon = "mdi:bank-transfer"
verbose_name = _("SEPA Direct Debit")
name = "sdd"
creditor = models.CharField(verbose_name=_("Creditor name"), max_length=255)
creditor_identifier = models.CharField(
verbose_name=_("Creditor identifier"),
max_length=35,
validators=[RegexValidator("^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,31}$")],
)
iban = IBANField(verbose_name=_("IBAN of bank account"))
bic = BICField(verbose_name=_("BIC/SWIFT code of bank"))
def get_provider(self):
from djp_sepa.providers import DirectDebitProvider # noqa
provider = DirectDebitProvider(
creditor=self.creditor,
creditor_identifier=self.creditor_identifier,
iban=self.iban,
bic=self.bic,
)
return provider
class Meta:
verbose_name = _("SEPA Direct Debit payment variant")
verbose_name_plural = _("SEPA Direct Debit payment variants")
class PledgePaymentVariant(PaymentVariant):
icon = "mdi:hand-coin"
verbose_name = _("Payment pledge/Manual payment")
name = "pledge"
def get_provider(self):
from djp_sepa.providers import PaymentPledgeProvider # noqa
provider = PaymentPledgeProvider()
return provider
class Meta:
verbose_name = _("Pledge payment variant")
verbose_name_plural = _("Pledge payment variants")
...@@ -9,8 +9,9 @@ from aleksis.core.util.predicates import ( ...@@ -9,8 +9,9 @@ from aleksis.core.util.predicates import (
is_site_preference_set, is_site_preference_set,
) )
from .models.base import Client, PaymentVariant from .models.base import Client
from .models.invoice import InvoiceGroup from .models.invoice import InvoiceGroup
from .models.payment_variant import PaymentVariant
from .predicates import ( from .predicates import (
has_no_payment_variant, has_no_payment_variant,
has_payment_variant, has_payment_variant,
......
...@@ -15,6 +15,11 @@ class PurchasedItemsTable(tables.Table): ...@@ -15,6 +15,11 @@ class PurchasedItemsTable(tables.Table):
attrs={"td": {"class": "right-align"}}, attrs={"td": {"class": "right-align"}},
) )
quantity = tables.Column(verbose_name=_("Qty."), attrs={"td": {"class": "right-align"}}) quantity = tables.Column(verbose_name=_("Qty."), attrs={"td": {"class": "right-align"}})
unit_price = tables.TemplateColumn(
verbose_name=_("Unit Net"),
template_code="{{value|floatformat:2}} {{record.currency}}",
attrs={"td": {"class": "right-align"}},
)
price = tables.TemplateColumn( price = tables.TemplateColumn(
verbose_name=_("Net"), verbose_name=_("Net"),
template_code="{{value|floatformat:2}} {{record.currency}}", template_code="{{value|floatformat:2}} {{record.currency}}",
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
{% block content %} {% block content %}
<a class="btn green waves-effect waves-light" href="{% url 'create_client' %}"> <a class="btn green waves-effect waves-light" href="{% url 'create_client' %}">
<i class="material-icons left">add</i> <i class="material-icons left iconify" data-icon="mdi:add"></i>
{% trans "Create client" %} {% trans "Create client" %}
</a> </a>
......
...@@ -75,8 +75,8 @@ ...@@ -75,8 +75,8 @@
</td> </td>
<td> <td>
<select name="variant" {% if not can_change_variant %}disabled{% endif %}> <select name="variant" {% if not can_change_variant %}disabled{% endif %}>
{% for choice in object.group.client.payment_variants.all %} {% for payment_variant in object.group.client.payment_variants.all %}
<option value="{{ choice.label }}" {% if object.variant == choice.label %}selected{% endif %}>{{ choice.name }}</option> <option value="{{ payment_variant.name }}" {% if object.variant == payment_variant.name %}selected{% endif %}>{{ payment_variant.verbose_name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
......
...@@ -74,17 +74,17 @@ urlpatterns = [ ...@@ -74,17 +74,17 @@ urlpatterns = [
name="payment_variants", name="payment_variants",
), ),
path( path(
"payment_variants/create/<str:model>/", "payment_variant/create/<str:model>/",
views.PaymentVariantCreateView.as_view(), views.PaymentVariantCreateView.as_view(),
name="create_payment_variant", name="create_payment_variant",
), ),
path( path(
"payment_variants/<int:pk>/edit/", "payment_variant/<int:pk>/edit/",
views.PaymentVariantEditView.as_view(), views.PaymentVariantEditView.as_view(),
name="edit_payment_variant_by_pk", name="edit_payment_variant_by_pk",
), ),
path( path(
"payment_variants/<int:pk>/delete/", "payment_variant/<int:pk>/delete/",
views.PaymentVariantDeleteView.as_view(), views.PaymentVariantDeleteView.as_view(),
name="delete_payment_variant_by_pk", name="delete_payment_variant_by_pk",
), ),
......
...@@ -20,8 +20,9 @@ from aleksis.core.views import RenderPDFView ...@@ -20,8 +20,9 @@ from aleksis.core.views import RenderPDFView
from .filters import InvoicesFilter from .filters import InvoicesFilter
from .forms import EditClientForm, EditInvoiceGroupForm, InvoicesActionForm from .forms import EditClientForm, EditInvoiceGroupForm, InvoicesActionForm
from .models.base import Client, PaymentVariant from .models.base import Client
from .models.invoice import Invoice, InvoiceGroup from .models.invoice import Invoice, InvoiceGroup
from .models.payment_variant import PaymentVariant
from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable, PaymentVariantsTable from .tables import ClientsTable, InvoiceGroupsTable, InvoicesTable, PaymentVariantsTable
from .tasks import email_invoice from .tasks import email_invoice
...@@ -53,7 +54,7 @@ class DoPaymentView(PermissionRequiredMixin, View): ...@@ -53,7 +54,7 @@ class DoPaymentView(PermissionRequiredMixin, View):
self.object = get_object_or_404(self.model, token=token) self.object = get_object_or_404(self.model, token=token)
allowed_variants = [ allowed_variants = [
variant.label for variant in self.object.group.client.payment_variants.all() variant.name for variant in self.object.group.client.payment_variants.all()
] ]
new_variant = request.GET.get("variant", None) new_variant = request.GET.get("variant", None)
......
...@@ -34,7 +34,7 @@ url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple" ...@@ -34,7 +34,7 @@ url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
priority = "supplemental" priority = "supplemental"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
django-payments-sepa = { version = "^2.0.0.dev0", allow-prereleases = true, extras = ["fints"] } django-payments-sepa = { version = "2.0.0.dev0", allow-prereleases = true, extras = ["fints"] }
aleksis-core = "^4.0.0.dev16" aleksis-core = "^4.0.0.dev16"
django-payments = { version = "^3.0.0", extras = ["sofort"] } django-payments = { version = "^3.0.0", extras = ["sofort"] }
......