mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-11-13 12:23:44 +01:00
Merge pull request #653 from eitchtee/fix/transfer-form-fields
fix(transfers): entities, paid state and inactive items in the transfer form
This commit is contained in:
3 files changed
+97
-20
No files matched your search
@@ -632,6 +632,23 @@ class TransferForm(forms.Form):
|
||||
queryset=TransactionTag.objects.filter(active=True),
|
||||
)
|
||||
|
||||
from_entities = DynamicModelMultipleChoiceField(
|
||||
model=TransactionEntity,
|
||||
to_field_name="name",
|
||||
create_field="name",
|
||||
required=False,
|
||||
label=_("Entities"),
|
||||
)
|
||||
to_entities = DynamicModelMultipleChoiceField(
|
||||
model=TransactionEntity,
|
||||
to_field_name="name",
|
||||
create_field="name",
|
||||
required=False,
|
||||
label=_("Entities"),
|
||||
)
|
||||
|
||||
is_paid = forms.BooleanField(label=_("Paid"), initial=True, required=False)
|
||||
|
||||
date = forms.DateField(label=_("Date"))
|
||||
|
||||
reference_date = forms.DateField(
|
||||
@@ -672,11 +689,14 @@ class TransferForm(forms.Form):
|
||||
"to_category": to_t.category,
|
||||
"from_tags": list(from_t.tags.all()),
|
||||
"to_tags": list(to_t.tags.all()),
|
||||
"from_entities": list(from_t.entities.all()),
|
||||
"to_entities": list(to_t.entities.all()),
|
||||
"date": from_t.date,
|
||||
"reference_date": from_t.reference_date,
|
||||
"description": from_t.description,
|
||||
"notes": from_t.notes,
|
||||
"mute": from_t.mute,
|
||||
"is_paid": from_t.is_paid,
|
||||
}
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -685,6 +705,7 @@ class TransferForm(forms.Form):
|
||||
self.helper.form_method = "post"
|
||||
|
||||
self.helper.layout = Layout(
|
||||
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
|
||||
Row(
|
||||
Column(Field("date")),
|
||||
Column(
|
||||
@@ -699,6 +720,7 @@ class TransferForm(forms.Form):
|
||||
Column(Field("from_amount")),
|
||||
Column("from_category"),
|
||||
Column("from_tags"),
|
||||
Column("from_entities"),
|
||||
css_class="bg-base-100 rounded-box p-4 border-base-content/60 border my-3",
|
||||
),
|
||||
Row(
|
||||
@@ -710,6 +732,7 @@ class TransferForm(forms.Form):
|
||||
),
|
||||
Column("to_category"),
|
||||
Column("to_tags"),
|
||||
Column("to_entities"),
|
||||
css_class="bg-base-100 rounded-box p-4 border-base-content/60 border",
|
||||
),
|
||||
FormActions(
|
||||
@@ -725,23 +748,28 @@ class TransferForm(forms.Form):
|
||||
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
self.fields["from_account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
# When editing, keep the transfer's own archived/inactive items selectable,
|
||||
# like TransactionForm does, so saving doesn't drop them.
|
||||
own = (
|
||||
[instance.transfer_from.pk, instance.transfer_to.pk] if instance else []
|
||||
)
|
||||
|
||||
self.fields["from_category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
self.fields["from_tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
|
||||
self.fields["to_account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.fields["to_category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
self.fields["to_tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
accounts = Account.objects.filter(
|
||||
Q(is_archived=False) | Q(transactions__in=own)
|
||||
).distinct()
|
||||
categories = TransactionCategory.objects.filter(
|
||||
Q(active=True) | Q(transaction__in=own)
|
||||
).distinct()
|
||||
tags = TransactionTag.objects.filter(
|
||||
Q(active=True) | Q(transaction__in=own)
|
||||
).distinct()
|
||||
entities = TransactionEntity.objects.filter(
|
||||
Q(active=True) | Q(transactions__in=own)
|
||||
).distinct()
|
||||
for side in ("from", "to"):
|
||||
self.fields[f"{side}_account"].queryset = accounts
|
||||
self.fields[f"{side}_category"].queryset = categories
|
||||
self.fields[f"{side}_tags"].queryset = tags
|
||||
self.fields[f"{side}_entities"].queryset = entities
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@@ -765,8 +793,8 @@ class TransferForm(forms.Form):
|
||||
for t in (from_transaction, to_transaction)
|
||||
]
|
||||
else:
|
||||
from_transaction = Transaction(type=Transaction.Type.EXPENSE, is_paid=True)
|
||||
to_transaction = Transaction(type=Transaction.Type.INCOME, is_paid=True)
|
||||
from_transaction = Transaction(type=Transaction.Type.EXPENSE)
|
||||
to_transaction = Transaction(type=Transaction.Type.INCOME)
|
||||
|
||||
for transaction, side in ((from_transaction, "from"), (to_transaction, "to")):
|
||||
transaction.account = data[f"{side}_account"]
|
||||
@@ -776,6 +804,7 @@ class TransferForm(forms.Form):
|
||||
transaction.description = data["description"]
|
||||
transaction.notes = data.get("notes")
|
||||
transaction.mute = data["mute"]
|
||||
transaction.is_paid = data["is_paid"]
|
||||
from_transaction.amount = data["from_amount"]
|
||||
to_transaction.amount = data["to_amount"] or data["from_amount"]
|
||||
|
||||
@@ -791,6 +820,8 @@ class TransferForm(forms.Form):
|
||||
|
||||
from_transaction.tags.set(data.get("from_tags", []))
|
||||
to_transaction.tags.set(data.get("to_tags", []))
|
||||
from_transaction.entities.set(data.get("from_entities", []))
|
||||
to_transaction.entities.set(data.get("to_entities", []))
|
||||
|
||||
# Rules run for each side, like any other transaction
|
||||
for i, transaction in enumerate((from_transaction, to_transaction)):
|
||||
|
||||
@@ -15,6 +15,7 @@ from apps.transactions.models import (
|
||||
RecurringTransaction,
|
||||
Transaction,
|
||||
TransactionCategory,
|
||||
TransactionEntity,
|
||||
TransactionTag,
|
||||
)
|
||||
from apps.rules.signals import transaction_created, transaction_updated
|
||||
@@ -53,6 +54,7 @@ class TransferTestBase(TestCase):
|
||||
"reference_date": "2026-03-01",
|
||||
"description": "Move money",
|
||||
"mute": True,
|
||||
"is_paid": "true",
|
||||
**overrides,
|
||||
}
|
||||
form = TransferForm(data)
|
||||
@@ -73,6 +75,12 @@ class TransferTests(TransferTestBase):
|
||||
self.assertEqual(from_t.amount, Decimal("100"))
|
||||
self.assertEqual(to_t.amount, Decimal("98"))
|
||||
|
||||
def test_paid_toggle_applies_to_both_sides(self):
|
||||
from_t, to_t = self._transfer(is_paid="false")
|
||||
self.assertFalse(from_t.is_paid)
|
||||
self.assertFalse(to_t.is_paid)
|
||||
self.assertFalse(TransferForm(instance=from_t).initial["is_paid"])
|
||||
|
||||
def test_listing_collapses_pair_unless_one_side_is_filtered_out(self):
|
||||
from_t, to_t = self._transfer()
|
||||
Transaction.objects.create(
|
||||
@@ -195,21 +203,49 @@ class TransferTests(TransferTestBase):
|
||||
in_cat = TransactionCategory.objects.create(name="InCat")
|
||||
out_tag = TransactionTag.objects.create(name="OutTag")
|
||||
in_tag = TransactionTag.objects.create(name="InTag")
|
||||
self._transfer(
|
||||
out_ent = TransactionEntity.objects.create(name="OutEnt")
|
||||
in_ent = TransactionEntity.objects.create(name="InEnt")
|
||||
from_t, to_t = self._transfer(
|
||||
from_category=str(out_cat.id),
|
||||
to_category=str(in_cat.id),
|
||||
from_tags=[out_tag.name],
|
||||
to_tags=[in_tag.name],
|
||||
from_entities=[out_ent.name],
|
||||
to_entities=[in_ent.name],
|
||||
)
|
||||
self.assertEqual(list(from_t.entities.all()), [out_ent])
|
||||
self.assertEqual(list(to_t.entities.all()), [in_ent])
|
||||
|
||||
response = self.client.get(
|
||||
reverse("monthly_transactions_list", kwargs={"month": 3, "year": 2026}),
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
content = response.content.decode()
|
||||
for name in ("OutCat", "InCat", "OutTag", "InTag"):
|
||||
for name in ("OutCat", "InCat", "OutTag", "InTag", "OutEnt", "InEnt"):
|
||||
self.assertIn(name, content)
|
||||
|
||||
def test_edit_keeps_inactive_tag_and_archived_account(self):
|
||||
tag = TransactionTag.objects.create(name="OldTag")
|
||||
from_t, to_t = self._transfer(from_tags=[tag.name])
|
||||
tag.active = False
|
||||
tag.save()
|
||||
self.savings.is_archived = True
|
||||
self.savings.save()
|
||||
|
||||
form = TransferForm(
|
||||
{
|
||||
"from_account": self.checking.id,
|
||||
"to_account": self.savings.id,
|
||||
"from_amount": "100",
|
||||
"date": "2026-03-10",
|
||||
"from_tags": [tag.name],
|
||||
},
|
||||
instance=from_t,
|
||||
)
|
||||
self.assertTrue(form.is_valid(), form.errors)
|
||||
form.save()
|
||||
self.assertEqual(list(from_t.tags.all()), [tag])
|
||||
|
||||
|
||||
class TransferTemplateTests(TransferTestBase):
|
||||
"""Recurring and quick transactions of type Transfer."""
|
||||
|
||||
@@ -76,6 +76,16 @@
|
||||
<div class="flex-1 ps-0">{{ overriden_entities|join:", " }}</div>
|
||||
</div>
|
||||
|
||||
{% elif is_transfer %}
|
||||
{% if transaction.entities.all or transfer_to.entities.all %}
|
||||
<div class="grid grid-cols-[auto_1fr] mb-2 lg:mb-1 text-base-content/70">
|
||||
<div class="w-auto pe-1"><i class="fa-solid fa-user-group fa-fw mr-1 fa-xs"></i></div>
|
||||
<div>{{ transaction.entities.all|join:", "|default:"—" }}</div>
|
||||
<div class="w-auto pe-1"><i class="fa-solid fa-arrow-right fa-fw mr-1 fa-xs text-base-content/40"></i></div>
|
||||
<div>{{ transfer_to.entities.all|join:", "|default:"—" }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% comment %} If no override, fall back to transaction entities, but ONLY if the transaction has an ID. {% endcomment %}
|
||||
{% elif transaction.id and transaction.entities.all %}
|
||||
<div class="flex flex-wrap mb-2 lg:mb-1 text-base-content/70">
|
||||
|
||||
Reference in new issue
Block a user