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/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/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/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):