From 768ef5a7d200ba2bf5fcf6a1c583b3ca4a1afc5d Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Fri, 26 Jul 2024 00:57:22 +0200 Subject: [PATCH] Add new admins, rework relation between orders and memberships, MembershipType can contain many Products, Memberships can be activated and revoked --- src/accounting/admin.py | 15 +++++ src/accounting/apps.py | 4 ++ ...06_alter_account_owner_alter_order_user.py | 2 +- src/accounting/models.py | 5 ++ src/accounting/signals.py | 21 +++++++ ...ivated_membership_activated_on_and_more.py | 55 +++++++++++++++++++ ...shiptype_current_membershiptype_product.py | 26 --------- src/membership/models.py | 31 ++++++++++- 8 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 src/accounting/signals.py create mode 100644 src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py delete mode 100644 src/membership/migrations/0007_membershiptype_current_membershiptype_product.py diff --git a/src/accounting/admin.py b/src/accounting/admin.py index efdfb38..b8b311c 100644 --- a/src/accounting/admin.py +++ b/src/accounting/admin.py @@ -56,3 +56,18 @@ class PaymentAdmin(admin.ModelAdmin): def order_id(self, instance: models.Payment) -> int: """Return the ID of the order.""" return instance.order.id + + +@admin.register(models.Product) +class ProductAdmin(admin.ModelAdmin): # noqa: D101 + list_display = ("name", "price", "vat") + + +class TransactionInline(admin.TabularInline): # noqa: D101 + model = models.Transaction + + +@admin.register(models.Account) +class AccountAdmin(admin.ModelAdmin): # noqa: D101 + list_display = ("owner", "balance") + inlines = (TransactionInline,) diff --git a/src/accounting/apps.py b/src/accounting/apps.py index 296dae8..9193122 100644 --- a/src/accounting/apps.py +++ b/src/accounting/apps.py @@ -7,3 +7,7 @@ class AccountingConfig(AppConfig): """Accounting app config.""" name = "accounting" + + def ready(self) -> None: + """Implicitly connect a signal handlers decorated with @receiver.""" + from . import signals # noqa: F401 diff --git a/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py b/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py index 9bda0e6..cdaeaff 100644 --- a/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py +++ b/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('accounting', '0005_remove_order_price_remove_order_price_currency_and_more'), - ('membership', '0007_membershiptype_current_membershiptype_product'), + ('membership', '0006_waitinglistentry_alter_membership_options'), ] operations = [ diff --git a/src/accounting/models.py b/src/accounting/models.py index d4cd2b0..6629a2b 100644 --- a/src/accounting/models.py +++ b/src/accounting/models.py @@ -95,6 +95,11 @@ class Order(CreatedModifiedAbstract): """Return the total VAT of the order.""" return sum(order_product.vat for order_product in self.order_products) + @property + def total_with_vat(self) -> Money: + """Return the TOTAL amount WITH VAT.""" + return self.total + self.total_vat + @property def display_id(self) -> str: """Return an id for the order.""" diff --git a/src/accounting/signals.py b/src/accounting/signals.py new file mode 100644 index 0000000..c3bd259 --- /dev/null +++ b/src/accounting/signals.py @@ -0,0 +1,21 @@ +"""Loaded with the AppConfig.ready() method.""" + +from django.conf import settings +from django.core.mail import send_mail +from django.db.models.signals import post_save +from django.dispatch import receiver + +from . import models + + +# method for updating +@receiver(post_save, sender=models.Payment) +def check_total_amount(sender: models.Payment, instance: models.Payment, **kwargs: dict) -> None: # noqa: ARG001 + """Check that we receive Payments with the correct amount.""" + if instance.amount != instance.order.total_with_vat: + send_mail( + "Payment received: wrong amount", + f"Please check payment ID {instance.pk}", + settings.DEFAULT_FROM_EMAIL, + settings.ADMINS, + ) diff --git a/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py b/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py new file mode 100644 index 0000000..0c7c57d --- /dev/null +++ b/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 5.1b1 on 2024-07-25 22:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounting', '0007_rename_user_order_member'), + ('membership', '0006_waitinglistentry_alter_membership_options'), + ] + + operations = [ + migrations.AddField( + model_name='membership', + name='activated', + field=models.BooleanField(default=False, help_text='Membership was activated.', verbose_name='activated'), + ), + migrations.AddField( + model_name='membership', + name='activated_on', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='membership', + name='order', + field=models.ForeignKey(blank=True, help_text='The order filled in for paying this membership.', null=True, on_delete=django.db.models.deletion.PROTECT, to='accounting.order', verbose_name='order'), + ), + migrations.AddField( + model_name='membership', + name='revoked', + field=models.BooleanField(default=False, help_text='Membership has explicitly been revoked. Revoking a membership is not associated with regular expiration of the membership period.', verbose_name='revoked'), + ), + migrations.AddField( + model_name='membership', + name='revoked_on', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='membership', + name='revoked_reason', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='membershiptype', + name='active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='membershiptype', + name='products', + field=models.ManyToManyField(to='accounting.product'), + ), + ] diff --git a/src/membership/migrations/0007_membershiptype_current_membershiptype_product.py b/src/membership/migrations/0007_membershiptype_current_membershiptype_product.py deleted file mode 100644 index f7accaa..0000000 --- a/src/membership/migrations/0007_membershiptype_current_membershiptype_product.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.7 on 2024-07-21 14:12 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounting', '0004_paymenttype_product_payment_external_transaction_id_and_more'), - ('membership', '0006_waitinglistentry_alter_membership_options'), - ] - - operations = [ - migrations.AddField( - model_name='membershiptype', - name='current', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='membershiptype', - name='product', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounting.product'), - preserve_default=False, - ), - ] diff --git a/src/membership/models.py b/src/membership/models.py index 2d5e69d..e8cce22 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -84,7 +84,7 @@ class Membership(CreatedModifiedAbstract): def _current(self) -> Self: """Filter memberships for the current period.""" - return self.filter(period__period__contains=timezone.now()) + return self.filter(activated=True, revoked=False, period__period__contains=timezone.now()) def current(self) -> "Membership | None": """Get the current membership.""" @@ -116,6 +116,31 @@ class Membership(CreatedModifiedAbstract): on_delete=models.PROTECT, ) + order = models.ForeignKey( + "accounting.Order", + null=True, + blank=True, + verbose_name=_("order"), + help_text=_("The order filled in for paying this membership."), + on_delete=models.PROTECT, + ) + + activated = models.BooleanField( + default=False, verbose_name=_("activated"), help_text=_("Membership was activated.") + ) + activated_on = models.DateTimeField(null=True, blank=True) + + revoked = models.BooleanField( + default=False, + verbose_name=_("revoked"), + help_text=_( + "Membership has explicitly been revoked. Revoking a membership is not associated with regular expiration " + "of the membership period." + ), + ) + revoked_reason = models.TextField(blank=True) + revoked_on = models.DateTimeField(null=True, blank=True) + class Meta: verbose_name = _("membership") verbose_name_plural = _("memberships") @@ -133,9 +158,9 @@ class MembershipType(CreatedModifiedAbstract): name = models.CharField(verbose_name=_("name"), max_length=64) - product = models.ForeignKey("accounting.Product", on_delete=models.PROTECT) + products = models.ManyToManyField("accounting.Product") - current = models.BooleanField(default=False) + active = models.BooleanField(default=True) class Meta: verbose_name = _("membership type")