diff --git a/jdav_web/finance/admin.py b/jdav_web/finance/admin.py index c19e9ad..830ed0b 100644 --- a/jdav_web/finance/admin.py +++ b/jdav_web/finance/admin.py @@ -64,7 +64,7 @@ def decorate_statement_view(model, perm=None): @admin.register(StatementUnSubmitted) class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin): - fields = ['short_description', 'explanation', 'excursion', 'submitted'] + fields = ['short_description', 'explanation', 'excursion', 'status'] list_display = ['__str__', 'excursion', 'created_by'] inlines = [BillOnStatementInline] @@ -74,7 +74,7 @@ class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin): super().save_model(request, obj, form, change) def get_readonly_fields(self, request, obj=None): - readonly_fields = ['submitted', 'excursion'] + readonly_fields = ['status', 'excursion'] if obj is not None and obj.submitted: return readonly_fields + self.fields else: @@ -167,7 +167,7 @@ class BillOnSubmittedStatementInline(BillOnStatementInline): @admin.register(StatementSubmitted) class StatementSubmittedAdmin(admin.ModelAdmin): - fields = ['short_description', 'explanation', 'excursion', 'submitted'] + fields = ['short_description', 'explanation', 'excursion', 'status'] list_display = ['__str__', 'is_valid', 'submitted_date', 'submitted_by'] ordering = ('-submitted_date',) inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline] @@ -186,7 +186,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin): return False def get_readonly_fields(self, request, obj=None): - readonly_fields = ['submitted'] + readonly_fields = ['status'] if obj is not None and obj.submitted: return readonly_fields + self.fields else: @@ -274,7 +274,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin): return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) if "reject" in request.POST: - statement.submitted = False + statement.status = Statement.UNSUBMITTED statement.save() messages.success(request, _("Successfully rejected %(name)s. The requestor can reapply, when needed.") @@ -315,7 +315,7 @@ class StatementSubmittedAdmin(admin.ModelAdmin): @admin.register(StatementConfirmed) class StatementConfirmedAdmin(admin.ModelAdmin): - fields = ['short_description', 'explanation', 'excursion', 'confirmed'] + fields = ['short_description', 'explanation', 'excursion', 'status'] #readonly_fields = fields list_display = ['__str__', 'total_pretty', 'confirmed_date', 'confirmed_by'] ordering = ('-confirmed_date',) @@ -365,7 +365,7 @@ class StatementConfirmedAdmin(admin.ModelAdmin): _("%(name)s is not yet confirmed.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) if "unconfirm" in request.POST: - statement.confirmed = False + statement.status = Statement.SUBMITTED statement.confirmed_date = None statement.confired_by = None statement.save() diff --git a/jdav_web/finance/locale/de/LC_MESSAGES/django.po b/jdav_web/finance/locale/de/LC_MESSAGES/django.po index 24fed2d..4895706 100644 --- a/jdav_web/finance/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/finance/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-03 19:06+0200\n" +"POT-Creation-Date: 2025-10-12 11:37+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Statement not found." msgstr "Abrechnung nicht gefunden." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Insufficient permissions." msgstr "Unzureichende Berechtigungen." @@ -31,7 +31,7 @@ msgstr "Unzureichende Berechtigungen." msgid "%(name)s is already submitted." msgstr "%(name)s ist bereits eingereicht." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "" "Successfully submited %(name)s. The finance department will notify the " @@ -40,11 +40,11 @@ msgstr "" "Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich " "sobald wie möglich zukommen." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Finance overview" msgstr "Kostenübersicht" -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Submit statement" msgstr "Rechnung einreichen" @@ -64,11 +64,11 @@ msgstr "" "Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es " "erneut." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Successfully sent receipt to the office." msgstr "Abrechnungsbeleg an die Geschäftsstelle gesendet." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "" "Successfully confirmed %(name)s. I hope you executed the associated " @@ -84,11 +84,11 @@ msgstr "" "Hier kannst du den Abrechnungsbeleg herunterladen." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Statement confirmed" msgstr "Abrechnung abgewickelt" -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "" "Transactions do not match the covered expenses. Please correct the mistakes " "listed below." @@ -96,12 +96,12 @@ msgstr "" "Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte " "korrigiere die unten aufgeführten Fehler." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "Some transactions have no ledger configured. Please fill in the gaps." msgstr "" "Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "" "The configured recipients for the allowance don't match the regulations. " "Please correct this on the excursion." @@ -117,14 +117,14 @@ msgstr "" "Der berechnete Gesamtbetrag stimmt nicht mit der Summe aller Überweisungen " "überein. Das ist höchstwahrscheinlich ein Fehler in der Implementierung." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." msgstr "" "Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut " "einstellen, wenn es benötigt wird." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "" "%(name)s already has transactions. Please delete them first, if you want to " @@ -133,12 +133,12 @@ msgstr "" "%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du " "neue generierst." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "Successfully generated transactions for %(name)s" msgstr "Automatisch Überweisungsträger für %(name)s generiert." -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "" "Error while generating transactions for %(name)s. Do all bills have a payer " @@ -150,11 +150,11 @@ msgstr "" "einer Ausfahrt gehört, wurde eine Person als Empfänger*in der Übernachtungs- " "und Fahrtkostenzuschüsse ausgewählt?" -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py msgid "View submitted statement" msgstr "Eingereichte Abrechnung einsehen" -#: finance/admin.py +#: finance/admin.py finance/tests/admin.py #, python-format msgid "Successfully reduced transactions for %(name)s." msgstr "Überweisungsträger für %(name)s minimiert." @@ -172,6 +172,7 @@ msgstr "" "was du machst." #: finance/admin.py finance/templates/admin/unconfirm_statement.html +#: finance/tests/admin.py msgid "Unconfirm statement" msgstr "Bestätigung zurücknehmen" @@ -196,6 +197,18 @@ msgstr "Geldtopf" msgid "Ledgers" msgstr "Geldtöpfe" +#: finance/models.py +msgid "In preparation" +msgstr "In Vorbereitung" + +#: finance/models.py +msgid "Submitted" +msgstr "Eingereicht" + +#: finance/models.py +msgid "Confirmed" +msgstr "Abgewickelt" + #: finance/models.py msgid "Short description" msgstr "Kurzbeschreibung" @@ -247,17 +260,13 @@ msgid "Price per night" msgstr "Preis pro Nacht" #: finance/models.py -msgid "Submitted" -msgstr "Eingereicht" +msgid "Status" +msgstr "Status" #: finance/models.py msgid "Submitted on" msgstr "Eingereicht am" -#: finance/models.py -msgid "Confirmed" -msgstr "Abgewickelt" - #: finance/models.py msgid "Paid on" msgstr "Bezahlt am" @@ -288,6 +297,7 @@ msgid "Statement: %(excursion)s" msgstr "Abrechnung: %(excursion)s" #: finance/models.py +#, python-format msgid "Excursion %(excursion)s" msgstr "Ausfahrt %(excursion)s" @@ -305,7 +315,7 @@ msgstr "Aufwandsentschädigung für %(excu)s" msgid "Night and travel costs for %(excu)s" msgstr "Übernachtungs- und Fahrtkosten für %(excu)s" -#: finance/models.py +#: finance/models.py finance/tests/models.py msgid "reduced by org fee" msgstr "reduziert um Org-Beitrag" diff --git a/jdav_web/finance/migrations/0010_statement_status.py b/jdav_web/finance/migrations/0010_statement_status.py new file mode 100644 index 0000000..7c3a4d3 --- /dev/null +++ b/jdav_web/finance/migrations/0010_statement_status.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.20 on 2025-10-11 15:43 + +from django.db import migrations, models + + +def set_status_from_old_fields(apps, schema_editor): + """ + Set the status field based on the existing submitted and confirmed fields. + - If confirmed is True, status = CONFIRMED (2) + - If submitted is True but confirmed is False, status = SUBMITTED (1) + - Otherwise, status = UNSUBMITTED (0) + """ + Statement = apps.get_model('finance', 'Statement') + UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2 + + for statement in Statement.objects.all(): + if statement.confirmed: + statement.status = CONFIRMED + elif statement.submitted: + statement.status = SUBMITTED + else: + statement.status = UNSUBMITTED + statement.save(update_fields=['status']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0009_statement_ljp_to'), + ] + + operations = [ + migrations.AddField( + model_name='statement', + name='status', + field=models.IntegerField(choices=[(0, 'In preparation'), (1, 'Submitted'), (2, 'Confirmed')], default=0, verbose_name='Status'), + ), + migrations.RunPython(set_status_from_old_fields, reverse_code=migrations.RunPython.noop), + ] diff --git a/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py b/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py new file mode 100644 index 0000000..53e36a4 --- /dev/null +++ b/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.20 on 2025-10-11 16:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0010_statement_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='statement', + name='confirmed', + ), + migrations.RemoveField( + model_name='statement', + name='submitted', + ), + ] diff --git a/jdav_web/finance/models.py b/jdav_web/finance/models.py index b9c8a7a..0356220 100644 --- a/jdav_web/finance/models.py +++ b/jdav_web/finance/models.py @@ -46,11 +46,15 @@ class TransactionIssue: class StatementManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(submitted=False, confirmed=False) + return super().get_queryset().filter(status=Statement.UNSUBMITTED) class Statement(CommonModel): MISSING_LEDGER, NON_MATCHING_TRANSACTIONS, INVALID_ALLOWANCE_TO, INVALID_TOTAL, VALID = 0, 1, 2, 3, 4 + UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2 + STATUS_CHOICES = [(UNSUBMITTED, _('In preparation')), + (SUBMITTED, _('Submitted')), + (CONFIRMED, _('Confirmed'))] short_description = models.CharField(verbose_name=_('Short description'), max_length=30, @@ -82,9 +86,10 @@ class Statement(CommonModel): night_cost = models.DecimalField(verbose_name=_('Price per night'), default=0, decimal_places=2, max_digits=5) - submitted = models.BooleanField(verbose_name=_('Submitted'), default=False) + status = models.IntegerField(verbose_name=_('Status'), + choices=STATUS_CHOICES, + default=UNSUBMITTED) submitted_date = models.DateTimeField(verbose_name=_('Submitted on'), default=None, null=True) - confirmed = models.BooleanField(verbose_name=_('Confirmed'), default=False) confirmed_date = models.DateTimeField(verbose_name=_('Paid on'), default=None, null=True) created_by = models.ForeignKey(Member, verbose_name=_('Created by'), @@ -131,8 +136,16 @@ class Statement(CommonModel): else: return self.short_description + @property + def submitted(self): + return self.status == Statement.SUBMITTED or self.status == Statement.CONFIRMED + + @property + def confirmed(self): + return self.status == Statement.CONFIRMED + def submit(self, submitter=None): - self.submitted = True + self.status = self.SUBMITTED self.submitted_date = timezone.now() self.submitted_by = submitter self.save() @@ -231,7 +244,7 @@ class Statement(CommonModel): if not self.validity == Statement.VALID: return False - self.confirmed = True + self.status = self.CONFIRMED self.confirmed_date = timezone.now() self.confirmed_by = confirmer for trans in self.transaction_set.all(): @@ -569,7 +582,7 @@ class Statement(CommonModel): class StatementUnSubmittedManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(submitted=False, confirmed=False) + return super().get_queryset().filter(status=Statement.UNSUBMITTED) class StatementUnSubmitted(Statement): @@ -589,7 +602,7 @@ class StatementUnSubmitted(Statement): class StatementSubmittedManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(submitted=True, confirmed=False) + return super().get_queryset().filter(status=Statement.SUBMITTED) class StatementSubmitted(Statement): @@ -606,7 +619,7 @@ class StatementSubmitted(Statement): class StatementConfirmedManager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(confirmed=True) + return super().get_queryset().filter(status=Statement.CONFIRMED) class StatementConfirmed(Statement): diff --git a/jdav_web/finance/tests/__init__.py b/jdav_web/finance/tests/__init__.py index 41989f9..023e7d7 100644 --- a/jdav_web/finance/tests/__init__.py +++ b/jdav_web/finance/tests/__init__.py @@ -1,3 +1,4 @@ from .admin import * from .models import * from .rules import * +from .migrations import * diff --git a/jdav_web/finance/tests/admin.py b/jdav_web/finance/tests/admin.py index 320a14f..1e0f676 100644 --- a/jdav_web/finance/tests/admin.py +++ b/jdav_web/finance/tests/admin.py @@ -99,16 +99,16 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase): def test_get_readonly_fields_submitted(self): """Test readonly fields when statement is submitted""" # Mark statement as submitted - self.statement.submitted = True + self.statement.status = Statement.SUBMITTED readonly_fields = self.admin.get_readonly_fields(None, self.statement) - self.assertIn('submitted', readonly_fields) + self.assertIn('status', readonly_fields) self.assertIn('excursion', readonly_fields) self.assertIn('short_description', readonly_fields) def test_get_readonly_fields_not_submitted(self): """Test readonly fields when statement is not submitted""" readonly_fields = self.admin.get_readonly_fields(None, self.statement) - self.assertEqual(readonly_fields, ['submitted', 'excursion']) + self.assertEqual(readonly_fields, ['status', 'excursion']) def test_submit_view_insufficient_permission(self): url = reverse('admin:finance_statementunsubmitted_submit', @@ -163,7 +163,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase): self.statement = Statement.objects.create( short_description='Submitted Statement', explanation='Test explanation', - submitted=True, + status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), night_cost=25 @@ -198,7 +198,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase): self.statement_no_trans_success = Statement.objects.create( short_description='No Transactions Success', explanation='Test explanation', - submitted=True, + status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), night_cost=25 @@ -206,7 +206,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase): self.statement_no_trans_error = Statement.objects.create( short_description='No Transactions Error', explanation='Test explanation', - submitted=True, + status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), night_cost=25 @@ -294,7 +294,7 @@ class StatementSubmittedAdminTestCase(AdminTestCase): """Test overview_view with statement that can't be found in StatementSubmitted queryset""" # When trying to access an unsubmitted statement via StatementSubmitted admin, # the decorator will fail to find it and show "Statement not found" - self.statement.submitted = False + self.statement.status = Statement.UNSUBMITTED self.statement.save() url = reverse('admin:finance_statementsubmitted_overview', args=(self.statement.pk,)) @@ -496,8 +496,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase): base_statement = Statement.objects.create( short_description='Confirmed Statement', explanation='Test explanation', - submitted=True, - confirmed=True, + status=Statement.CONFIRMED, confirmed_by=self.member, confirmed_date=timezone.now(), night_cost=25 @@ -510,8 +509,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase): self.unconfirmed_statement = Statement.objects.create( short_description='Unconfirmed Statement', explanation='Test explanation', - submitted=True, - confirmed=False, + status=Statement.SUBMITTED, night_cost=25 ) @@ -528,8 +526,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase): confirmed_with_excursion_base = Statement.objects.create( short_description='Confirmed with Excursion', explanation='Test explanation', - submitted=True, - confirmed=True, + status=Statement.CONFIRMED, confirmed_by=self.member, confirmed_date=timezone.now(), excursion=self.excursion, diff --git a/jdav_web/finance/tests/migrations.py b/jdav_web/finance/tests/migrations.py new file mode 100644 index 0000000..5c8894a --- /dev/null +++ b/jdav_web/finance/tests/migrations.py @@ -0,0 +1,70 @@ +import django.test +from django.apps import apps +from django.db import connection +from django.db.migrations.executor import MigrationExecutor + + +class StatusMigrationTestCase(django.test.TransactionTestCase): + """Test the migration from submitted/confirmed fields to status field.""" + + app = 'finance' + migrate_from = [('finance', '0009_statement_ljp_to')] + migrate_to = [('finance', '0010_statement_status')] + + def setUp(self): + # Get the state before migration + executor = MigrationExecutor(connection) + executor.migrate(self.migrate_from) + + # Get the old models (before migration) + old_apps = executor.loader.project_state(self.migrate_from).apps + self.Statement = old_apps.get_model(self.app, 'Statement') + + # Create statements with different combinations of submitted/confirmed + # created_by is nullable, so we don't need to create a Member + self.unsubmitted = self.Statement.objects.create( + short_description='Unsubmitted Statement', + submitted=False, + confirmed=False + ) + + self.submitted = self.Statement.objects.create( + short_description='Submitted Statement', + submitted=True, + confirmed=False + ) + + self.confirmed = self.Statement.objects.create( + short_description='Confirmed Statement', + submitted=True, + confirmed=True + ) + + def test_status_field_migration(self): + """Test that status field is correctly set from old submitted/confirmed fields.""" + # Run the migration + executor = MigrationExecutor(connection) + executor.loader.build_graph() + executor.migrate(self.migrate_to) + + # Get the new models (after migration) + new_apps = executor.loader.project_state(self.migrate_to).apps + Statement = new_apps.get_model(self.app, 'Statement') + + # Constants from the Statement model + UNSUBMITTED = 0 + SUBMITTED = 1 + CONFIRMED = 2 + + # Verify the migration worked correctly + unsubmitted = Statement.objects.get(pk=self.unsubmitted.pk) + self.assertEqual(unsubmitted.status, UNSUBMITTED, + 'Statement with submitted=False, confirmed=False should have status=UNSUBMITTED') + + submitted = Statement.objects.get(pk=self.submitted.pk) + self.assertEqual(submitted.status, SUBMITTED, + 'Statement with submitted=True, confirmed=False should have status=SUBMITTED') + + confirmed = Statement.objects.get(pk=self.confirmed.pk) + self.assertEqual(confirmed.status, CONFIRMED, + 'Statement with submitted=True, confirmed=True should have status=CONFIRMED') diff --git a/jdav_web/finance/tests/models.py b/jdav_web/finance/tests/models.py index 0ff7553..1e5c624 100644 --- a/jdav_web/finance/tests/models.py +++ b/jdav_web/finance/tests/models.py @@ -505,11 +505,11 @@ class ManagerTestCase(TestCase): self.st_submitted = Statement.objects.create(short_description='A statement', explanation='Important!', night_cost=0, - submitted=True) + status=Statement.SUBMITTED) self.st_confirmed = Statement.objects.create(short_description='A statement', explanation='Important!', night_cost=0, - confirmed=True) + status=Statement.CONFIRMED) def test_get_queryset(self): # TODO: remove this manager, since it is not used diff --git a/jdav_web/finance/tests/rules.py b/jdav_web/finance/tests/rules.py index ce5fa40..5470f54 100644 --- a/jdav_web/finance/tests/rules.py +++ b/jdav_web/finance/tests/rules.py @@ -52,15 +52,15 @@ class FinanceRulesTestCase(TestCase): def test_not_submitted_statement(self): """Test not_submitted predicate returns True when statement is not submitted""" - self.statement.submitted = False + self.statement.status = Statement.UNSUBMITTED self.assertTrue(not_submitted(self.user1, self.statement)) - self.statement.submitted = True + self.statement.status = Statement.SUBMITTED self.assertFalse(not_submitted(self.user1, self.statement)) def test_not_submitted_freizeit_with_statement(self): """Test not_submitted predicate with Freizeit having unsubmitted statement""" self.freizeit.statement = self.statement - self.statement.submitted = False + self.statement.status = Statement.UNSUBMITTED self.assertTrue(not_submitted(self.user1, self.freizeit)) def test_not_submitted_freizeit_without_statement(self): diff --git a/jdav_web/members/tests/rules.py b/jdav_web/members/tests/rules.py index 9a95289..ee7762d 100644 --- a/jdav_web/members/tests/rules.py +++ b/jdav_web/members/tests/rules.py @@ -88,12 +88,12 @@ class RulesTestCase(TestCase): self.statement_unsubmitted = Statement.objects.create( short_description='Unsubmitted Statement', excursion=self.excursion, - submitted=False + status=Statement.UNSUBMITTED ) self.statement_submitted = Statement.objects.create( short_description='Submitted Statement', - submitted=True + status=Statement.SUBMITTED ) def test_is_oneself(self):