unify eboutic and regular counter price selection

This commit is contained in:
imperosol
2026-03-08 16:29:48 +01:00
parent e188acc78b
commit 59d7fadf4f
6 changed files with 73 additions and 59 deletions

View File

@@ -119,4 +119,31 @@ class Migration(migrations.Migration):
migrations.RunPython(migrate_prices, reverse_code=migrations.RunPython.noop), migrations.RunPython(migrate_prices, reverse_code=migrations.RunPython.noop),
migrations.RemoveField(model_name="product", name="selling_price"), migrations.RemoveField(model_name="product", name="selling_price"),
migrations.RemoveField(model_name="product", name="special_selling_price"), migrations.RemoveField(model_name="product", name="special_selling_price"),
migrations.AlterField(
model_name="product",
name="description",
field=models.TextField(blank=True, default="", verbose_name="description"),
),
migrations.AlterField(
model_name="product",
name="product_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="products",
to="counter.producttype",
verbose_name="product type",
),
),
migrations.AlterField(
model_name="productformula",
name="result",
field=models.OneToOneField(
help_text="The product got with the formula.",
on_delete=django.db.models.deletion.CASCADE,
related_name="formula",
to="counter.product",
verbose_name="result product",
),
),
] ]

View File

@@ -22,7 +22,7 @@ import string
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from datetime import timezone as tz from datetime import timezone as tz
from decimal import Decimal from decimal import Decimal
from typing import Literal, Self from typing import TYPE_CHECKING, Literal, Self
from dict2xml import dict2xml from dict2xml import dict2xml
from django.conf import settings from django.conf import settings
@@ -47,6 +47,9 @@ from core.utils import get_start_of_semester
from counter.fields import CurrencyField from counter.fields import CurrencyField
from subscription.models import Subscription from subscription.models import Subscription
if TYPE_CHECKING:
from collections.abc import Sequence
def get_eboutic() -> Counter: def get_eboutic() -> Counter:
return Counter.objects.filter(type="EBOUTIC").order_by("id").first() return Counter.objects.filter(type="EBOUTIC").order_by("id").first()
@@ -356,13 +359,13 @@ class Product(models.Model):
QUANTITY_FOR_TRAY_PRICE = 6 QUANTITY_FOR_TRAY_PRICE = 6
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
description = models.TextField(_("description"), default="") description = models.TextField(_("description"), blank=True, default="")
product_type = models.ForeignKey( product_type = models.ForeignKey(
ProductType, ProductType,
related_name="products", related_name="products",
verbose_name=_("product type"), verbose_name=_("product type"),
null=True, null=True,
blank=True, blank=False,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
code = models.CharField(_("code"), max_length=16, blank=True) code = models.CharField(_("code"), max_length=16, blank=True)
@@ -726,13 +729,20 @@ class Counter(models.Model):
# but they share the same primary key # but they share the same primary key
return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list) return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)
def get_prices_for(self, customer: Customer) -> list[Price]: def get_prices_for(
return list( self, customer: Customer, *, order_by: Sequence[str] | None = None
Price.objects.filter(product__counters=self) ) -> list[Price]:
qs = (
Price.objects.filter(
product__counters=self, product__product_type__isnull=False
)
.for_user(customer.user) .for_user(customer.user)
.select_related("product", "product__product_type") .select_related("product", "product__product_type")
.prefetch_related("groups") .prefetch_related("groups")
) )
if order_by:
qs = qs.order_by(*order_by)
return list(qs)
class RefillingQuerySet(models.QuerySet): class RefillingQuerySet(models.QuerySet):

View File

@@ -16,7 +16,7 @@ from counter.forms import (
ScheduledProductActionForm, ScheduledProductActionForm,
ScheduledProductActionFormSet, ScheduledProductActionFormSet,
) )
from counter.models import Product, ScheduledProductAction from counter.models import Product, ProductType, ScheduledProductAction
@pytest.mark.django_db @pytest.mark.django_db
@@ -47,8 +47,7 @@ def test_create_actions_alongside_product():
form = ProductForm( form = ProductForm(
data={ data={
"name": "foo", "name": "foo",
"description": "bar", "product_type": ProductType.objects.first(),
"product_type": product.product_type_id,
"club": product.club_id, "club": product.club_id,
"code": "FOO", "code": "FOO",
"purchase_price": 1.0, "purchase_price": 1.0,
@@ -63,6 +62,7 @@ def test_create_actions_alongside_product():
"action-0-trigger_at": trigger_at, "action-0-trigger_at": trigger_at,
}, },
) )
form.is_valid()
assert form.is_valid() assert form.is_valid()
product = form.save() product = form.save()
action = ScheduledProductAction.objects.last() action = ScheduledProductAction.objects.last()

View File

@@ -39,6 +39,7 @@ from counter.models import (
Counter, Counter,
Customer, Customer,
Permanency, Permanency,
ProductType,
Refilling, Refilling,
ReturnableProduct, ReturnableProduct,
Selling, Selling,
@@ -238,31 +239,38 @@ class TestCounterClick(TestFullClickBase):
old_subscriber_group = Group.objects.get( old_subscriber_group = Group.objects.get(
id=settings.SITH_GROUP_OLD_SUBSCRIBERS_ID id=settings.SITH_GROUP_OLD_SUBSCRIBERS_ID
) )
_product_recipe = product_recipe.extend(product_type=baker.make(ProductType))
cls.gift = price_recipe.make(amount=-1.5, groups=[subscriber_group]) cls.gift = price_recipe.make(
amount=-1.5, groups=[subscriber_group], product=_product_recipe.make()
)
cls.beer = price_recipe.make( cls.beer = price_recipe.make(
groups=[subscriber_group], groups=[subscriber_group],
amount=1.5, amount=1.5,
product=product_recipe.make(limit_age=18), product=_product_recipe.make(limit_age=18),
) )
cls.beer_tap = price_recipe.make( cls.beer_tap = price_recipe.make(
groups=[subscriber_group], groups=[subscriber_group],
amount=1.5, amount=1.5,
product=product_recipe.make(limit_age=18, tray=True), product=_product_recipe.make(limit_age=18, tray=True),
) )
cls.snack = price_recipe.make( cls.snack = price_recipe.make(
groups=[subscriber_group, old_subscriber_group], groups=[subscriber_group, old_subscriber_group],
amount=1.5, amount=1.5,
product=product_recipe.make(limit_age=0), product=_product_recipe.make(limit_age=0),
) )
cls.stamps = price_recipe.make( cls.stamps = price_recipe.make(
groups=[subscriber_group], groups=[subscriber_group],
amount=1.5, amount=1.5,
product=product_recipe.make(limit_age=0), product=_product_recipe.make(limit_age=0),
) )
ReturnableProduct.objects.all().delete() ReturnableProduct.objects.all().delete()
cls.cons = price_recipe.make(amount=1, groups=[subscriber_group]) cls.cons = price_recipe.make(
cls.dcons = price_recipe.make(amount=-1, groups=[subscriber_group]) amount=1, groups=[subscriber_group], product=_product_recipe.make()
)
cls.dcons = price_recipe.make(
amount=-1, groups=[subscriber_group], product=_product_recipe.make()
)
baker.make( baker.make(
ReturnableProduct, ReturnableProduct,
product=cls.cons.product, product=cls.cons.product,
@@ -575,18 +583,17 @@ class TestCounterClick(TestFullClickBase):
group = baker.make(Group) group = baker.make(Group)
customer = baker.make(Customer) customer = baker.make(Customer)
group.users.add(customer.user) group.users.add(customer.user)
_product_recipe = product_recipe.extend(
counters=[counter], product_type=baker.make(ProductType)
)
price_recipe.make( price_recipe.make(
_quantity=2, _quantity=2,
product=iter( product=iter(_product_recipe.make(archived=True, _quantity=2)),
product_recipe.make(archived=True, counters=[counter], _quantity=2)
),
groups=[group], groups=[group],
) )
unarchived_prices = price_recipe.make( unarchived_prices = price_recipe.make(
_quantity=2, _quantity=2,
product=iter( product=iter(_product_recipe.make(archived=False, _quantity=2)),
product_recipe.make(archived=False, counters=[counter], _quantity=2)
),
groups=[group], groups=[group],
) )
customer_prices = counter.get_prices_for(customer) customer_prices = counter.get_prices_for(customer)

View File

@@ -22,7 +22,7 @@ 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, Q, Subquery, Sum from django.db.models import F, OuterRef, 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 _
@@ -39,30 +39,6 @@ from counter.models import (
) )
def get_eboutic_prices(user: User) -> list[Price]:
return list(
Price.objects.filter(
Q(is_always_shown=True, groups__in=user.all_groups)
| Q(
id=Subquery(
Price.objects.filter(
product_id=OuterRef("product_id"), groups__in=user.all_groups
)
.order_by("amount")
.values("id")[:1]
)
),
product__product_type__isnull=False,
product__archived=False,
product__limit_age__lte=user.age,
product__counters=get_eboutic(),
)
.select_related("product", "product__product_type")
.order_by("product__product_type__order", "product_id", "amount")
.distinct()
)
class BillingInfoState(Enum): class BillingInfoState(Enum):
VALID = 1 VALID = 1
EMPTY = 2 EMPTY = 2

View File

@@ -29,9 +29,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import LoginRequiredMixin
LoginRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import SuspiciousOperation, ValidationError from django.core.exceptions import SuspiciousOperation, ValidationError
from django.db import DatabaseError, transaction from django.db import DatabaseError, transaction
@@ -58,14 +56,7 @@ from counter.models import (
Selling, Selling,
get_eboutic, get_eboutic,
) )
from eboutic.models import ( from eboutic.models import Basket, BasketItem, BillingInfoState, Invoice, InvoiceItem
Basket,
BasketItem,
BillingInfoState,
Invoice,
InvoiceItem,
get_eboutic_prices,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
@@ -125,7 +116,10 @@ class EbouticMainView(LoginRequiredMixin, FormView):
@cached_property @cached_property
def prices(self) -> list[Price]: def prices(self) -> list[Price]:
return get_eboutic_prices(self.request.user) return get_eboutic().get_prices_for(
self.customer,
order_by=["product__product_type__order", "product_id", "amount"],
)
@cached_property @cached_property
def customer(self) -> Customer: def customer(self) -> Customer: