Implement invoice model and admin API
This commit is contained in:
parent
a136bcc52b
commit
0962891a9b
16 changed files with 331 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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/
|
||||||
|
|
|
@ -236,6 +236,7 @@ LOCAL_APPS = (
|
||||||
"hostingpackages",
|
"hostingpackages",
|
||||||
"websites",
|
"websites",
|
||||||
"help",
|
"help",
|
||||||
|
"invoices",
|
||||||
"contact_form",
|
"contact_form",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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")),
|
||||||
|
|
0
gnuviechadmin/invoices/__init__.py
Normal file
0
gnuviechadmin/invoices/__init__.py
Normal file
14
gnuviechadmin/invoices/admin.py
Normal file
14
gnuviechadmin/invoices/admin.py
Normal 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)
|
8
gnuviechadmin/invoices/apps.py
Normal file
8
gnuviechadmin/invoices/apps.py
Normal 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")
|
69
gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po
Normal file
69
gnuviechadmin/invoices/locale/de/LC_MESSAGES/django.po
Normal 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}"
|
|
@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
0
gnuviechadmin/invoices/migrations/__init__.py
Normal file
0
gnuviechadmin/invoices/migrations/__init__.py
Normal file
56
gnuviechadmin/invoices/models.py
Normal file
56
gnuviechadmin/invoices/models.py
Normal 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)
|
33
gnuviechadmin/invoices/serializers.py
Normal file
33
gnuviechadmin/invoices/serializers.py
Normal 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",
|
||||||
|
},
|
||||||
|
}
|
3
gnuviechadmin/invoices/tests.py
Normal file
3
gnuviechadmin/invoices/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
28
gnuviechadmin/invoices/views.py
Normal file
28
gnuviechadmin/invoices/views.py
Normal 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
14
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue