mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-13 15:15:03 +00:00
refactor InvoiceItem and BasketItem models
This commit is contained in:
@@ -22,23 +22,22 @@ from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem
|
|||||||
class BasketAdmin(admin.ModelAdmin):
|
class BasketAdmin(admin.ModelAdmin):
|
||||||
list_display = ("user", "date", "total")
|
list_display = ("user", "date", "total")
|
||||||
autocomplete_fields = ("user",)
|
autocomplete_fields = ("user",)
|
||||||
|
date_hierarchy = "date"
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_queryset(request)
|
.get_queryset(request)
|
||||||
.annotate(
|
.annotate(
|
||||||
total=Sum(
|
total=Sum(F("items__quantity") * F("items__unit_price"), default=0)
|
||||||
F("items__quantity") * F("items__product_unit_price"), default=0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(BasketItem)
|
@admin.register(BasketItem)
|
||||||
class BasketItemAdmin(admin.ModelAdmin):
|
class BasketItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ("basket", "product_name", "product_unit_price", "quantity")
|
list_display = ("label", "unit_price", "quantity")
|
||||||
search_fields = ("product_name",)
|
search_fields = ("label",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Invoice)
|
@admin.register(Invoice)
|
||||||
@@ -50,5 +49,6 @@ class InvoiceAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(InvoiceItem)
|
@admin.register(InvoiceItem)
|
||||||
class InvoiceItemAdmin(admin.ModelAdmin):
|
class InvoiceItemAdmin(admin.ModelAdmin):
|
||||||
list_display = ("invoice", "product_name", "product_unit_price", "quantity")
|
list_display = ("label", "unit_price", "quantity")
|
||||||
search_fields = ("product_name",)
|
search_fields = ("label",)
|
||||||
|
list_select_related = ("price",)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 5.2.11 on 2026-02-22 18:13
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("counter", "0038_price"), ("eboutic", "0002_auto_20221005_2243")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="basketitem", old_name="product_name", new_name="label"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="basketitem",
|
||||||
|
old_name="product_unit_price",
|
||||||
|
new_name="unit_price",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="basketitem", old_name="product_id", new_name="product"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="invoiceitem", old_name="product_name", new_name="label"
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="invoiceitem",
|
||||||
|
old_name="product_unit_price",
|
||||||
|
new_name="unit_price",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="invoiceitem", old_name="product_id", new_name="product"
|
||||||
|
),
|
||||||
|
migrations.RemoveField(model_name="basketitem", name="type_id"),
|
||||||
|
migrations.RemoveField(model_name="invoiceitem", name="type_id"),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="basketitem",
|
||||||
|
name="product",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="counter.product",
|
||||||
|
verbose_name="product",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="invoiceitem",
|
||||||
|
name="product",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="counter.product",
|
||||||
|
verbose_name="product",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -17,12 +17,12 @@ from __future__ import annotations
|
|||||||
import hmac
|
import hmac
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Self
|
from typing import Self
|
||||||
|
|
||||||
from dict2xml import dict2xml
|
from dict2xml import dict2xml
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import DataError, models
|
from django.db import DataError, models
|
||||||
from django.db.models import F, OuterRef, Subquery, Sum
|
from django.db.models import F, OuterRef, Q, Subquery, Sum
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ from core.models import User
|
|||||||
from counter.fields import CurrencyField
|
from counter.fields import CurrencyField
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
BillingInfo,
|
BillingInfo,
|
||||||
Counter,
|
|
||||||
Customer,
|
Customer,
|
||||||
|
Price,
|
||||||
Product,
|
Product,
|
||||||
Refilling,
|
Refilling,
|
||||||
Selling,
|
Selling,
|
||||||
@@ -39,20 +39,28 @@ from counter.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_eboutic_products(user: User) -> list[Product]:
|
def get_eboutic_prices(user: User) -> list[Price]:
|
||||||
products = (
|
return list(
|
||||||
get_eboutic()
|
Price.objects.filter(
|
||||||
.products.filter(product_type__isnull=False)
|
Q(is_always_shown=True, groups__in=user.all_groups)
|
||||||
.filter(archived=False, limit_age__lte=user.age)
|
| Q(
|
||||||
.annotate(
|
id=Subquery(
|
||||||
order=F("product_type__order"),
|
Price.objects.filter(
|
||||||
category=F("product_type__name"),
|
product_id=OuterRef("product_id"), groups__in=user.all_groups
|
||||||
category_comment=F("product_type__comment"),
|
)
|
||||||
price=F("selling_price"), # <-- selected price for basket validation
|
.order_by("amount")
|
||||||
|
.values("id")[:1]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
product__product_type__isnull=False,
|
||||||
|
product__archived=False,
|
||||||
|
product__limit_age__lte=user.age,
|
||||||
|
product__counters=get_eboutic(),
|
||||||
)
|
)
|
||||||
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
|
.select_related("product", "product__product_type")
|
||||||
|
.order_by("product__product_type__order", "product_id", "amount")
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
return [p for p in products if p.can_be_sold_to(user)]
|
|
||||||
|
|
||||||
|
|
||||||
class BillingInfoState(Enum):
|
class BillingInfoState(Enum):
|
||||||
@@ -94,21 +102,21 @@ class Basket(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user: User):
|
||||||
return self.user == user
|
return self.user == user
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def contains_refilling_item(self) -> bool:
|
def contains_refilling_item(self) -> bool:
|
||||||
return self.items.filter(
|
return self.items.filter(
|
||||||
type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
product__product_type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def total(self) -> float:
|
def total(self) -> float:
|
||||||
return float(
|
return float(
|
||||||
self.items.aggregate(
|
self.items.aggregate(total=Sum(F("quantity") * F("unit_price"), default=0))[
|
||||||
total=Sum(F("quantity") * F("product_unit_price"), default=0)
|
"total"
|
||||||
)["total"]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_sales(
|
def generate_sales(
|
||||||
@@ -120,7 +128,8 @@ class Basket(models.Model):
|
|||||||
Example:
|
Example:
|
||||||
```python
|
```python
|
||||||
counter = Counter.objects.get(name="Eboutic")
|
counter = Counter.objects.get(name="Eboutic")
|
||||||
sales = basket.generate_sales(counter, "SITH_ACCOUNT")
|
user = User.objects.get(username="bibou")
|
||||||
|
sales = basket.generate_sales(counter, user, Selling.PaymentMethod.SITH_ACCOUNT)
|
||||||
# here the basket is in the same state as before the method call
|
# here the basket is in the same state as before the method call
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -131,31 +140,23 @@ class Basket(models.Model):
|
|||||||
# thus only the sales remain
|
# thus only the sales remain
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
# I must proceed with two distinct requests instead of
|
customer = Customer.get_or_create(self.user)[0]
|
||||||
# only one with a join because the AbstractBaseItem model has been
|
return [
|
||||||
# poorly designed. If you refactor the model, please refactor this too.
|
Selling(
|
||||||
items = self.items.order_by("product_id")
|
label=item.label,
|
||||||
ids = [item.product_id for item in items]
|
counter=counter,
|
||||||
products = Product.objects.filter(id__in=ids).order_by("id")
|
club_id=item.product.club_id,
|
||||||
# items and products are sorted in the same order
|
product=item.product,
|
||||||
sales = []
|
seller=seller,
|
||||||
for item, product in zip(items, products, strict=False):
|
customer=customer,
|
||||||
sales.append(
|
unit_price=item.unit_price,
|
||||||
Selling(
|
quantity=item.quantity,
|
||||||
label=product.name,
|
payment_method=payment_method,
|
||||||
counter=counter,
|
|
||||||
club=product.club,
|
|
||||||
product=product,
|
|
||||||
seller=seller,
|
|
||||||
customer=Customer.get_or_create(self.user)[0],
|
|
||||||
unit_price=item.product_unit_price,
|
|
||||||
quantity=item.quantity,
|
|
||||||
payment_method=payment_method,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return sales
|
for item in self.items.select_related("product")
|
||||||
|
]
|
||||||
|
|
||||||
def get_e_transaction_data(self) -> list[tuple[str, Any]]:
|
def get_e_transaction_data(self) -> list[tuple[str, str]]:
|
||||||
user = self.user
|
user = self.user
|
||||||
if not hasattr(user, "customer"):
|
if not hasattr(user, "customer"):
|
||||||
raise Customer.DoesNotExist
|
raise Customer.DoesNotExist
|
||||||
@@ -201,7 +202,7 @@ class InvoiceQueryset(models.QuerySet):
|
|||||||
def annotate_total(self) -> Self:
|
def annotate_total(self) -> Self:
|
||||||
"""Annotate the queryset with the total amount of each invoice.
|
"""Annotate the queryset with the total amount of each invoice.
|
||||||
|
|
||||||
The total amount is the sum of (product_unit_price * quantity)
|
The total amount is the sum of (unit_price * quantity)
|
||||||
for all items related to the invoice.
|
for all items related to the invoice.
|
||||||
"""
|
"""
|
||||||
# aggregates within subqueries require a little bit of black magic,
|
# aggregates within subqueries require a little bit of black magic,
|
||||||
@@ -211,7 +212,7 @@ class InvoiceQueryset(models.QuerySet):
|
|||||||
total=Subquery(
|
total=Subquery(
|
||||||
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
|
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
|
||||||
.values("invoice_id")
|
.values("invoice_id")
|
||||||
.annotate(total=Sum(F("product_unit_price") * F("quantity")))
|
.annotate(total=Sum(F("unit_price") * F("quantity")))
|
||||||
.values("total")
|
.values("total")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -221,11 +222,7 @@ class Invoice(models.Model):
|
|||||||
"""Invoices are generated once the payment has been validated."""
|
"""Invoices are generated once the payment has been validated."""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User,
|
User, related_name="invoices", verbose_name=_("user"), on_delete=models.CASCADE
|
||||||
related_name="invoices",
|
|
||||||
verbose_name=_("user"),
|
|
||||||
blank=False,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
)
|
||||||
date = models.DateTimeField(_("date"), auto_now=True)
|
date = models.DateTimeField(_("date"), auto_now=True)
|
||||||
validated = models.BooleanField(_("validated"), default=False)
|
validated = models.BooleanField(_("validated"), default=False)
|
||||||
@@ -246,53 +243,44 @@ class Invoice(models.Model):
|
|||||||
if self.validated:
|
if self.validated:
|
||||||
raise DataError(_("Invoice already validated"))
|
raise DataError(_("Invoice already validated"))
|
||||||
customer, _created = Customer.get_or_create(user=self.user)
|
customer, _created = Customer.get_or_create(user=self.user)
|
||||||
eboutic = Counter.objects.filter(type="EBOUTIC").first()
|
kwargs = {
|
||||||
for i in self.items.all():
|
"counter": get_eboutic(),
|
||||||
if i.type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
|
"customer": customer,
|
||||||
new = Refilling(
|
"date": self.date,
|
||||||
counter=eboutic,
|
"payment_method": Selling.PaymentMethod.CARD,
|
||||||
customer=customer,
|
}
|
||||||
operator=self.user,
|
for i in self.items.select_related("product"):
|
||||||
amount=i.product_unit_price * i.quantity,
|
if i.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
|
||||||
payment_method=Refilling.PaymentMethod.CARD,
|
Refilling.objects.create(
|
||||||
date=self.date,
|
**kwargs, operator=self.user, amount=i.unit_price * i.quantity
|
||||||
)
|
)
|
||||||
new.save()
|
|
||||||
else:
|
else:
|
||||||
product = Product.objects.filter(id=i.product_id).first()
|
Selling.objects.create(
|
||||||
new = Selling(
|
**kwargs,
|
||||||
label=i.product_name,
|
label=i.label,
|
||||||
counter=eboutic,
|
club_id=i.product.club_id,
|
||||||
club=product.club,
|
product=i.product,
|
||||||
product=product,
|
|
||||||
seller=self.user,
|
seller=self.user,
|
||||||
customer=customer,
|
unit_price=i.unit_price,
|
||||||
unit_price=i.product_unit_price,
|
|
||||||
quantity=i.quantity,
|
quantity=i.quantity,
|
||||||
payment_method=Selling.PaymentMethod.CARD,
|
|
||||||
date=self.date,
|
|
||||||
)
|
)
|
||||||
new.save()
|
|
||||||
self.validated = True
|
self.validated = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class AbstractBaseItem(models.Model):
|
class AbstractBaseItem(models.Model):
|
||||||
product_id = models.IntegerField(_("product id"))
|
product = models.ForeignKey(
|
||||||
product_name = models.CharField(_("product name"), max_length=255)
|
Product, verbose_name=_("product"), on_delete=models.PROTECT
|
||||||
type_id = models.IntegerField(_("product type id"))
|
)
|
||||||
product_unit_price = CurrencyField(_("unit price"))
|
label = models.CharField(_("product name"), max_length=255)
|
||||||
|
unit_price = CurrencyField(_("unit price"))
|
||||||
quantity = models.PositiveIntegerField(_("quantity"))
|
quantity = models.PositiveIntegerField(_("quantity"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Item: %s (%s) x%d" % (
|
return "Item: %s (%s) x%d" % (self.product.name, self.unit_price, self.quantity)
|
||||||
self.product_name,
|
|
||||||
self.product_unit_price,
|
|
||||||
self.quantity,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BasketItem(AbstractBaseItem):
|
class BasketItem(AbstractBaseItem):
|
||||||
@@ -301,21 +289,16 @@ class BasketItem(AbstractBaseItem):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_product(cls, product: Product, quantity: int, basket: Basket):
|
def from_price(cls, price: Price, quantity: int, basket: Basket):
|
||||||
"""Create a BasketItem with the same characteristics as the
|
"""Create a BasketItem with the same characteristics as the
|
||||||
product passed in parameters, with the specified quantity.
|
product price passed in parameters, with the specified quantity.
|
||||||
|
|
||||||
Warning:
|
|
||||||
the basket field is not filled, so you must set
|
|
||||||
it yourself before saving the model.
|
|
||||||
"""
|
"""
|
||||||
return cls(
|
return cls(
|
||||||
basket=basket,
|
basket=basket,
|
||||||
product_id=product.id,
|
label=price.full_label,
|
||||||
product_name=product.name,
|
product_id=price.product_id,
|
||||||
type_id=product.product_type_id,
|
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
product_unit_price=product.selling_price,
|
unit_price=price.amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user