mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-11-13 12:23:44 +01:00
feat(transactions): add views to convert a transaction into a quick transaction, recurring transaction or installment plan
This commit is contained in:
1 parent
cd6069f857
commit
8c5c299312
5 files changed
+340
No files matched your search
@@ -2,11 +2,15 @@ from datetime import date
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.accounts.models import Account, AccountGroup
|
from apps.accounts.models import Account, AccountGroup
|
||||||
from apps.currencies.models import Currency
|
from apps.currencies.models import Currency
|
||||||
from apps.transactions.models import (
|
from apps.transactions.models import (
|
||||||
|
InstallmentPlan,
|
||||||
|
QuickTransaction,
|
||||||
|
RecurringTransaction,
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionCategory,
|
TransactionCategory,
|
||||||
TransactionTag,
|
TransactionTag,
|
||||||
@@ -172,3 +176,154 @@ class TransactionSimpleAddViewTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
form = response.context["form"]
|
form = response.context["form"]
|
||||||
self.assertEqual(form.initial.get("category"), self.category.id)
|
self.assertEqual(form.initial.get("category"), self.category.id)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
)
|
||||||
|
class TransactionConvertViewTests(TestCase):
|
||||||
|
"""Tests for converting a transaction into quick/recurring/installment"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
email="testuser@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.client.login(username="testuser@test.com", password="testpass123")
|
||||||
|
|
||||||
|
self.currency = Currency.objects.create(
|
||||||
|
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
|
||||||
|
)
|
||||||
|
self.account = Account.objects.create(
|
||||||
|
name="Test Account", currency=self.currency
|
||||||
|
)
|
||||||
|
self.category = TransactionCategory.objects.create(name="Test Category")
|
||||||
|
self.tag = TransactionTag.objects.create(name="TestTag")
|
||||||
|
|
||||||
|
self.transaction = Transaction.objects.create(
|
||||||
|
account=self.account,
|
||||||
|
type=Transaction.Type.EXPENSE,
|
||||||
|
date=date(2030, 1, 15),
|
||||||
|
amount=50,
|
||||||
|
description="Gym",
|
||||||
|
category=self.category,
|
||||||
|
is_paid=True,
|
||||||
|
)
|
||||||
|
self.transaction.tags.add(self.tag)
|
||||||
|
|
||||||
|
def _get(self, url_name):
|
||||||
|
return self.client.get(
|
||||||
|
reverse(url_name, kwargs={"transaction_id": self.transaction.id}),
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _post(self, url_name, data):
|
||||||
|
return self.client.post(
|
||||||
|
reverse(url_name, kwargs={"transaction_id": self.transaction.id}),
|
||||||
|
data,
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_quick_transaction_form_is_prefilled_without_name(self):
|
||||||
|
response = self._get("transaction_convert_to_quick_transaction")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
initial = response.context["form"].initial
|
||||||
|
self.assertEqual(initial["description"], "Gym")
|
||||||
|
self.assertEqual(initial["tags"], ["TestTag"])
|
||||||
|
self.assertNotIn("name", initial)
|
||||||
|
|
||||||
|
def test_quick_transaction_requires_name_and_keeps_transaction(self):
|
||||||
|
data = {
|
||||||
|
"account": self.account.id,
|
||||||
|
"type": "EX",
|
||||||
|
"amount": "50",
|
||||||
|
"description": "Gym",
|
||||||
|
}
|
||||||
|
response = self._post("transaction_convert_to_quick_transaction", data)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(QuickTransaction.objects.exists())
|
||||||
|
|
||||||
|
response = self._post(
|
||||||
|
"transaction_convert_to_quick_transaction", {**data, "name": "Gym"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertTrue(QuickTransaction.objects.filter(name="Gym").exists())
|
||||||
|
self.assertTrue(Transaction.objects.filter(id=self.transaction.id).exists())
|
||||||
|
|
||||||
|
def test_recurring_transaction_adopts_original_as_first_occurrence(self):
|
||||||
|
response = self._post(
|
||||||
|
"transaction_convert_to_recurring_transaction",
|
||||||
|
{
|
||||||
|
"account": self.account.id,
|
||||||
|
"type": "EX",
|
||||||
|
"amount": "60",
|
||||||
|
"description": "Gym",
|
||||||
|
"category": self.category.id,
|
||||||
|
"tags": ["TestTag"],
|
||||||
|
"start_date": "2030-01-15",
|
||||||
|
"recurrence_type": "month",
|
||||||
|
"recurrence_interval": 1,
|
||||||
|
"keep_at_most": 6,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
recurring = RecurringTransaction.all_objects.get()
|
||||||
|
self.transaction.refresh_from_db()
|
||||||
|
self.assertEqual(self.transaction.recurring_transaction, recurring)
|
||||||
|
# Paid amount is preserved
|
||||||
|
self.assertEqual(self.transaction.amount, 50)
|
||||||
|
self.assertEqual(
|
||||||
|
recurring.transactions.filter(date=date(2030, 1, 15)).count(), 1
|
||||||
|
)
|
||||||
|
self.assertEqual(recurring.last_generated_date, date(2030, 1, 15))
|
||||||
|
|
||||||
|
def test_installment_plan_adopts_original_as_first_installment(self):
|
||||||
|
response = self._post(
|
||||||
|
"transaction_convert_to_installment_plan",
|
||||||
|
{
|
||||||
|
"account": self.account.id,
|
||||||
|
"type": "EX",
|
||||||
|
"installment_amount": "50",
|
||||||
|
"description": "TV",
|
||||||
|
"add_description_to_transaction": "on",
|
||||||
|
"number_of_installments": 3,
|
||||||
|
"installment_start": 1,
|
||||||
|
"start_date": "2030-01-15",
|
||||||
|
"recurrence": "monthly",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
|
plan = InstallmentPlan.all_objects.get()
|
||||||
|
self.transaction.refresh_from_db()
|
||||||
|
self.assertEqual(self.transaction.installment_plan, plan)
|
||||||
|
self.assertEqual(self.transaction.installment_id, 1)
|
||||||
|
self.assertTrue(self.transaction.is_paid)
|
||||||
|
self.assertEqual(self.transaction.description, "TV")
|
||||||
|
self.assertEqual(plan.transactions.count(), 3)
|
||||||
|
|
||||||
|
def test_linked_transaction_cannot_be_converted_into_plan(self):
|
||||||
|
self._post(
|
||||||
|
"transaction_convert_to_installment_plan",
|
||||||
|
{
|
||||||
|
"account": self.account.id,
|
||||||
|
"type": "EX",
|
||||||
|
"installment_amount": "50",
|
||||||
|
"description": "TV",
|
||||||
|
"number_of_installments": 2,
|
||||||
|
"installment_start": 1,
|
||||||
|
"start_date": "2030-01-15",
|
||||||
|
"recurrence": "monthly",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self._get("transaction_convert_to_recurring_transaction").status_code, 404
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self._get("transaction_convert_to_installment_plan").status_code, 404
|
||||||
|
)
|
||||||
@@ -402,4 +402,19 @@ urlpatterns = [
|
|||||||
views.quick_transaction_add_as_quick_transaction,
|
views.quick_transaction_add_as_quick_transaction,
|
||||||
name="quick_transaction_add_as_quick_transaction",
|
name="quick_transaction_add_as_quick_transaction",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/convert/quick-transaction/",
|
||||||
|
views.transaction_convert_to_quick_transaction,
|
||||||
|
name="transaction_convert_to_quick_transaction",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/convert/recurring-transaction/",
|
||||||
|
views.transaction_convert_to_recurring_transaction,
|
||||||
|
name="transaction_convert_to_recurring_transaction",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transaction/<int:transaction_id>/convert/installment-plan/",
|
||||||
|
views.transaction_convert_to_installment_plan,
|
||||||
|
name="transaction_convert_to_installment_plan",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
@@ -6,3 +6,4 @@ from .actions import *
|
|||||||
from .installment_plans import *
|
from .installment_plans import *
|
||||||
from .recurring_transactions import *
|
from .recurring_transactions import *
|
||||||
from .quick_transactions import *
|
from .quick_transactions import *
|
||||||
|
from .conversions import *
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db import transaction as db_transaction
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.transactions.forms import (
|
||||||
|
InstallmentPlanForm,
|
||||||
|
QuickTransactionForm,
|
||||||
|
RecurringTransactionForm,
|
||||||
|
)
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
def _initial_from_transaction(transaction: Transaction):
|
||||||
|
return {
|
||||||
|
"account": transaction.account_id,
|
||||||
|
"type": transaction.type,
|
||||||
|
"description": transaction.description,
|
||||||
|
"notes": transaction.notes,
|
||||||
|
"category": transaction.category_id,
|
||||||
|
"tags": list(transaction.tags.values_list("name", flat=True)),
|
||||||
|
"entities": list(transaction.entities.values_list("name", flat=True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_unlinked_transaction(transaction_id):
|
||||||
|
# Transactions already generated by a plan or recurrence can't be converted again
|
||||||
|
return get_object_or_404(
|
||||||
|
Transaction,
|
||||||
|
id=transaction_id,
|
||||||
|
installment_plan__isnull=True,
|
||||||
|
recurring_transaction__isnull=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_convert_form(request, form, title, transaction_id, url_name):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/convert.html",
|
||||||
|
{
|
||||||
|
"form": form,
|
||||||
|
"title": title,
|
||||||
|
"form_url": reverse(url_name, kwargs={"transaction_id": transaction_id}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def transaction_convert_to_quick_transaction(request, transaction_id):
|
||||||
|
transaction = get_object_or_404(Transaction, id=transaction_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = QuickTransactionForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, _("Item added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = QuickTransactionForm(
|
||||||
|
initial={
|
||||||
|
**_initial_from_transaction(transaction),
|
||||||
|
"is_paid": transaction.is_paid,
|
||||||
|
"amount": transaction.amount,
|
||||||
|
"mute": transaction.mute,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return _render_convert_form(
|
||||||
|
request,
|
||||||
|
form,
|
||||||
|
_("Convert into quick transaction"),
|
||||||
|
transaction_id,
|
||||||
|
"transaction_convert_to_quick_transaction",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def transaction_convert_to_recurring_transaction(request, transaction_id):
|
||||||
|
transaction = _get_unlinked_transaction(transaction_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = RecurringTransactionForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
with db_transaction.atomic():
|
||||||
|
form.save(source_transaction=transaction)
|
||||||
|
messages.success(request, _("Recurring Transaction added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = RecurringTransactionForm(
|
||||||
|
initial={
|
||||||
|
**_initial_from_transaction(transaction),
|
||||||
|
"amount": transaction.amount,
|
||||||
|
"start_date": transaction.date,
|
||||||
|
"reference_date": transaction.reference_date,
|
||||||
|
"recurrence_interval": 1,
|
||||||
|
"recurrence_type": "month",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return _render_convert_form(
|
||||||
|
request,
|
||||||
|
form,
|
||||||
|
_("Convert into recurring transaction"),
|
||||||
|
transaction_id,
|
||||||
|
"transaction_convert_to_recurring_transaction",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def transaction_convert_to_installment_plan(request, transaction_id):
|
||||||
|
transaction = _get_unlinked_transaction(transaction_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = InstallmentPlanForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
with db_transaction.atomic():
|
||||||
|
form.save(source_transaction=transaction)
|
||||||
|
messages.success(request, _("Installment Plan added successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={"HX-Trigger": "updated, hide_offcanvas"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = InstallmentPlanForm(
|
||||||
|
initial={
|
||||||
|
**_initial_from_transaction(transaction),
|
||||||
|
"installment_amount": transaction.amount,
|
||||||
|
"start_date": transaction.date,
|
||||||
|
"reference_date": transaction.reference_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return _render_convert_form(
|
||||||
|
request,
|
||||||
|
form,
|
||||||
|
_("Convert into installment plan"),
|
||||||
|
transaction_id,
|
||||||
|
"transaction_convert_to_installment_plan",
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form hx-post="{{ form_url }}" hx-target="#generic-offcanvas" novalidate>
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
Reference in new issue
Block a user