diff --git a/counter/migrations/0038_price.py b/counter/migrations/0038_price.py index 5a06100e..a52eda4f 100644 --- a/counter/migrations/0038_price.py +++ b/counter/migrations/0038_price.py @@ -119,4 +119,31 @@ class Migration(migrations.Migration): migrations.RunPython(migrate_prices, reverse_code=migrations.RunPython.noop), migrations.RemoveField(model_name="product", name="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", + ), + ), ] diff --git a/counter/models.py b/counter/models.py index a60fff59..e3d9113d 100644 --- a/counter/models.py +++ b/counter/models.py @@ -22,7 +22,7 @@ import string from datetime import date, datetime, timedelta from datetime import timezone as tz from decimal import Decimal -from typing import Literal, Self +from typing import TYPE_CHECKING, Literal, Self from dict2xml import dict2xml from django.conf import settings @@ -47,6 +47,9 @@ from core.utils import get_start_of_semester from counter.fields import CurrencyField from subscription.models import Subscription +if TYPE_CHECKING: + from collections.abc import Sequence + def get_eboutic() -> Counter: return Counter.objects.filter(type="EBOUTIC").order_by("id").first() @@ -356,13 +359,13 @@ class Product(models.Model): QUANTITY_FOR_TRAY_PRICE = 6 name = models.CharField(_("name"), max_length=64) - description = models.TextField(_("description"), default="") + description = models.TextField(_("description"), blank=True, default="") product_type = models.ForeignKey( ProductType, related_name="products", verbose_name=_("product type"), null=True, - blank=True, + blank=False, on_delete=models.SET_NULL, ) code = models.CharField(_("code"), max_length=16, blank=True) @@ -726,13 +729,20 @@ class Counter(models.Model): # but they share the same primary key 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]: - return list( - Price.objects.filter(product__counters=self) + def get_prices_for( + self, customer: Customer, *, order_by: Sequence[str] | None = None + ) -> list[Price]: + qs = ( + Price.objects.filter( + product__counters=self, product__product_type__isnull=False + ) .for_user(customer.user) .select_related("product", "product__product_type") .prefetch_related("groups") ) + if order_by: + qs = qs.order_by(*order_by) + return list(qs) class RefillingQuerySet(models.QuerySet): diff --git a/counter/tests/test_auto_actions.py b/counter/tests/test_auto_actions.py index 27a897c1..0d241c3a 100644 --- a/counter/tests/test_auto_actions.py +++ b/counter/tests/test_auto_actions.py @@ -16,7 +16,7 @@ from counter.forms import ( ScheduledProductActionForm, ScheduledProductActionFormSet, ) -from counter.models import Product, ScheduledProductAction +from counter.models import Product, ProductType, ScheduledProductAction @pytest.mark.django_db @@ -47,8 +47,7 @@ def test_create_actions_alongside_product(): form = ProductForm( data={ "name": "foo", - "description": "bar", - "product_type": product.product_type_id, + "product_type": ProductType.objects.first(), "club": product.club_id, "code": "FOO", "purchase_price": 1.0, @@ -63,6 +62,7 @@ def test_create_actions_alongside_product(): "action-0-trigger_at": trigger_at, }, ) + form.is_valid() assert form.is_valid() product = form.save() action = ScheduledProductAction.objects.last() diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 1f689087..a195c1f9 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -39,6 +39,7 @@ from counter.models import ( Counter, Customer, Permanency, + ProductType, Refilling, ReturnableProduct, Selling, @@ -238,31 +239,38 @@ class TestCounterClick(TestFullClickBase): old_subscriber_group = Group.objects.get( 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( groups=[subscriber_group], amount=1.5, - product=product_recipe.make(limit_age=18), + product=_product_recipe.make(limit_age=18), ) cls.beer_tap = price_recipe.make( groups=[subscriber_group], 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( groups=[subscriber_group, old_subscriber_group], amount=1.5, - product=product_recipe.make(limit_age=0), + product=_product_recipe.make(limit_age=0), ) cls.stamps = price_recipe.make( groups=[subscriber_group], amount=1.5, - product=product_recipe.make(limit_age=0), + product=_product_recipe.make(limit_age=0), ) ReturnableProduct.objects.all().delete() - cls.cons = price_recipe.make(amount=1, groups=[subscriber_group]) - cls.dcons = price_recipe.make(amount=-1, groups=[subscriber_group]) + cls.cons = price_recipe.make( + 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( ReturnableProduct, product=cls.cons.product, @@ -575,18 +583,17 @@ class TestCounterClick(TestFullClickBase): group = baker.make(Group) customer = baker.make(Customer) group.users.add(customer.user) + _product_recipe = product_recipe.extend( + counters=[counter], product_type=baker.make(ProductType) + ) price_recipe.make( _quantity=2, - product=iter( - product_recipe.make(archived=True, counters=[counter], _quantity=2) - ), + product=iter(_product_recipe.make(archived=True, _quantity=2)), groups=[group], ) unarchived_prices = price_recipe.make( _quantity=2, - product=iter( - product_recipe.make(archived=False, counters=[counter], _quantity=2) - ), + product=iter(_product_recipe.make(archived=False, _quantity=2)), groups=[group], ) customer_prices = counter.get_prices_for(customer) diff --git a/eboutic/models.py b/eboutic/models.py index a5c42bb8..cf6e15ab 100644 --- a/eboutic/models.py +++ b/eboutic/models.py @@ -22,7 +22,7 @@ from typing import Self from dict2xml import dict2xml from django.conf import settings 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.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): VALID = 1 EMPTY = 2 diff --git a/eboutic/views.py b/eboutic/views.py index efea3f89..a1c4c38c 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -29,9 +29,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import ( - LoginRequiredMixin, -) +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import SuspiciousOperation, ValidationError from django.db import DatabaseError, transaction @@ -58,14 +56,7 @@ from counter.models import ( Selling, get_eboutic, ) -from eboutic.models import ( - Basket, - BasketItem, - BillingInfoState, - Invoice, - InvoiceItem, - get_eboutic_prices, -) +from eboutic.models import Basket, BasketItem, BillingInfoState, Invoice, InvoiceItem if TYPE_CHECKING: from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey @@ -125,7 +116,10 @@ class EbouticMainView(LoginRequiredMixin, FormView): @cached_property 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 def customer(self) -> Customer: