diff --git a/nummi/main/forms.py b/nummi/main/forms.py index 05a6ae4..36e5628 100644 --- a/nummi/main/forms.py +++ b/nummi/main/forms.py @@ -12,7 +12,8 @@ class NummiForm(forms.ModelForm): template_name = "main/form/form_base.html" meta_fieldsets = [] - def __init__(self, *args, user, **kwargs): + def __init__(self, *args, **kwargs): + kwargs.pop("user", None) super().__init__(*args, **kwargs) @property diff --git a/nummi/main/static/main/css/form.css b/nummi/main/static/main/css/form.css index 3a25e1d..73c9eb9 100644 --- a/nummi/main/static/main/css/form.css +++ b/nummi/main/static/main/css/form.css @@ -1,8 +1,32 @@ -@keyframes border-pulse { - from { - border-color: var(--green); +.drop-zone { + position: absolute; + 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; } - to { + + main.highlight > & { + z-index: 100; + backdrop-filter: blur(0.1rem); + > span { + color: var(--green); + } } } @@ -14,6 +38,10 @@ form { grid-template-columns: 1fr; } + &.hidden { + display: none; + } + .column { display: grid; gap: 0.5rem; diff --git a/nummi/main/static/main/css/main.css b/nummi/main/static/main/css/main.css index 3dc29d4..a154868 100644 --- a/nummi/main/static/main/css/main.css +++ b/nummi/main/static/main/css/main.css @@ -85,6 +85,7 @@ footer { @media (width > 720px) { padding: 2rem; } + background: var(--bg); } main { position: relative; diff --git a/nummi/main/templatetags/main_extras.py b/nummi/main/templatetags/main_extras.py index 0a9abcc..c5eea6c 100644 --- a/nummi/main/templatetags/main_extras.py +++ b/nummi/main/templatetags/main_extras.py @@ -59,7 +59,7 @@ def messageicon(level): @register.filter def extension(file): - return file.name.split(".")[-1].upper() + return file.name.split(".", 1)[1].upper() @register.filter diff --git a/nummi/statement/forms.py b/nummi/statement/forms.py index c3841f2..353ebe7 100644 --- a/nummi/statement/forms.py +++ b/nummi/statement/forms.py @@ -25,7 +25,7 @@ class StatementForm(NummiForm): ] def __init__(self, *args, **kwargs): - _user = kwargs.get("user") + _user = kwargs.pop("user") _disable_account = kwargs.pop("disable_account", False) super().__init__(*args, **kwargs) self.fields["account"].queryset = _user.account_set.exclude(archived=True) diff --git a/nummi/transaction/forms.py b/nummi/transaction/forms.py index 5583c84..807c00e 100644 --- a/nummi/transaction/forms.py +++ b/nummi/transaction/forms.py @@ -1,6 +1,8 @@ import json from category.forms import CategorySelect +from django import forms +from django.forms import formset_factory from main.forms import DatalistInput, NummiFileInput, NummiForm from statement.forms import StatementSelect @@ -46,7 +48,7 @@ class TransactionForm(NummiForm): ] def __init__(self, *args, **kwargs): - _user = kwargs.get("user") + _user = kwargs.pop("user") _disable_statement = kwargs.pop("disable_statement", False) _autocomplete = kwargs.pop("autocomplete", False) super().__init__(*args, **kwargs) @@ -117,3 +119,29 @@ class InvoiceForm(NummiForm): widgets = { "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) diff --git a/nummi/transaction/static/transaction/js/invoice_form.js b/nummi/transaction/static/transaction/js/invoice_form.js new file mode 100644 index 0000000..d18abfa --- /dev/null +++ b/nummi/transaction/static/transaction/js/invoice_form.js @@ -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(); +}); diff --git a/nummi/transaction/templates/transaction/transaction_detail.html b/nummi/transaction/templates/transaction/transaction_detail.html index 4cfd068..7e58c0f 100644 --- a/nummi/transaction/templates/transaction/transaction_detail.html +++ b/nummi/transaction/templates/transaction/transaction_detail.html @@ -10,6 +10,7 @@ {% css "main/css/form.css" %} {% css "main/css/table.css" %} {% css "main/css/plot.css" %} + {% js "transaction/js/invoice_form.js" %} {% endblock link %} {% block body %}

{{ transaction }}

@@ -44,5 +45,15 @@

{% translate "Invoices" %}

{% invoice_table transaction %} +
+
+ {{ "file-add"|remix }}{% translate "Add invoice" %} +
{% endblock body %} diff --git a/nummi/transaction/urls.py b/nummi/transaction/urls.py index c39aef6..7b3c802 100644 --- a/nummi/transaction/urls.py +++ b/nummi/transaction/urls.py @@ -31,6 +31,11 @@ urlpatterns = [ views.InvoiceCreateView.as_view(), name="new_invoice", ), + path( + "/invoice/multiple", + views.MultipleInvoiceCreateView.as_view(), + name="multiple_invoice", + ), path( "/invoice/", views.InvoiceUpdateView.as_view(), diff --git a/nummi/transaction/views.py b/nummi/transaction/views.py index 64e1f53..ebf2238 100644 --- a/nummi/transaction/views.py +++ b/nummi/transaction/views.py @@ -1,8 +1,13 @@ from account.models import Account 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.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.edit import FormView from history.utils import history from main.views import ( NummiCreateView, @@ -13,7 +18,7 @@ from main.views import ( UserMixin, ) -from .forms import InvoiceForm, TransactionForm +from .forms import InvoiceForm, MultipleInvoicesForm, TransactionForm from .models import Invoice, Transaction @@ -57,6 +62,45 @@ class InvoiceCreateView(NummiCreateView): 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): model = Transaction form_class = TransactionForm @@ -70,12 +114,8 @@ class TransactionDetailView(NummiDetailView): def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - transaction = data.get("transaction") - - return data | { - "statement": transaction.statement, - "category": transaction.category, - } + data["invoices_form"] = MultipleInvoicesForm() + return data class InvoiceUpdateView(NummiUpdateView):