Reorganize Order fields, add admin, Upgrade to Django 5.1b1
Some checks failed
continuous-integration/drone/pr Build is failing

This commit is contained in:
Benjamin Bach 2024-07-21 19:07:59 +02:00
parent e27bc7969d
commit ee2ef48b96
No known key found for this signature in database
GPG key ID: 486F0D69C845416E
12 changed files with 262 additions and 42 deletions

View file

@ -14,7 +14,6 @@ ARG REQUIREMENTS_FILE=requirements.txt
WORKDIR /app WORKDIR /app
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
COPY --chown=www:www . .
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y \ apt-get install -y \
binutils \ binutils \

View file

@ -84,3 +84,17 @@ make makemigrations
```bash ```bash
hatch run dev:server hatch run dev:server
``` ```
#### Upgrade requirements
We use hatch-pip-compile.
```bash
# Install hatch-pip-compile in some environment
pipx install hatch-pip-compile
# Change requirements in pyproject.toml (...)
# Update requirements.txt:
hatch-pip-compile
# Update requirements/requirements-dev.txt:
hatch-pip-compile dev
```

View file

@ -12,7 +12,7 @@ authors = [
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" }, { name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
] ]
dependencies = [ dependencies = [
"Django==5.0.7", "Django>=5.1b1,<5.2",
"django-money==3.5.2", "django-money==3.5.2",
"django-allauth==0.63.6", "django-allauth==0.63.6",
"psycopg[binary]==3.2.1", "psycopg[binary]==3.2.1",
@ -52,7 +52,7 @@ dependencies = [
[[tool.hatch.envs.tests.matrix]] [[tool.hatch.envs.tests.matrix]]
python = ["3.12"] python = ["3.12"]
django = ["5.0"] django = ["5.1b1"]
[tool.hatch.envs.tests.overrides] [tool.hatch.envs.tests.overrides]
matrix.django.dependencies = [ matrix.django.dependencies = [
@ -66,7 +66,7 @@ matrix.python.dependencies = [
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}" cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
no-cov = "cov --no-cov {args}" no-cov = "cov --no-cov {args}"
typecheck = "mypy --config-file=pyproject.toml ." typecheck = "mypy --config-file=pyproject.toml ."
requirements = "pip-compile --output-file requirements/base.txt pyproject.toml" # requirements = "pip-compile --output-file requirements/base.txt pyproject.toml"
server = "./src/manage.py runserver 0.0.0.0:8000" server = "./src/manage.py runserver 0.0.0.0:8000"
migrate = "./src/manage.py migrate" migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations" makemigrations = "./src/manage.py makemigrations"

View file

@ -7,7 +7,7 @@
# - django-registries==0.0.3 # - django-registries==0.0.3
# - django-view-decorator==0.0.4 # - django-view-decorator==0.0.4
# - django-zen-queries==2.1.0 # - django-zen-queries==2.1.0
# - django==5.0.7 # - django<5.2,>=5.1b1
# - environs[django]==11.0.0 # - environs[django]==11.0.0
# - psycopg[binary]==3.2.1 # - psycopg[binary]==3.2.1
# - uvicorn==0.30.1 # - uvicorn==0.30.1
@ -32,7 +32,7 @@ dj-database-url==2.2.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.0.7 django==5.1b1
# via # via
# hatch.envs.default # hatch.envs.default
# dj-database-url # dj-database-url

View file

@ -17,7 +17,7 @@
# - django-registries==0.0.3 # - django-registries==0.0.3
# - django-view-decorator==0.0.4 # - django-view-decorator==0.0.4
# - django-zen-queries==2.1.0 # - django-zen-queries==2.1.0
# - django==5.0.7 # - django<5.2,>=5.1b1
# - environs[django]==11.0.0 # - environs[django]==11.0.0
# - psycopg[binary]==3.2.1 # - psycopg[binary]==3.2.1
# - uvicorn==0.30.1 # - uvicorn==0.30.1
@ -45,7 +45,6 @@ click==8.1.7
coverage==7.3.0 coverage==7.3.0
# via # via
# hatch.envs.dev # hatch.envs.dev
# coverage
# pytest-cov # pytest-cov
cryptography==42.0.8 cryptography==42.0.8
# via jwcrypto # via jwcrypto
@ -53,7 +52,7 @@ dj-database-url==2.2.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.0.7 django==5.1b1
# via # via
# hatch.envs.dev # hatch.envs.dev
# dj-database-url # dj-database-url
@ -91,9 +90,7 @@ django-view-decorator==0.0.4
django-zen-queries==2.1.0 django-zen-queries==2.1.0
# via hatch.envs.dev # via hatch.envs.dev
environs==11.0.0 environs==11.0.0
# via # via hatch.envs.dev
# hatch.envs.dev
# environs
h11==0.14.0 h11==0.14.0
# via uvicorn # via uvicorn
idna==3.7 idna==3.7
@ -124,9 +121,7 @@ pip-tools==7.3.0
pluggy==1.5.0 pluggy==1.5.0
# via pytest # via pytest
psycopg==3.2.1 psycopg==3.2.1
# via # via hatch.envs.dev
# hatch.envs.dev
# psycopg
psycopg-binary==3.2.1 psycopg-binary==3.2.1
# via psycopg # via psycopg
py-moneyed==3.0 py-moneyed==3.0

View file

@ -1,36 +1,63 @@
"""Admin for the accounting app.""" """Admin for the accounting app."""
from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Order from . import models
from .models import Payment
@admin.register(Order) class OrderProductInline(admin.TabularInline):
"""Administer contents of an order inline."""
model = models.OrderProduct
class OrderAdminForm(forms.ModelForm):
"""Special Form for the OrderAdmin so we don't need to require the account field."""
account = forms.ModelChoiceField(
required=False,
queryset=models.Account.objects.all(),
help_text=_("Leave empty to auto-choose the member's own account or to create one."),
)
class Meta:
model = models.Order
exclude = () # noqa: DJ006
def clean(self): # noqa: D102, ANN201
cd = super().clean()
if not cd["account"] and cd["member"]:
try:
cd["account"] = models.Account.objects.get_or_create(owner=cd["member"])[0]
except models.Account.MultipleObjectsReturned:
cd["account"] = models.Account.objects.filter(owner=cd["member"]).first()
return cd
@admin.register(models.Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
"""Admin for the Order model.""" """Admin for the Order model."""
inlines = (OrderProductInline,)
form = OrderAdminForm
list_display = ("who", "description", "created", "is_paid") list_display = ("who", "description", "created", "is_paid")
@admin.display(description=_("Customer")) @admin.display(description=_("Customer"))
def who(self, instance: Order) -> str: def who(self, instance: models.Order) -> str:
"""Return the full name of the user who made the order.""" """Return the full name of the user who made the order."""
return instance.user.get_full_name() return instance.member.get_full_name()
@admin.register(Payment) @admin.register(models.Payment)
class PaymentAdmin(admin.ModelAdmin): class PaymentAdmin(admin.ModelAdmin):
"""Admin for the Payment model.""" """Admin for the Payment model."""
list_display = ("who", "description", "order_id", "created") list_display = ("order__member", "description", "order_id", "created")
@admin.display(description=_("Customer"))
def who(self, instance: Payment) -> str:
"""Return the full name of the user who made the payment."""
return instance.order.user.get_full_name()
@admin.display(description=_("Order ID")) @admin.display(description=_("Order ID"))
def order_id(self, instance: Payment) -> int: def order_id(self, instance: models.Payment) -> int:
"""Return the ID of the order.""" """Return the ID of the order."""
return instance.order.id return instance.order.id

View file

@ -0,0 +1,78 @@
# Generated by Django 5.0.7 on 2024-07-21 14:12
import django.db.models.deletion
import djmoney.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0003_alter_payment_stripe_charge_id'),
]
operations = [
migrations.CreateModel(
name='PaymentType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
('name', models.CharField(max_length=1024, verbose_name='description')),
('description', models.TextField(blank=True, max_length=2048)),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
('name', models.CharField(max_length=512)),
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='payment',
name='external_transaction_id',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AlterField(
model_name='payment',
name='stripe_charge_id',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='payment',
name='payment_type',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounting.paymenttype'),
preserve_default=False,
),
migrations.CreateModel(
name='OrderProduct',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='oprettet')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_products', to='accounting.order')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ordered_products', to='accounting.product')),
],
options={
'abstract': False,
},
),
]

View file

@ -0,0 +1,40 @@
# Generated by Django 5.0.7 on 2024-07-21 14:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0004_paymenttype_product_payment_external_transaction_id_and_more'),
]
operations = [
migrations.RemoveField(
model_name='order',
name='price',
),
migrations.RemoveField(
model_name='order',
name='price_currency',
),
migrations.RemoveField(
model_name='order',
name='vat',
),
migrations.RemoveField(
model_name='order',
name='vat_currency',
),
migrations.AlterField(
model_name='orderproduct',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_products', to='accounting.order'),
),
migrations.AlterField(
model_name='orderproduct',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_products', to='accounting.product'),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 5.0.7 on 2024-07-21 15:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0005_remove_order_price_remove_order_price_currency_and_more'),
('membership', '0007_membershiptype_current_membershiptype_product'),
]
operations = [
migrations.AlterField(
model_name='account',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'),
),
migrations.AlterField(
model_name='order',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.7 on 2024-07-21 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounting', '0006_alter_account_owner_alter_order_user'),
]
operations = [
migrations.RenameField(
model_name='order',
old_name='user',
new_name='member',
),
]

View file

@ -29,7 +29,7 @@ class Account(CreatedModifiedAbstract):
can decide which account to use to pay for something. can decide which account to use to pay for something.
""" """
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT) owner = models.ForeignKey("membership.Member", on_delete=models.PROTECT)
def __str__(self) -> str: def __str__(self) -> str:
return f"Account of {self.owner.get_full_name()}" return f"Account of {self.owner.get_full_name()}"
@ -71,18 +71,11 @@ class Order(CreatedModifiedAbstract):
considered locked. considered locked.
""" """
user = models.ForeignKey("auth.User", on_delete=models.PROTECT) member = models.ForeignKey("membership.Member", on_delete=models.PROTECT)
account = models.ForeignKey(Account, on_delete=models.PROTECT) account = models.ForeignKey(Account, on_delete=models.PROTECT)
description = models.CharField(max_length=1024, verbose_name=_("description")) description = models.CharField(max_length=1024, verbose_name=_("description"))
price = MoneyField(
verbose_name=_("price (excl. VAT)"),
max_digits=16,
decimal_places=2,
)
vat = MoneyField(verbose_name=_("VAT"), max_digits=16, decimal_places=2)
is_paid = models.BooleanField(default=False, verbose_name=_("is paid")) is_paid = models.BooleanField(default=False, verbose_name=_("is paid"))
class Meta: class Meta:
@ -94,8 +87,13 @@ class Order(CreatedModifiedAbstract):
@property @property
def total(self) -> Money: def total(self) -> Money:
"""Return the total price of the order.""" """Return the total price of the order (excl VAT)."""
return self.price + self.vat return sum(order_product.price for order_product in self.order_products)
@property
def total_vat(self) -> Money:
"""Return the total VAT of the order."""
return sum(order_product.vat for order_product in self.order_products)
@property @property
def display_id(self) -> str: def display_id(self) -> str:
@ -130,8 +128,8 @@ class OrderProduct(CreatedModifiedAbstract):
This includes pricing information. This includes pricing information.
""" """
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="ordered_products") order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order_products")
product = models.ForeignKey(Product, related_name="ordered_products", on_delete=models.PROTECT) product = models.ForeignKey(Product, related_name="order_products", on_delete=models.PROTECT)
price = MoneyField(max_digits=16, decimal_places=2) price = MoneyField(max_digits=16, decimal_places=2)
vat = MoneyField(max_digits=16, decimal_places=2) vat = MoneyField(max_digits=16, decimal_places=2)
@ -170,7 +168,7 @@ class Payment(CreatedModifiedAbstract):
return cls.objects.create( return cls.objects.create(
order=order, order=order,
user=order.user, user=order.user,
amount=order.total, amount=order.total + order.total_vat,
description=order.description, description=order.description,
payment_type=payment_type, payment_type=payment_type,
) )

View file

@ -0,0 +1,26 @@
# Generated by Django 5.0.7 on 2024-07-21 14:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0004_paymenttype_product_payment_external_transaction_id_and_more'),
('membership', '0006_waitinglistentry_alter_membership_options'),
]
operations = [
migrations.AddField(
model_name='membershiptype',
name='current',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='membershiptype',
name='product',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounting.product'),
preserve_default=False,
),
]