parent
cfb2ceb2c3
commit
805c7d3dc0
10 changed files with 151 additions and 15 deletions
|
@ -12,7 +12,8 @@ class NummiForm(forms.ModelForm):
|
||||||
template_name = "main/form/form_base.html"
|
template_name = "main/form/form_base.html"
|
||||||
meta_fieldsets = []
|
meta_fieldsets = []
|
||||||
|
|
||||||
def __init__(self, *args, user, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.pop("user", None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,8 +1,32 @@
|
||||||
@keyframes border-pulse {
|
.drop-zone {
|
||||||
from {
|
position: absolute;
|
||||||
border-color: var(--green);
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: transparent;
|
||||||
|
display: grid;
|
||||||
|
transition-property: backdrop-filter;
|
||||||
|
transition-duration: 750ms;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font-weight: 650;
|
||||||
|
font-size: 2rem;
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.highlight > & {
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(0.1rem);
|
||||||
|
> span {
|
||||||
|
color: var(--green);
|
||||||
}
|
}
|
||||||
to {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +38,10 @@ form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
|
@ -85,6 +85,7 @@ footer {
|
||||||
@media (width > 720px) {
|
@media (width > 720px) {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -59,7 +59,7 @@ def messageicon(level):
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def extension(file):
|
def extension(file):
|
||||||
return file.name.split(".")[-1].upper()
|
return file.name.split(".", 1)[1].upper()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|
|
@ -25,7 +25,7 @@ class StatementForm(NummiForm):
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
_user = kwargs.get("user")
|
_user = kwargs.pop("user")
|
||||||
_disable_account = kwargs.pop("disable_account", False)
|
_disable_account = kwargs.pop("disable_account", False)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["account"].queryset = _user.account_set.exclude(archived=True)
|
self.fields["account"].queryset = _user.account_set.exclude(archived=True)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from category.forms import CategorySelect
|
from category.forms import CategorySelect
|
||||||
|
from django import forms
|
||||||
|
from django.forms import formset_factory
|
||||||
from main.forms import DatalistInput, NummiFileInput, NummiForm
|
from main.forms import DatalistInput, NummiFileInput, NummiForm
|
||||||
from statement.forms import StatementSelect
|
from statement.forms import StatementSelect
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ class TransactionForm(NummiForm):
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
_user = kwargs.get("user")
|
_user = kwargs.pop("user")
|
||||||
_disable_statement = kwargs.pop("disable_statement", False)
|
_disable_statement = kwargs.pop("disable_statement", False)
|
||||||
_autocomplete = kwargs.pop("autocomplete", False)
|
_autocomplete = kwargs.pop("autocomplete", False)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -117,3 +119,29 @@ class InvoiceForm(NummiForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"file": NummiFileInput,
|
"file": NummiFileInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFileInput(forms.ClearableFileInput):
|
||||||
|
allow_multiple_selected = True
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleFileField(forms.FileField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("widget", MultipleFileInput())
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self, data, initial=None):
|
||||||
|
single_file_clean = super().clean
|
||||||
|
if isinstance(data, (list, tuple)):
|
||||||
|
result = [single_file_clean(d, initial) for d in data]
|
||||||
|
else:
|
||||||
|
result = single_file_clean(data, initial)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleInvoicesForm(forms.Form):
|
||||||
|
prefix = "invoices"
|
||||||
|
invoices = MultipleFileField()
|
||||||
|
|
||||||
|
|
||||||
|
InvoicesFormSet = formset_factory(MultipleInvoicesForm)
|
||||||
|
|
22
nummi/transaction/static/transaction/js/invoice_form.js
Normal file
22
nummi/transaction/static/transaction/js/invoice_form.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const dropArea = document.querySelector("main");
|
||||||
|
const form = document.querySelector("form.invoices");
|
||||||
|
|
||||||
|
dropArea.addEventListener("dragover", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dropArea.classList.add("highlight");
|
||||||
|
});
|
||||||
|
|
||||||
|
dropArea.addEventListener("dragleave", () => {
|
||||||
|
dropArea.classList.remove("highlight");
|
||||||
|
});
|
||||||
|
|
||||||
|
dropArea.addEventListener("drop", (event) => {
|
||||||
|
console.log(event);
|
||||||
|
event.preventDefault();
|
||||||
|
dropArea.classList.remove("highlight");
|
||||||
|
const files = event.dataTransfer.files;
|
||||||
|
console.log(files);
|
||||||
|
const input = form.querySelector("input[type=file]");
|
||||||
|
input.files = files;
|
||||||
|
form.submit();
|
||||||
|
});
|
|
@ -10,6 +10,7 @@
|
||||||
{% css "main/css/form.css" %}
|
{% css "main/css/form.css" %}
|
||||||
{% css "main/css/table.css" %}
|
{% css "main/css/table.css" %}
|
||||||
{% css "main/css/plot.css" %}
|
{% css "main/css/plot.css" %}
|
||||||
|
{% js "transaction/js/invoice_form.js" %}
|
||||||
{% endblock link %}
|
{% endblock link %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>{{ transaction }}</h2>
|
<h2>{{ transaction }}</h2>
|
||||||
|
@ -44,5 +45,15 @@
|
||||||
<section>
|
<section>
|
||||||
<h3>{% translate "Invoices" %}</h3>
|
<h3>{% translate "Invoices" %}</h3>
|
||||||
{% invoice_table transaction %}
|
{% invoice_table transaction %}
|
||||||
|
<form class="hidden invoices"
|
||||||
|
method="post"
|
||||||
|
action="{% url "multiple_invoice" transaction=transaction.pk %}"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ invoices_form }}
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="drop-zone">
|
||||||
|
<span class="wi">{{ "file-add"|remix }}{% translate "Add invoice" %}</span>
|
||||||
|
</div>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -31,6 +31,11 @@ urlpatterns = [
|
||||||
views.InvoiceCreateView.as_view(),
|
views.InvoiceCreateView.as_view(),
|
||||||
name="new_invoice",
|
name="new_invoice",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"<transaction>/invoice/multiple",
|
||||||
|
views.MultipleInvoiceCreateView.as_view(),
|
||||||
|
name="multiple_invoice",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"<transaction>/invoice/<invoice>",
|
"<transaction>/invoice/<invoice>",
|
||||||
views.InvoiceUpdateView.as_view(),
|
views.InvoiceUpdateView.as_view(),
|
||||||
|
|
|
@ -1,8 +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.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
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.dates import MonthArchiveView, YearArchiveView
|
from django.views.generic.dates import MonthArchiveView, YearArchiveView
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
from history.utils import history
|
from history.utils import history
|
||||||
from main.views import (
|
from main.views import (
|
||||||
NummiCreateView,
|
NummiCreateView,
|
||||||
|
@ -13,7 +18,7 @@ from main.views import (
|
||||||
UserMixin,
|
UserMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .forms import InvoiceForm, TransactionForm
|
from .forms import InvoiceForm, MultipleInvoicesForm, TransactionForm
|
||||||
from .models import Invoice, Transaction
|
from .models import Invoice, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,6 +62,45 @@ class InvoiceCreateView(NummiCreateView):
|
||||||
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
|
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleInvoiceCreateView(FormView):
|
||||||
|
form_class = MultipleInvoicesForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
transaction = get_object_or_404(
|
||||||
|
self.request.user.transaction_set, pk=self.kwargs["transaction"]
|
||||||
|
)
|
||||||
|
|
||||||
|
invoices = []
|
||||||
|
for file in form.cleaned_data["invoices"]:
|
||||||
|
invoice = Invoice(
|
||||||
|
transaction=transaction,
|
||||||
|
user=self.request.user,
|
||||||
|
file=file,
|
||||||
|
name=file.name.split(".", 1)[0],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
invoice.full_clean()
|
||||||
|
except ValidationError as err:
|
||||||
|
for msg in err.messages:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
format_html(
|
||||||
|
"{msg} {file}. {err}",
|
||||||
|
msg=_("Error processing file"),
|
||||||
|
file=file.name,
|
||||||
|
err=msg,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
invoices.append(invoice)
|
||||||
|
|
||||||
|
Invoice.objects.bulk_create(invoices)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy("transaction", args=(self.kwargs["transaction"],))
|
||||||
|
|
||||||
|
|
||||||
class TransactionUpdateView(NummiUpdateView):
|
class TransactionUpdateView(NummiUpdateView):
|
||||||
model = Transaction
|
model = Transaction
|
||||||
form_class = TransactionForm
|
form_class = TransactionForm
|
||||||
|
@ -70,12 +114,8 @@ class TransactionDetailView(NummiDetailView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
transaction = data.get("transaction")
|
data["invoices_form"] = MultipleInvoicesForm()
|
||||||
|
return data
|
||||||
return data | {
|
|
||||||
"statement": transaction.statement,
|
|
||||||
"category": transaction.category,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceUpdateView(NummiUpdateView):
|
class InvoiceUpdateView(NummiUpdateView):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue