You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

206 lines
7.6 KiB

from django.contrib import messages
from django.core.paginator import Paginator
from django.db import DatabaseError, connection
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from finance.models import Account, Category, Payee, Transaction
from finance.queries import (
QueryInputError, TransactionFilters, filtered_transactions,
listed_accounts, listed_categories, listed_payees,
)
from finance.services import (
Actor, DomainError, annotate_transaction, archive, create_account,
create_category, create_payee, record_transaction,
)
from .forms import AccountForm, AnnotationForm, CategoryForm, ManualTransactionForm, PayeeForm
LOCAL_ACTOR = Actor(kind="HUMAN", identifier="local-owner")
DOMAIN_MESSAGES = {
"archived_reference": "That reference is archived and cannot be selected.",
"category_kind_mismatch": "The category does not match the transaction kind.",
"conflict": "A record with these values already exists.",
"invalid_amount": "Enter a valid amount with at most two decimal places.",
"invalid_name": "Enter a name.",
"not_found": "The requested record was not found.",
"validation_error": "The values do not satisfy the financial rules.",
"zero_amount": "The amount cannot be zero.",
}
UNKNOWN_DOMAIN_MESSAGE = "Unable to save this change."
def health(request):
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
except DatabaseError:
return JsonResponse({"status": "unavailable"}, status=503)
return JsonResponse({"status": "ok"})
def home(request):
return transaction_list(request)
def transaction_list(request):
filter_errors = {}
try:
filters = TransactionFilters.from_mapping(request.GET)
rows = filtered_transactions(filters)
except QueryInputError as exc:
filters = TransactionFilters()
rows = Transaction.objects.none()
filter_errors = {field: _query_message(code) for field, code in exc.errors.items()}
page = Paginator(rows, 50).get_page(request.GET.get("page"))
pagination_parameters = request.GET.copy()
pagination_parameters.pop("page", None)
return render(request, "core/transactions/list.html", {
"page": page,
"filters": filters,
"filter_errors": filter_errors,
"accounts": listed_accounts(),
"categories": listed_categories(),
"payees": listed_payees(),
"kinds": Transaction.Kind.choices,
"pagination_query": pagination_parameters.urlencode(),
})
def transaction_detail(request, transaction_id):
row = get_object_or_404(
Transaction.objects.select_related(
"account", "payee", "category", "statement", "refund_of",
"installment_plan", "outgoing_transfer__incoming__account",
"incoming_transfer__outgoing__account",
).prefetch_related("source_records", "refunds"),
pk=transaction_id,
)
return render(request, "core/transactions/detail.html", {"transaction": row})
def transaction_create(request):
form = ManualTransactionForm(request.POST or None)
if request.method == "POST" and form.is_valid():
values = form.cleaned_data
try:
row = record_transaction(
account_id=values["account"], amount=values["amount"],
posting_date=values["posting_date"], occurrence_date=values["occurrence_date"],
kind=values["kind"], description=values["description"],
payee_id=values["payee"], category_id=values["category"], actor=LOCAL_ACTOR,
)
except DomainError as exc:
form.add_error(None, _domain_message(exc))
else:
messages.success(request, "Transaction recorded.")
return redirect("transaction-detail", transaction_id=row.pk)
return render(request, "core/transactions/form.html", {"form": form, "title": "New transaction"})
def transaction_annotate(request, transaction_id):
row = get_object_or_404(Transaction.objects.select_related("payee", "category"), pk=transaction_id)
form = AnnotationForm(request.POST or None, transaction=row)
if request.method == "POST" and form.is_valid():
changes = form.changes()
if not changes:
return redirect("transaction-detail", transaction_id=row.pk)
try:
annotate_transaction(transaction_id=row.pk, actor=LOCAL_ACTOR, **changes)
except DomainError as exc:
form.add_error(None, _domain_message(exc))
else:
messages.success(request, "Transaction annotations updated.")
return redirect("transaction-detail", transaction_id=row.pk)
return render(request, "core/transactions/form.html", {
"form": form, "title": "Edit transaction annotations", "transaction": row,
})
def account_list(request):
return _reference_list(request, listed_accounts(), "Accounts", "account-create", "account-archive")
def category_list(request):
return _reference_list(request, listed_categories(), "Categories", "category-create", "category-archive")
def payee_list(request):
return _reference_list(request, listed_payees(), "Payees", "payee-create", "payee-archive")
def account_create(request):
return _reference_create(request, AccountForm, create_account, "New account", "account-list")
def category_create(request):
return _reference_create(request, CategoryForm, create_category, "New category", "category-list")
def payee_create(request):
return _reference_create(request, PayeeForm, create_payee, "New payee", "payee-list")
@require_POST
def account_archive(request, reference_id):
return _reference_archive(request, Account, reference_id, "account-list")
@require_POST
def category_archive(request, reference_id):
return _reference_archive(request, Category, reference_id, "category-list")
@require_POST
def payee_archive(request, reference_id):
return _reference_archive(request, Payee, reference_id, "payee-list")
def _reference_list(request, rows, title, create_url, archive_url):
return render(request, "core/references/list.html", {
"title": title,
"rows": rows,
"create_url": create_url,
"archive_url": archive_url,
})
def _reference_create(request, form_class, service, title, redirect_name):
form = form_class(request.POST or None)
if request.method == "POST" and form.is_valid():
try:
service(actor=LOCAL_ACTOR, **form.cleaned_data)
except DomainError as exc:
form.add_error(None, _domain_message(exc))
else:
messages.success(request, f"{title.removeprefix('New ')} created.")
return redirect(redirect_name)
return render(request, "core/references/form.html", {"form": form, "title": title})
def _reference_archive(request, model, reference_id, redirect_name):
item = get_object_or_404(model, pk=reference_id)
try:
archive(item, actor=LOCAL_ACTOR)
except DomainError as exc:
messages.error(request, _domain_message(exc))
else:
messages.success(request, "Reference archived.")
return redirect(redirect_name)
def _domain_message(error):
return DOMAIN_MESSAGES.get(error.code, UNKNOWN_DOMAIN_MESSAGE)
def _query_message(code):
return {
"invalid_date": "Enter a valid date.",
"before_date_from": "The end date must be on or after the start date.",
"invalid_uuid": "Select a valid reference.",
"invalid_choice": "Select a valid choice.",
"invalid_currency": "Enter a three-letter uppercase currency code.",
}.get(code, "Invalid filter.")