Changes to payment models #32
|
@ -14,7 +14,6 @@ ARG REQUIREMENTS_FILE=requirements.txt
|
|||
WORKDIR /app
|
||||
|
||||
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
|
||||
COPY --chown=www:www . .
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
binutils \
|
||||
|
|
14
README.md
|
@ -84,3 +84,17 @@ make makemigrations
|
|||
```bash
|
||||
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
|
||||
```
|
||||
|
|
|
@ -12,7 +12,7 @@ authors = [
|
|||
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
|
||||
]
|
||||
dependencies = [
|
||||
"Django==5.0.7",
|
||||
"Django>=5.1b1,<5.2",
|
||||
benjaoming marked this conversation as resolved
|
||||
"django-money==3.5.2",
|
||||
"django-allauth==0.63.6",
|
||||
"psycopg[binary]==3.2.1",
|
||||
|
@ -52,7 +52,7 @@ dependencies = [
|
|||
|
||||
[[tool.hatch.envs.tests.matrix]]
|
||||
python = ["3.12"]
|
||||
django = ["5.0"]
|
||||
django = ["5.1b1"]
|
||||
|
||||
[tool.hatch.envs.tests.overrides]
|
||||
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}"
|
||||
no-cov = "cov --no-cov {args}"
|
||||
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"
|
||||
migrate = "./src/manage.py migrate"
|
||||
benjaoming marked this conversation as resolved
Outdated
benjaoming
commented
Isn't this the correct way @valberg ? Isn't this the correct way @valberg ?
valberg
commented
Yeah the one being removed is the "correct" way which I found at https://github.com/juftin/hatch-pip-compile/blob/main/docs/upgrading.md Yeah the one being removed is the "correct" way which I found at https://github.com/juftin/hatch-pip-compile/blob/main/docs/upgrading.md
benjaoming
commented
I get this error when running it: I get this error when running it: `LockFileError: Could not find lock file python version`
<img width="687" alt="image" src="/attachments/26d96dfc-0f57-4606-b140-f8d10be6aea4">
benjaoming
commented
Maybe that error is because the old "requirements.txt" I had didn't specify the Python version using the same string pattern 🤪 Notably when this was changed, the command started working:
After that, it started working. Maybe that error is because the old "requirements.txt" I had didn't specify the Python version using the same string pattern 🤪
Notably when this was changed, the command started working:
```
-# This file is autogenerated by pip-compile with Python 3.12
-# by the following command:
+# This file is autogenerated by hatch-pip-compile with Python 3.12
```
After that, it started working.
benjaoming
commented
I've restored it... I've restored it... `hatch env run --env default -- python --version; hatch env run --env dev -- python --version` is really incomprehensible compared to the old version, also funny that `hatch run dev:requirements` spawns another hatch command in another environment - but I guess that it's just part of some magic that Just Works.
|
||||
makemigrations = "./src/manage.py makemigrations"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
# - django-registries==0.0.3
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries==2.1.0
|
||||
# - django==5.0.7
|
||||
# - django<5.2,>=5.1b1
|
||||
# - environs[django]==11.0.0
|
||||
# - psycopg[binary]==3.2.1
|
||||
# - uvicorn==0.30.1
|
||||
|
@ -32,7 +32,7 @@ dj-database-url==2.2.0
|
|||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.0.7
|
||||
django==5.1b1
|
||||
# via
|
||||
# hatch.envs.default
|
||||
# dj-database-url
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# - django-registries==0.0.3
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries==2.1.0
|
||||
# - django==5.0.7
|
||||
# - django<5.2,>=5.1b1
|
||||
# - environs[django]==11.0.0
|
||||
# - psycopg[binary]==3.2.1
|
||||
# - uvicorn==0.30.1
|
||||
|
@ -45,7 +45,6 @@ click==8.1.7
|
|||
coverage==7.3.0
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# coverage
|
||||
# pytest-cov
|
||||
cryptography==42.0.8
|
||||
# via jwcrypto
|
||||
|
@ -53,7 +52,7 @@ dj-database-url==2.2.0
|
|||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.0.7
|
||||
django==5.1b1
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# dj-database-url
|
||||
|
@ -91,9 +90,7 @@ django-view-decorator==0.0.4
|
|||
django-zen-queries==2.1.0
|
||||
# via hatch.envs.dev
|
||||
environs==11.0.0
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# environs
|
||||
# via hatch.envs.dev
|
||||
h11==0.14.0
|
||||
# via uvicorn
|
||||
idna==3.7
|
||||
|
@ -124,9 +121,7 @@ pip-tools==7.3.0
|
|||
pluggy==1.5.0
|
||||
# via pytest
|
||||
psycopg==3.2.1
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# psycopg
|
||||
# via hatch.envs.dev
|
||||
psycopg-binary==3.2.1
|
||||
# via psycopg
|
||||
py-moneyed==3.0
|
||||
|
|
|
@ -1,36 +1,63 @@
|
|||
"""Admin for the accounting app."""
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Order
|
||||
from .models import Payment
|
||||
from . import models
|
||||
|
||||
|
||||
@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):
|
||||
"""Admin for the Order model."""
|
||||
|
||||
inlines = (OrderProductInline,)
|
||||
form = OrderAdminForm
|
||||
|
||||
list_display = ("who", "description", "created", "is_paid")
|
||||
|
||||
@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 instance.user.get_full_name()
|
||||
return instance.member.get_full_name()
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
@admin.register(models.Payment)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
"""Admin for the Payment model."""
|
||||
|
||||
list_display = ("who", "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()
|
||||
list_display = ("order__member", "description", "order_id", "created")
|
||||
|
||||
@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 instance.order.id
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
18
src/accounting/migrations/0007_rename_user_order_member.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -29,7 +29,7 @@ class Account(CreatedModifiedAbstract):
|
|||
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:
|
||||
return f"Account of {self.owner.get_full_name()}"
|
||||
|
@ -71,18 +71,11 @@ class Order(CreatedModifiedAbstract):
|
|||
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)
|
||||
|
||||
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"))
|
||||
|
||||
class Meta:
|
||||
|
@ -94,8 +87,13 @@ class Order(CreatedModifiedAbstract):
|
|||
|
||||
@property
|
||||
def total(self) -> Money:
|
||||
"""Return the total price of the order."""
|
||||
return self.price + self.vat
|
||||
"""Return the total price of the order (excl 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
|
||||
def display_id(self) -> str:
|
||||
|
@ -130,8 +128,8 @@ class OrderProduct(CreatedModifiedAbstract):
|
|||
This includes pricing information.
|
||||
"""
|
||||
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="ordered_products")
|
||||
product = models.ForeignKey(Product, related_name="ordered_products", on_delete=models.PROTECT)
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order_products")
|
||||
product = models.ForeignKey(Product, related_name="order_products", on_delete=models.PROTECT)
|
||||
price = 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(
|
||||
order=order,
|
||||
user=order.user,
|
||||
amount=order.total,
|
||||
amount=order.total + order.total_vat,
|
||||
description=order.description,
|
||||
payment_type=payment_type,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
It should be okay to use the new Django 5.1... then we can help out if we discover a bug :) it's rc1 now...