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_media/.empty
|
||||
!/docker/django_static/.empty
|
||||
/media/
|
||||
/static/
|
||||
|
|
|
@ -236,6 +236,7 @@ LOCAL_APPS = (
|
|||
"hostingpackages",
|
||||
"websites",
|
||||
"help",
|
||||
"invoices",
|
||||
"contact_form",
|
||||
)
|
||||
|
||||
|
|
|
@ -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_number>/",
|
||||
invoice_views.InvoiceAPIView.as_view(),
|
||||
name="invoice-detail",
|
||||
),
|
||||
path("admin/", admin.site.urls),
|
||||
path("impersonate/", include("impersonate.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)"]
|
||||
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"
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue