From 0962891a9b52b814e646bbe485b5b1016fa78f2d Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Sun, 23 Apr 2023 14:43:44 +0200 Subject: [PATCH] Implement invoice model and admin API --- .gitignore | 1 + gnuviechadmin/gnuviechadmin/settings.py | 1 + gnuviechadmin/gnuviechadmin/urls.py | 7 ++ gnuviechadmin/invoices/__init__.py | 0 gnuviechadmin/invoices/admin.py | 14 ++++ gnuviechadmin/invoices/apps.py | 8 +++ .../invoices/locale/de/LC_MESSAGES/django.po | 69 +++++++++++++++++++ .../migrations/0001_initial_invoice_model.py | 38 ++++++++++ .../migrations/0002_refine_invoice_model.py | 59 ++++++++++++++++ gnuviechadmin/invoices/migrations/__init__.py | 0 gnuviechadmin/invoices/models.py | 56 +++++++++++++++ gnuviechadmin/invoices/serializers.py | 33 +++++++++ gnuviechadmin/invoices/tests.py | 3 + gnuviechadmin/invoices/views.py | 28 ++++++++ poetry.lock | 14 +++- pyproject.toml | 1 + 16 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 gnuviechadmin/invoices/__init__.py create mode 100644 gnuviechadmin/invoices/admin.py create mode 100644 gnuviechadmin/invoices/apps.py create mode 100644 gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po create mode 100644 gnuviechadmin/invoices/migrations/0001_initial_invoice_model.py create mode 100644 gnuviechadmin/invoices/migrations/0002_refine_invoice_model.py create mode 100644 gnuviechadmin/invoices/migrations/__init__.py create mode 100644 gnuviechadmin/invoices/models.py create mode 100644 gnuviechadmin/invoices/serializers.py create mode 100644 gnuviechadmin/invoices/tests.py create mode 100644 gnuviechadmin/invoices/views.py diff --git a/.gitignore b/.gitignore index fcab49b..9577e06 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,5 @@ coverage-report/ /docker/django_static !/docker/django_media/.empty !/docker/django_static/.empty +/media/ /static/ diff --git a/gnuviechadmin/gnuviechadmin/settings.py b/gnuviechadmin/gnuviechadmin/settings.py index 3a90de0..d32f9c0 100644 --- a/gnuviechadmin/gnuviechadmin/settings.py +++ b/gnuviechadmin/gnuviechadmin/settings.py @@ -236,6 +236,7 @@ LOCAL_APPS = ( "hostingpackages", "websites", "help", + "invoices", "contact_form", ) diff --git a/gnuviechadmin/gnuviechadmin/urls.py b/gnuviechadmin/gnuviechadmin/urls.py index a22a5b4..4253e77 100644 --- a/gnuviechadmin/gnuviechadmin/urls.py +++ b/gnuviechadmin/gnuviechadmin/urls.py @@ -8,6 +8,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import path from help import views as help_views +from invoices import views as invoice_views admin.autodiscover() @@ -19,6 +20,12 @@ urlpatterns = [ help_views.HelpUserAPIView.as_view(), name="helpuser-detail", ), + path("api/invoices/", invoice_views.ListInvoiceAPIView.as_view()), + path( + "api/invoices//", + invoice_views.InvoiceAPIView.as_view(), + name="invoice-detail", + ), path("admin/", admin.site.urls), path("impersonate/", include("impersonate.urls")), path("accounts/", include("allauth.urls")), diff --git a/gnuviechadmin/invoices/__init__.py b/gnuviechadmin/invoices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/invoices/admin.py b/gnuviechadmin/invoices/admin.py new file mode 100644 index 0000000..65cfb5f --- /dev/null +++ b/gnuviechadmin/invoices/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from invoices.models import Invoice + + +class InvoiceAdmin(admin.ModelAdmin): + list_display = ["invoice_number", "customer"] + readonly_fields = ["customer"] + + def has_add_permission(self, request): + return False + + +admin.site.register(Invoice, InvoiceAdmin) diff --git a/gnuviechadmin/invoices/apps.py b/gnuviechadmin/invoices/apps.py new file mode 100644 index 0000000..9062add --- /dev/null +++ b/gnuviechadmin/invoices/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class InvoiceConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "invoices" + verbose_name = _("Invoices") diff --git a/gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..0e4e652 --- /dev/null +++ b/gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,69 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: gnuviechadmin invoice\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-04-23 14:35+0200\n" +"PO-Revision-Date: 2023-04-23 14:35+0200\n" +"Last-Translator: \n" +"Language-Team: Jan Dittberner \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: invoice/apps.py:8 +msgid "Invoices" +msgstr "Rechnungen" + +#: invoice/models.py:26 +msgid "customer" +msgstr "Kunde" + +#: invoice/models.py:33 +msgid "invoice number" +msgstr "Rechnungsnummer" + +#: invoice/models.py:35 +msgid "invoice date" +msgstr "Rechnungsdatum" + +#: invoice/models.py:37 +msgid "amount" +msgstr "Betrag" + +#: invoice/models.py:40 +msgid "currency" +msgstr "Währung" + +#: invoice/models.py:42 +msgid "due date" +msgstr "Fälligkeit" + +#: invoice/models.py:44 +msgid "payment date" +msgstr "Zahlungsdatum" + +#: invoice/models.py:47 +msgid "payment variant" +msgstr "Zahlungsart" + +#: invoice/models.py:51 +msgid "invoice" +msgstr "Rechnung" + +#: invoice/models.py:52 +msgid "invoices" +msgstr "Rechnungen" + +#: invoice/models.py:56 +#, python-brace-format +msgid "Invoice {0}" +msgstr "Rechnung {0}" diff --git a/gnuviechadmin/invoices/migrations/0001_initial_invoice_model.py b/gnuviechadmin/invoices/migrations/0001_initial_invoice_model.py new file mode 100644 index 0000000..3d36e9c --- /dev/null +++ b/gnuviechadmin/invoices/migrations/0001_initial_invoice_model.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.18 on 2023-04-23 10:37 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import invoices.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('invoice', models.FileField(upload_to=invoices.models.customer_invoice_path)), + ('invoice_number', models.SlugField(max_length=10, unique=True, verbose_name='Invoice number')), + ('invoice_date', models.DateField(verbose_name='Invoice date')), + ('invoice_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')), + ('invoice_currency', models.PositiveSmallIntegerField(choices=[(1, 'EUR')], verbose_name='Currency')), + ('due_date', models.DateField(verbose_name='Due date')), + ('payment_date', models.DateField(blank=True, null=True, verbose_name='Payment date')), + ('payment_variant', models.TextField(blank=True, null=True, verbose_name='Payment variant')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='customer')), + ], + options={ + 'verbose_name': 'Invoice', + 'verbose_name_plural': 'Invoices', + 'ordering': ['-invoice_date', 'customer'], + }, + ), + ] diff --git a/gnuviechadmin/invoices/migrations/0002_refine_invoice_model.py b/gnuviechadmin/invoices/migrations/0002_refine_invoice_model.py new file mode 100644 index 0000000..1326269 --- /dev/null +++ b/gnuviechadmin/invoices/migrations/0002_refine_invoice_model.py @@ -0,0 +1,59 @@ +# Generated by Django 3.2.18 on 2023-04-23 12:15 + +import django.core.validators +from django.db import migrations, models +import invoices.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0001_initial_invoice_model'), + ] + + operations = [ + migrations.AlterModelOptions( + name='invoice', + options={'ordering': ['-invoice_date', 'customer'], 'verbose_name': 'invoice', 'verbose_name_plural': 'invoices'}, + ), + migrations.AlterField( + model_name='invoice', + name='due_date', + field=models.DateField(verbose_name='due date'), + ), + migrations.AlterField( + model_name='invoice', + name='invoice', + field=models.FileField(upload_to=invoices.models.customer_invoice_path, validators=[invoices.models.validate_pdf, django.core.validators.FileExtensionValidator(allowed_extensions=['pdf'])]), + ), + migrations.AlterField( + model_name='invoice', + name='invoice_currency', + field=models.PositiveSmallIntegerField(choices=[(1, 'EUR')], verbose_name='currency'), + ), + migrations.AlterField( + model_name='invoice', + name='invoice_date', + field=models.DateField(verbose_name='invoice date'), + ), + migrations.AlterField( + model_name='invoice', + name='invoice_number', + field=models.SlugField(max_length=10, unique=True, verbose_name='invoice number'), + ), + migrations.AlterField( + model_name='invoice', + name='invoice_value', + field=models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount'), + ), + migrations.AlterField( + model_name='invoice', + name='payment_date', + field=models.DateField(blank=True, null=True, verbose_name='payment date'), + ), + migrations.AlterField( + model_name='invoice', + name='payment_variant', + field=models.TextField(blank=True, null=True, verbose_name='payment variant'), + ), + ] diff --git a/gnuviechadmin/invoices/migrations/__init__.py b/gnuviechadmin/invoices/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/invoices/models.py b/gnuviechadmin/invoices/models.py new file mode 100644 index 0000000..8d600d3 --- /dev/null +++ b/gnuviechadmin/invoices/models.py @@ -0,0 +1,56 @@ +import magic +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.validators import FileExtensionValidator +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +CURRENCIES = [(1, "EUR")] + + +def customer_invoice_path(instance, filename): + return "invoices/{0}/{1}.pdf".format( + instance.customer.username, instance.invoice_number + ) + + +def validate_pdf(value): + valid_mime_types = ["application/pdf"] + file_mime_type = magic.from_buffer(value.read(1024), mime=True) + if file_mime_type not in valid_mime_types: + raise ValidationError("Unsupported file type.") + + +class Invoice(models.Model): + customer = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_("customer"), on_delete=models.CASCADE + ) + invoice = models.FileField( + upload_to=customer_invoice_path, + validators=[validate_pdf, FileExtensionValidator(allowed_extensions=["pdf"])], + ) + invoice_number = models.SlugField( + verbose_name=_("invoice number"), max_length=10, unique=True + ) + invoice_date = models.DateField(verbose_name=_("invoice date")) + invoice_value = models.DecimalField( + verbose_name=_("amount"), decimal_places=2, max_digits=10 + ) + invoice_currency = models.PositiveSmallIntegerField( + verbose_name=_("currency"), choices=CURRENCIES + ) + due_date = models.DateField(verbose_name=_("due date")) + payment_date = models.DateField( + verbose_name=_("payment date"), blank=True, null=True + ) + payment_variant = models.TextField( + verbose_name=_("payment variant"), blank=True, null=True + ) + + class Meta: + verbose_name = _("invoice") + verbose_name_plural = _("invoices") + ordering = ["-invoice_date", "customer"] + + def __str__(self): + return _("Invoice {0}").format(self.invoice_number) diff --git a/gnuviechadmin/invoices/serializers.py b/gnuviechadmin/invoices/serializers.py new file mode 100644 index 0000000..8506a78 --- /dev/null +++ b/gnuviechadmin/invoices/serializers.py @@ -0,0 +1,33 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from invoices.models import Invoice + +User = get_user_model() + + +class InvoiceSerializer(serializers.ModelSerializer): + customer = serializers.SlugRelatedField( + queryset=User.objects.all(), slug_field="username" + ) + + class Meta: + model = Invoice + fields = [ + "url", + "customer", + "invoice", + "invoice_number", + "invoice_date", + "invoice_value", + "invoice_currency", + "due_date", + "payment_date", + "payment_variant", + ] + extra_kwargs = { + "url": { + "lookup_field": "invoice_number", + "lookup_url_kwarg": "invoice_number", + }, + } diff --git a/gnuviechadmin/invoices/tests.py b/gnuviechadmin/invoices/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/gnuviechadmin/invoices/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gnuviechadmin/invoices/views.py b/gnuviechadmin/invoices/views.py new file mode 100644 index 0000000..60d27d1 --- /dev/null +++ b/gnuviechadmin/invoices/views.py @@ -0,0 +1,28 @@ +from django.contrib.auth import get_user_model +from rest_framework import generics + +from invoices.models import Invoice +from invoices.serializers import InvoiceSerializer + + +# Create your views here. +class ListInvoiceAPIView(generics.ListCreateAPIView): + """ + API endpoint that allows invoice to be viewed or edited. + + """ + + queryset = Invoice.objects.all().order_by("-invoice_date", "customer__username") + serializer_class = InvoiceSerializer + + +class InvoiceAPIView(generics.RetrieveUpdateAPIView): + """ + API endpoint for retrieving and updating invoices. + + """ + + queryset = Invoice.objects.all() + serializer_class = InvoiceSerializer + lookup_field = "invoice_number" + lookup_url_kwarg = "invoice_number" diff --git a/poetry.lock b/poetry.lock index 75365ed..1fa674f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1369,6 +1369,18 @@ tests = ["eradicate (>=2.0.0)", "mypy", "pylama-quotes", "pylint (>=2.11.1)", "p toml = ["toml (>=0.10.2)"] vulture = ["vulture"] +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, + {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, +] + [[package]] name = "python3-openid" version = "3.2.0" @@ -1824,4 +1836,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "4a66ce2ae06946da51bb8276bf252d41503e455db2e180c5f70dd4b9f240226a" +content-hash = "ad42dc7f52784174fcd41dd4909649b4645fac30e9e2d4105bafa78cdfcff367" diff --git a/pyproject.toml b/pyproject.toml index 8e4486d..e07ba69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ djangorestframework = "^3.14.0" markdown = "^3.4.3" django-filter = "^23.1" crispy-bootstrap5 = "^0.7" +python-magic = "^0.4.27" [tool.poetry.group.dev.dependencies]