parent
7851e8afbb
commit
b848bf8d65
10 changed files with 297 additions and 124 deletions
|
@ -48,141 +48,140 @@ form {
|
||||||
border: 1px solid var(--gray);
|
border: 1px solid var(--gray);
|
||||||
padding: var(--gap);
|
padding: var(--gap);
|
||||||
block-size: min-content;
|
block-size: min-content;
|
||||||
|
}
|
||||||
|
.fieldset {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: inherit;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.fieldset {
|
ul.errorlist {
|
||||||
display: grid;
|
color: var(--red);
|
||||||
grid-auto-columns: 1fr;
|
font-weight: 550;
|
||||||
grid-auto-flow: column;
|
list-style: none;
|
||||||
gap: inherit;
|
padding: 0;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
margin: 0;
|
}
|
||||||
border: none;
|
|
||||||
}
|
.field {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: min-content;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
|
||||||
ul.errorlist {
|
ul.errorlist {
|
||||||
color: var(--red);
|
font-size: 0.8rem;
|
||||||
font-weight: 550;
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
&:has(> textarea) {
|
||||||
display: grid;
|
grid-template-rows: min-content 1fr;
|
||||||
grid-auto-rows: min-content;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
|
|
||||||
ul.errorlist {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> textarea) {
|
|
||||||
grid-template-rows: min-content 1fr;
|
|
||||||
textarea {
|
|
||||||
resize: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:has(> input[type="checkbox"]) {
|
|
||||||
grid-template-columns: min-content 1fr;
|
|
||||||
> label {
|
|
||||||
font-size: inherit;
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 2;
|
|
||||||
padding: 0.5rem;
|
|
||||||
line-height: initial;
|
|
||||||
}
|
|
||||||
> input {
|
|
||||||
grid-row: 1;
|
|
||||||
grid-column: 1;
|
|
||||||
margin: 0.5rem;
|
|
||||||
}
|
|
||||||
&:has(> :focus) {
|
|
||||||
background: var(--bg-01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 0.8rem;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
> a {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
resize: block;
|
||||||
line-height: initial;
|
}
|
||||||
border: none;
|
}
|
||||||
border: 1px solid transparent;
|
&:has(> input[type="checkbox"]) {
|
||||||
border-bottom: 1px solid var(--gray);
|
grid-template-columns: min-content 1fr;
|
||||||
background: none;
|
> label {
|
||||||
z-index: 1;
|
font-size: inherit;
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
line-height: initial;
|
||||||
|
}
|
||||||
|
> input {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
&:has(> :focus) {
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:has(~ ul.errorlist) {
|
> label {
|
||||||
border-color: var(--red);
|
font-size: 0.8rem;
|
||||||
}
|
line-height: 0.8rem;
|
||||||
&.autocompleted:not(.mod) {
|
z-index: 10;
|
||||||
border-bottom-color: var(--green);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:not([type="checkbox"]) {
|
> a {
|
||||||
width: 100%;
|
padding: 0.5rem;
|
||||||
margin: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&[name*="value"] {
|
input,
|
||||||
text-align: right;
|
select,
|
||||||
font-feature-settings: var(--num);
|
textarea {
|
||||||
}
|
font: inherit;
|
||||||
&[name*="date"] {
|
line-height: initial;
|
||||||
font-feature-settings: var(--num);
|
border: none;
|
||||||
}
|
border: 1px solid transparent;
|
||||||
|
border-bottom: 1px solid var(--gray);
|
||||||
|
background: none;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
&:focus {
|
&:has(~ ul.errorlist) {
|
||||||
outline: none;
|
border-color: var(--red);
|
||||||
background: var(--bg-01);
|
}
|
||||||
}
|
&.autocompleted:not(.mod) {
|
||||||
|
border-bottom-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .file-input {
|
&:not([type="checkbox"]) {
|
||||||
display: grid;
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
> .current {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-auto-columns: max-content;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
a {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
&::file-selector-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
> .ico-input {
|
|
||||||
|
&[name*="value"] {
|
||||||
|
text-align: right;
|
||||||
|
font-feature-settings: var(--num);
|
||||||
|
}
|
||||||
|
&[name*="date"] {
|
||||||
|
font-feature-settings: var(--num);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file-input {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
> .current {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content 1fr;
|
grid-template-columns: 1fr;
|
||||||
column-gap: 0.5rem;
|
grid-auto-columns: max-content;
|
||||||
align-items: center;
|
grid-auto-flow: column;
|
||||||
span[class|="ri"] {
|
a {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:has(> :focus) {
|
input[type="file"] {
|
||||||
background: var(--bg-01);
|
&::file-selector-button {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> .ico-input {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
span[class|="ri"] {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(> :focus) {
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttons {
|
.buttons {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|
|
@ -538,3 +538,22 @@ ul.statements {
|
||||||
.value {
|
.value {
|
||||||
font-feature-settings: var(--num);
|
font-feature-settings: var(--num);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
border: var(--gray) 1px solid;
|
||||||
|
margin-bottom: var(--gap);
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] summary {
|
||||||
|
background: var(--bg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: var(--gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -135,3 +135,14 @@ if (accounts) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filterForm = document.querySelector("form.filter");
|
||||||
|
if (filterForm) {
|
||||||
|
filterForm.addEventListener("submit", (event) => {
|
||||||
|
for (element of filterForm.elements) {
|
||||||
|
if (element.value == "") {
|
||||||
|
element.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
{% load i18n main_extras %}
|
{% load i18n main_extras %}
|
||||||
{% if first.show %}
|
{% if first.show %}
|
||||||
<a href="?page=1" class="first">1</a>
|
<a href="?{% page_url 1 %}" class="first">1</a>
|
||||||
{% if first.dots %}<span>…</span>{% endif %}
|
{% if first.dots %}<span>…</span>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for page in pages %}
|
{% for page in pages %}
|
||||||
<a href="?page={{ page.number }}"
|
<a href="?{% page_url page.number %}"
|
||||||
{% if page.current %}class="cur"{% endif %}>{{ page.number }}</a>
|
{% if page.current %}class="cur"{% endif %}>{{ page.number }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if last.show %}
|
{% if last.show %}
|
||||||
{% if last.dots %}<span>…</span>{% endif %}
|
{% if last.dots %}<span>…</span>{% endif %}
|
||||||
<a href="?page={{ last.number }}" class="last">{{ last.number }}</a>
|
<a href="?{% page_url last.number %}" class="last">{{ last.number }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -91,10 +91,11 @@ def balance(accounts):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("main/pagination_links.html")
|
@register.inclusion_tag("main/pagination_links.html", takes_context=True)
|
||||||
def pagination_links(page_obj):
|
def pagination_links(context, page_obj):
|
||||||
_n = 3
|
_n = 3
|
||||||
return {
|
return {
|
||||||
|
"request": context["request"],
|
||||||
"pages": [
|
"pages": [
|
||||||
{"number": p, "current": p == page_obj.number}
|
{"number": p, "current": p == page_obj.number}
|
||||||
for p in page_obj.paginator.page_range
|
for p in page_obj.paginator.page_range
|
||||||
|
@ -110,3 +111,10 @@ def pagination_links(page_obj):
|
||||||
"number": page_obj.paginator.num_pages,
|
"number": page_obj.paginator.num_pages,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def page_url(context, page):
|
||||||
|
query = context["request"].GET.copy()
|
||||||
|
query["page"] = page
|
||||||
|
return query.urlencode()
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
from category.forms import CategorySelect
|
from category.forms import CategorySelect
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from main.forms import DatalistInput, NummiFileInput, NummiForm
|
from main.forms import DatalistInput, NummiFileInput, NummiForm
|
||||||
from statement.forms import StatementSelect
|
from statement.forms import StatementSelect
|
||||||
|
|
||||||
|
@ -145,3 +146,55 @@ class MultipleInvoicesForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
InvoicesFormSet = formset_factory(MultipleInvoicesForm)
|
InvoicesFormSet = formset_factory(MultipleInvoicesForm)
|
||||||
|
|
||||||
|
|
||||||
|
class DateInput(forms.DateInput):
|
||||||
|
input_type = "date"
|
||||||
|
|
||||||
|
def __init__(self, attrs=None):
|
||||||
|
super().__init__(attrs)
|
||||||
|
self.format = "%Y-%m-%d"
|
||||||
|
|
||||||
|
|
||||||
|
class DateField(forms.DateField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("widget", DateInput())
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionFiltersForm(forms.Form):
|
||||||
|
start_date = DateField(required=False)
|
||||||
|
end_date = DateField(required=False)
|
||||||
|
category = forms.ModelChoiceField(
|
||||||
|
queryset=None, required=False, widget=CategorySelect()
|
||||||
|
)
|
||||||
|
account = forms.ModelChoiceField(queryset=None, required=False)
|
||||||
|
search = forms.CharField(label=_("Search"), required=False)
|
||||||
|
sort_by = forms.ChoiceField(
|
||||||
|
label=_("Sort by"),
|
||||||
|
choices=[
|
||||||
|
("", _("Default")),
|
||||||
|
("date", _("Date +")),
|
||||||
|
("-date", _("Date -")),
|
||||||
|
("value", _("Value +")),
|
||||||
|
("-value", _("Value -")),
|
||||||
|
],
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
_user = kwargs.pop("user")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields["category"].queryset = _user.category_set
|
||||||
|
self.fields["account"].queryset = _user.account_set
|
||||||
|
|
||||||
|
self.fields["category"].widget.attrs |= {
|
||||||
|
"class": "category",
|
||||||
|
"data-icons": json.dumps(
|
||||||
|
{
|
||||||
|
str(cat.id): cat.icon
|
||||||
|
for cat in self.fields["category"].queryset.only("id", "icon")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<details {% if filters %}open{% endif %}>
|
||||||
|
<summary>{% translate "Filters" %}</summary>
|
||||||
|
<form class="filter" method="get">
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" value="{% translate "Filter" %}">
|
||||||
|
<input type="reset" value="{% translate "Reset" %}">
|
||||||
|
{% if filters %}
|
||||||
|
<a href="?" class="del">{% translate "Clear" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</details>
|
|
@ -3,6 +3,10 @@
|
||||||
{% block name %}
|
{% block name %}
|
||||||
{% translate "Transactions" %}
|
{% translate "Transactions" %}
|
||||||
{% endblock name %}
|
{% endblock name %}
|
||||||
|
{% block link %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% css "main/css/form.css" %}
|
||||||
|
{% endblock link %}
|
||||||
{% block h2 %}
|
{% block h2 %}
|
||||||
{% translate "Transactions" %}
|
{% translate "Transactions" %}
|
||||||
{% endblock h2 %}
|
{% endblock h2 %}
|
||||||
|
@ -10,5 +14,6 @@
|
||||||
<p>
|
<p>
|
||||||
<a class="big-link" href="{% url "new_transaction" %}">{{ "add-circle"|remix }}{% translate "Add transaction" %}</a>
|
<a class="big-link" href="{% url "new_transaction" %}">{{ "add-circle"|remix }}{% translate "Add transaction" %}</a>
|
||||||
</p>
|
</p>
|
||||||
|
{% transaction_filters form=filter_form %}
|
||||||
{% transaction_table transactions %}
|
{% transaction_table transactions %}
|
||||||
{% endblock table %}
|
{% endblock table %}
|
||||||
|
|
|
@ -17,10 +17,8 @@ def transaction_table(context, transactions, n_max=None, **kwargs):
|
||||||
del kwargs["transactions_url"]
|
del kwargs["transactions_url"]
|
||||||
transactions = transactions[:n_max]
|
transactions = transactions[:n_max]
|
||||||
|
|
||||||
if "account" in context or "statement" in context:
|
kwargs.setdefault("hide_account", "account" in context or "statement" in context)
|
||||||
kwargs.setdefault("hide_account", True)
|
kwargs.setdefault("hide_category", "category" in context)
|
||||||
if "category" in context:
|
|
||||||
kwargs.setdefault("hide_category", True)
|
|
||||||
|
|
||||||
ncol = 8
|
ncol = 8
|
||||||
if kwargs.get("hide_account"):
|
if kwargs.get("hide_account"):
|
||||||
|
@ -31,6 +29,12 @@ def transaction_table(context, transactions, n_max=None, **kwargs):
|
||||||
return kwargs | {"transactions": transactions, "ncol": ncol}
|
return kwargs | {"transactions": transactions, "ncol": ncol}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag("transaction/transaction_filters.html", takes_context=True)
|
||||||
|
def transaction_filters(context, **kwargs):
|
||||||
|
kwargs.setdefault("filters", context.get("filters"))
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("transaction/invoice_table.html")
|
@register.inclusion_tag("transaction/invoice_table.html")
|
||||||
def invoice_table(transaction, **kwargs):
|
def invoice_table(transaction, **kwargs):
|
||||||
return kwargs | {
|
return kwargs | {
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
from account.models import Account
|
from account.models import Account
|
||||||
from category.models import Category
|
from category.models import Category
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.postgres.search import (
|
||||||
|
SearchQuery,
|
||||||
|
SearchRank,
|
||||||
|
SearchVector,
|
||||||
|
TrigramSimilarity,
|
||||||
|
)
|
||||||
|
from django.db import models
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
@ -18,7 +25,12 @@ from main.views import (
|
||||||
UserMixin,
|
UserMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .forms import InvoiceForm, MultipleInvoicesForm, TransactionForm
|
from .forms import (
|
||||||
|
InvoiceForm,
|
||||||
|
MultipleInvoicesForm,
|
||||||
|
TransactionFiltersForm,
|
||||||
|
TransactionForm,
|
||||||
|
)
|
||||||
from .models import Invoice, Transaction
|
from .models import Invoice, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,6 +179,49 @@ class TransactionListView(NummiListView):
|
||||||
context_object_name = "transactions"
|
context_object_name = "transactions"
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
|
|
||||||
|
def get_queryset(self, **kwargs):
|
||||||
|
queryset = super().get_queryset(**kwargs)
|
||||||
|
|
||||||
|
if date := self.request.GET.get("start_date"):
|
||||||
|
queryset = queryset.filter(date__gte=date)
|
||||||
|
if date := self.request.GET.get("end_date"):
|
||||||
|
queryset = queryset.filter(date__lte=date)
|
||||||
|
if category := self.request.GET.get("category"):
|
||||||
|
queryset = queryset.filter(category=category)
|
||||||
|
if account := self.request.GET.get("account"):
|
||||||
|
queryset = queryset.filter(statement__account=account)
|
||||||
|
if search := self.request.GET.get("search"):
|
||||||
|
queryset = (
|
||||||
|
queryset.annotate(
|
||||||
|
rank=SearchRank(
|
||||||
|
SearchVector("name", weight="A")
|
||||||
|
+ SearchVector("description", weight="B")
|
||||||
|
+ SearchVector("trader", weight="B")
|
||||||
|
+ SearchVector("category__name", weight="C"),
|
||||||
|
SearchQuery(search, search_type="websearch"),
|
||||||
|
),
|
||||||
|
similarity=TrigramSimilarity("name", search),
|
||||||
|
)
|
||||||
|
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
|
||||||
|
.order_by("-rank", "-date")
|
||||||
|
)
|
||||||
|
if sort_by := self.request.GET.get("sort_by"):
|
||||||
|
queryset = queryset.order_by(sort_by)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
filters = self.request.GET.copy()
|
||||||
|
filters.pop("page", None)
|
||||||
|
if filters:
|
||||||
|
data["filters"] = True
|
||||||
|
data["filter_form"] = TransactionFiltersForm(
|
||||||
|
initial=filters, user=self.request.user
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class TransactionACMixin:
|
class TransactionACMixin:
|
||||||
model = Transaction
|
model = Transaction
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue