Implement invoice model and admin API

This commit is contained in:
Jan Dittberner 2023-04-23 14:43:44 +02:00
parent a136bcc52b
commit 0962891a9b
16 changed files with 331 additions and 1 deletions

1
.gitignore vendored
View file

@ -56,4 +56,5 @@ coverage-report/
/docker/django_static /docker/django_static
!/docker/django_media/.empty !/docker/django_media/.empty
!/docker/django_static/.empty !/docker/django_static/.empty
/media/
/static/ /static/

View file

@ -236,6 +236,7 @@ LOCAL_APPS = (
"hostingpackages", "hostingpackages",
"websites", "websites",
"help", "help",
"invoices",
"contact_form", "contact_form",
) )

View file

@ -8,6 +8,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path from django.urls import path
from help import views as help_views from help import views as help_views
from invoices import views as invoice_views
admin.autodiscover() admin.autodiscover()
@ -19,6 +20,12 @@ urlpatterns = [
help_views.HelpUserAPIView.as_view(), help_views.HelpUserAPIView.as_view(),
name="helpuser-detail", name="helpuser-detail",
), ),
path("api/invoices/", invoice_views.ListInvoiceAPIView.as_view()),
path(
"api/invoices/<invoice_number>/",
invoice_views.InvoiceAPIView.as_view(),
name="invoice-detail",
),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("impersonate/", include("impersonate.urls")), path("impersonate/", include("impersonate.urls")),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),

View file

View file

@ -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)

View file

@ -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")

View file

@ -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 <EMAIL@ADDRESS>, 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 <jan@dittberner.info>\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}"

View file

@ -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'],
},
),
]

View file

@ -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'),
),
]

View file

@ -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)

View file

@ -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",
},
}

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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"

14
poetry.lock generated
View file

@ -1369,6 +1369,18 @@ tests = ["eradicate (>=2.0.0)", "mypy", "pylama-quotes", "pylint (>=2.11.1)", "p
toml = ["toml (>=0.10.2)"] toml = ["toml (>=0.10.2)"]
vulture = ["vulture"] 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]] [[package]]
name = "python3-openid" name = "python3-openid"
version = "3.2.0" version = "3.2.0"
@ -1824,4 +1836,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "4a66ce2ae06946da51bb8276bf252d41503e455db2e180c5f70dd4b9f240226a" content-hash = "ad42dc7f52784174fcd41dd4909649b4645fac30e9e2d4105bafa78cdfcff367"

View file

@ -24,6 +24,7 @@ djangorestframework = "^3.14.0"
markdown = "^3.4.3" markdown = "^3.4.3"
django-filter = "^23.1" django-filter = "^23.1"
crispy-bootstrap5 = "^0.7" crispy-bootstrap5 = "^0.7"
python-magic = "^0.4.27"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]