From 1c6d7f435ac30ae921d85e542d157182e25f61f0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 2 Mar 2026 15:32:57 +0100 Subject: [PATCH 01/13] Price model --- counter/admin.py | 8 ++- counter/api.py | 8 +-- counter/migrations/0039_price.py | 108 +++++++++++++++++++++++++++++++ counter/models.py | 76 +++++++++++++++++++++- counter/schemas.py | 13 ++-- 5 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 counter/migrations/0039_price.py diff --git a/counter/admin.py b/counter/admin.py index 425134ef..5caab364 100644 --- a/counter/admin.py +++ b/counter/admin.py @@ -24,6 +24,7 @@ from counter.models import ( Eticket, InvoiceCall, Permanency, + Price, Product, ProductType, Refilling, @@ -32,19 +33,24 @@ from counter.models import ( ) +class PriceInline(admin.TabularInline): + model = Price + autocomplete_fields = ("groups",) + + @admin.register(Product) class ProductAdmin(SearchModelAdmin): list_display = ( "name", "code", "product_type", - "selling_price", "archived", "created_at", "updated_at", ) list_select_related = ("product_type",) search_fields = ("name", "code") + inlines = [PriceInline] @admin.register(ReturnableProduct) diff --git a/counter/api.py b/counter/api.py index 72da74e5..9ab60437 100644 --- a/counter/api.py +++ b/counter/api.py @@ -101,13 +101,9 @@ class ProductController(ControllerBase): """Get the detailed information about the products.""" return filters.filter( Product.objects.select_related("club") - .prefetch_related("buying_groups") + .prefetch_related("prices", "prices__groups") .select_related("product_type") - .order_by( - F("product_type__order").asc(nulls_last=True), - "product_type", - "name", - ) + .order_by(F("product_type__order").asc(nulls_last=True), "name") ) diff --git a/counter/migrations/0039_price.py b/counter/migrations/0039_price.py new file mode 100644 index 00000000..19db78d5 --- /dev/null +++ b/counter/migrations/0039_price.py @@ -0,0 +1,108 @@ +# Generated by Django 5.2.11 on 2026-02-18 13:30 + +import django.db.models.deletion +from django.db import migrations, models +from django.db.migrations.state import StateApps + +import counter.fields + + +def migrate_prices(apps: StateApps, schema_editor): + Product = apps.get_model("counter", "Product") + Price = apps.get_model("counter", "Price") + prices = [ + Price( + amount=p.selling_price, + product=p, + created_at=p.created_at, + updated_at=p.updated_at, + ) + for p in Product.objects.all() + ] + Price.objects.bulk_create(prices) + groups = [ + Price.groups.through(price=price, group=group) + for price in Price.objects.select_related("product").prefetch_related( + "product__buying_groups" + ) + for group in price.product.buying_groups.all() + ] + Price.groups.through.objects.bulk_create(groups) + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0048_alter_user_options"), + ("counter", "0038_countersellers"), + ] + + operations = [ + migrations.CreateModel( + name="Price", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "amount", + counter.fields.CurrencyField( + decimal_places=2, max_digits=12, verbose_name="amount" + ), + ), + ( + "is_always_shown", + models.BooleanField( + default=False, + help_text=( + "If this option is enabled, " + "people will see this price and be able to pay it, " + "even if another cheaper price exists. " + "Else it will visible only if it is the cheapest available price." + ), + verbose_name="always show", + ), + ), + ( + "label", + models.CharField( + default="", + help_text="A short label for easier differentiation if a user can see multiple prices.", + max_length=32, + verbose_name="label", + blank=True, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + ( + "groups", + models.ManyToManyField( + related_name="prices", to="core.group", verbose_name="groups" + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="prices", + to="counter.product", + verbose_name="product", + ), + ), + ], + options={"verbose_name": "price"}, + ), + migrations.RunPython(migrate_prices, reverse_code=migrations.RunPython.noop), + ] diff --git a/counter/models.py b/counter/models.py index 29ecad2f..9fa6a434 100644 --- a/counter/models.py +++ b/counter/models.py @@ -456,6 +456,78 @@ class Product(models.Model): return self.selling_price - self.purchase_price +class PriceQuerySet(models.QuerySet): + def for_user(self, user: User) -> Self: + age = user.age + if user.is_banned_alcohol: + age = min(age, 17) + return self.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__archived=False, + product__limit_age__lte=age, + ) + + +class Price(models.Model): + amount = CurrencyField(_("amount")) + product = models.ForeignKey( + Product, + verbose_name=_("product"), + related_name="prices", + on_delete=models.CASCADE, + ) + groups = models.ManyToManyField( + Group, verbose_name=_("groups"), related_name="prices" + ) + is_always_shown = models.BooleanField( + _("always show"), + help_text=_( + "If this option is enabled, " + "people will see this price and be able to pay it, " + "even if another cheaper price exists. " + "Else it will visible only if it is the cheapest available price." + ), + default=False, + ) + label = models.CharField( + _("label"), + help_text=_( + "A short label for easier differentiation " + "if a user can see multiple prices." + ), + max_length=32, + default="", + blank=True, + ) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + + objects = PriceQuerySet.as_manager() + + class Meta: + verbose_name = _("price") + + def __str__(self): + if not self.label: + return f"{self.product.name} ({self.amount}€)" + return f"{self.product.name} {self.label} ({self.amount}€)" + + @property + def full_label(self): + if not self.label: + return self.product.name + return f"{self.product.name} \u2013 {self.label}" + + class ProductFormula(models.Model): products = models.ManyToManyField( Product, @@ -1025,7 +1097,9 @@ class Selling(models.Model): event = self.product.eticket.event_title or _("Unknown event") subject = _("Eticket bought for the event %(event)s") % {"event": event} message_html = _( - "You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s." + "You bought an eticket for the event %(event)s.\n" + "You can download it directly from this link %(eticket)s.\n" + "You can also retrieve all your e-tickets on your account page %(url)s." ) % { "event": event, "url": ( diff --git a/counter/schemas.py b/counter/schemas.py index bec0606f..730a8315 100644 --- a/counter/schemas.py +++ b/counter/schemas.py @@ -6,8 +6,8 @@ from ninja import FilterLookup, FilterSchema, ModelSchema, Schema from pydantic import model_validator from club.schemas import SimpleClubSchema -from core.schemas import GroupSchema, NonEmptyStr, SimpleUserSchema -from counter.models import Counter, Product, ProductType +from core.schemas import NonEmptyStr, SimpleUserSchema +from counter.models import Counter, Price, Product, ProductType class CounterSchema(ModelSchema): @@ -66,6 +66,12 @@ class SimpleProductSchema(ModelSchema): fields = ["id", "name", "code"] +class ProductPriceSchema(ModelSchema): + class Meta: + model = Price + fields = ["amount", "groups"] + + class ProductSchema(ModelSchema): class Meta: model = Product @@ -75,13 +81,12 @@ class ProductSchema(ModelSchema): "code", "description", "purchase_price", - "selling_price", "icon", "limit_age", "archived", ] - buying_groups: list[GroupSchema] + prices: list[ProductPriceSchema] club: SimpleClubSchema product_type: SimpleProductTypeSchema | None url: str From 6634bd987d037dcc89b616b7eade73e436f4b3ee Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 2 Mar 2026 15:44:37 +0100 Subject: [PATCH 02/13] refactor InvoiceItem and BasketItem models --- eboutic/admin.py | 14 +- ..._product_name_basketitem_label_and_more.py | 53 ++++++ eboutic/models.py | 167 ++++++++---------- 3 files changed, 135 insertions(+), 99 deletions(-) create mode 100644 eboutic/migrations/0003_rename_product_name_basketitem_label_and_more.py diff --git a/eboutic/admin.py b/eboutic/admin.py index b9be2774..d06e2ca1 100644 --- a/eboutic/admin.py +++ b/eboutic/admin.py @@ -22,23 +22,22 @@ from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem class BasketAdmin(admin.ModelAdmin): list_display = ("user", "date", "total") autocomplete_fields = ("user",) + date_hierarchy = "date" def get_queryset(self, request): return ( super() .get_queryset(request) .annotate( - total=Sum( - F("items__quantity") * F("items__product_unit_price"), default=0 - ) + total=Sum(F("items__quantity") * F("items__unit_price"), default=0) ) ) @admin.register(BasketItem) class BasketItemAdmin(admin.ModelAdmin): - list_display = ("basket", "product_name", "product_unit_price", "quantity") - search_fields = ("product_name",) + list_display = ("label", "unit_price", "quantity") + search_fields = ("label",) @admin.register(Invoice) @@ -50,5 +49,6 @@ class InvoiceAdmin(admin.ModelAdmin): @admin.register(InvoiceItem) class InvoiceItemAdmin(admin.ModelAdmin): - list_display = ("invoice", "product_name", "product_unit_price", "quantity") - search_fields = ("product_name",) + list_display = ("label", "unit_price", "quantity") + search_fields = ("label",) + list_select_related = ("price",) diff --git a/eboutic/migrations/0003_rename_product_name_basketitem_label_and_more.py b/eboutic/migrations/0003_rename_product_name_basketitem_label_and_more.py new file mode 100644 index 00000000..09cc5d3a --- /dev/null +++ b/eboutic/migrations/0003_rename_product_name_basketitem_label_and_more.py @@ -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", "0039_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", + ), + ), + ] diff --git a/eboutic/models.py b/eboutic/models.py index e35b1ddf..a5c42bb8 100644 --- a/eboutic/models.py +++ b/eboutic/models.py @@ -17,12 +17,12 @@ from __future__ import annotations import hmac from datetime import datetime from enum import Enum -from typing import Any, Self +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, Subquery, Sum +from django.db.models import F, OuterRef, Q, Subquery, Sum from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ @@ -30,8 +30,8 @@ from core.models import User from counter.fields import CurrencyField from counter.models import ( BillingInfo, - Counter, Customer, + Price, Product, Refilling, Selling, @@ -39,20 +39,28 @@ from counter.models import ( ) -def get_eboutic_products(user: User) -> list[Product]: - products = ( - get_eboutic() - .products.filter(product_type__isnull=False) - .filter(archived=False, limit_age__lte=user.age) - .annotate( - order=F("product_type__order"), - category=F("product_type__name"), - category_comment=F("product_type__comment"), - price=F("selling_price"), # <-- selected price for basket validation +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(), ) - .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): @@ -94,21 +102,21 @@ class Basket(models.Model): def __str__(self): 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 @cached_property def contains_refilling_item(self) -> bool: return self.items.filter( - type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING + product__product_type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING ).exists() @cached_property def total(self) -> float: return float( - self.items.aggregate( - total=Sum(F("quantity") * F("product_unit_price"), default=0) - )["total"] + self.items.aggregate(total=Sum(F("quantity") * F("unit_price"), default=0))[ + "total" + ] ) def generate_sales( @@ -120,7 +128,8 @@ class Basket(models.Model): Example: ```python 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 with transaction.atomic(): @@ -131,31 +140,23 @@ class Basket(models.Model): # thus only the sales remain ``` """ - # I must proceed with two distinct requests instead of - # only one with a join because the AbstractBaseItem model has been - # poorly designed. If you refactor the model, please refactor this too. - items = self.items.order_by("product_id") - ids = [item.product_id for item in items] - products = Product.objects.filter(id__in=ids).order_by("id") - # items and products are sorted in the same order - sales = [] - for item, product in zip(items, products, strict=False): - sales.append( - Selling( - label=product.name, - 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, - ) + customer = Customer.get_or_create(self.user)[0] + return [ + Selling( + label=item.label, + counter=counter, + club_id=item.product.club_id, + product=item.product, + seller=seller, + customer=customer, + unit_price=item.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 if not hasattr(user, "customer"): raise Customer.DoesNotExist @@ -201,7 +202,7 @@ class InvoiceQueryset(models.QuerySet): def annotate_total(self) -> Self: """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. """ # aggregates within subqueries require a little bit of black magic, @@ -211,7 +212,7 @@ class InvoiceQueryset(models.QuerySet): total=Subquery( InvoiceItem.objects.filter(invoice_id=OuterRef("pk")) .values("invoice_id") - .annotate(total=Sum(F("product_unit_price") * F("quantity"))) + .annotate(total=Sum(F("unit_price") * F("quantity"))) .values("total") ) ) @@ -221,11 +222,7 @@ class Invoice(models.Model): """Invoices are generated once the payment has been validated.""" user = models.ForeignKey( - User, - related_name="invoices", - verbose_name=_("user"), - blank=False, - on_delete=models.CASCADE, + User, related_name="invoices", verbose_name=_("user"), on_delete=models.CASCADE ) date = models.DateTimeField(_("date"), auto_now=True) validated = models.BooleanField(_("validated"), default=False) @@ -246,53 +243,44 @@ class Invoice(models.Model): if self.validated: raise DataError(_("Invoice already validated")) customer, _created = Customer.get_or_create(user=self.user) - eboutic = Counter.objects.filter(type="EBOUTIC").first() - for i in self.items.all(): - if i.type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING: - new = Refilling( - counter=eboutic, - customer=customer, - operator=self.user, - amount=i.product_unit_price * i.quantity, - payment_method=Refilling.PaymentMethod.CARD, - date=self.date, + kwargs = { + "counter": get_eboutic(), + "customer": customer, + "date": self.date, + "payment_method": Selling.PaymentMethod.CARD, + } + for i in self.items.select_related("product"): + if i.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING: + Refilling.objects.create( + **kwargs, operator=self.user, amount=i.unit_price * i.quantity ) - new.save() else: - product = Product.objects.filter(id=i.product_id).first() - new = Selling( - label=i.product_name, - counter=eboutic, - club=product.club, - product=product, + Selling.objects.create( + **kwargs, + label=i.label, + club_id=i.product.club_id, + product=i.product, seller=self.user, - customer=customer, - unit_price=i.product_unit_price, + unit_price=i.unit_price, quantity=i.quantity, - payment_method=Selling.PaymentMethod.CARD, - date=self.date, ) - new.save() self.validated = True self.save() class AbstractBaseItem(models.Model): - product_id = models.IntegerField(_("product id")) - product_name = models.CharField(_("product name"), max_length=255) - type_id = models.IntegerField(_("product type id")) - product_unit_price = CurrencyField(_("unit price")) + product = models.ForeignKey( + Product, verbose_name=_("product"), on_delete=models.PROTECT + ) + label = models.CharField(_("product name"), max_length=255) + unit_price = CurrencyField(_("unit price")) quantity = models.PositiveIntegerField(_("quantity")) class Meta: abstract = True def __str__(self): - return "Item: %s (%s) x%d" % ( - self.product_name, - self.product_unit_price, - self.quantity, - ) + return "Item: %s (%s) x%d" % (self.product.name, self.unit_price, self.quantity) class BasketItem(AbstractBaseItem): @@ -301,21 +289,16 @@ class BasketItem(AbstractBaseItem): ) @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 - product passed in parameters, with the specified quantity. - - Warning: - the basket field is not filled, so you must set - it yourself before saving the model. + product price passed in parameters, with the specified quantity. """ return cls( basket=basket, - product_id=product.id, - product_name=product.name, - type_id=product.product_type_id, + label=price.full_label, + product_id=price.product_id, quantity=quantity, - product_unit_price=product.selling_price, + unit_price=price.amount, ) From 2613ede59d960502b4c67aac5fefa35d755148ae Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 2 Mar 2026 15:47:42 +0100 Subject: [PATCH 03/13] use new price system in the eboutic --- .../static/bundled/eboutic/eboutic-index.ts | 31 +++---- .../templates/eboutic/eboutic_checkout.jinja | 4 +- eboutic/templates/eboutic/eboutic_main.jinja | 87 +++++++++---------- eboutic/views.py | 70 ++++++++------- 4 files changed, 97 insertions(+), 95 deletions(-) diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 0cd689fd..e345d936 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -1,13 +1,15 @@ export {}; interface BasketItem { - id: number; + priceId: number; name: string; quantity: number; - // biome-ignore lint/style/useNamingConvention: the python code is snake_case - unit_price: number; + unitPrice: number; } +// increment the key number if the data schema of the cached basket changes +const BASKET_CACHE_KEY = "basket1"; + document.addEventListener("alpine:init", () => { Alpine.data("basket", (lastPurchaseTime?: number) => ({ basket: [] as BasketItem[], @@ -30,24 +32,24 @@ document.addEventListener("alpine:init", () => { // It's quite tricky to manually apply attributes to the management part // of a formset so we dynamically apply it here this.$refs.basketManagementForm - .querySelector("#id_form-TOTAL_FORMS") + .getElementById("#id_form-TOTAL_FORMS") .setAttribute(":value", "basket.length"); }, loadBasket(): BasketItem[] { - if (localStorage.basket === undefined) { + if (localStorage.getItem(BASKET_CACHE_KEY) === null) { return []; } try { - return JSON.parse(localStorage.basket); + return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY)); } catch (_err) { return []; } }, saveBasket() { - localStorage.basket = JSON.stringify(this.basket); - localStorage.basketTimestamp = Date.now(); + localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket)); + localStorage.setItem("basketTimestamp", Date.now().toString()); }, /** @@ -56,7 +58,7 @@ document.addEventListener("alpine:init", () => { */ getTotal() { return this.basket.reduce( - (acc: number, item: BasketItem) => acc + item.quantity * item.unit_price, + (acc: number, item: BasketItem) => acc + item.quantity * item.unitPrice, 0, ); }, @@ -74,7 +76,7 @@ document.addEventListener("alpine:init", () => { * @param itemId the id of the item to remove */ remove(itemId: number) { - const index = this.basket.findIndex((e: BasketItem) => e.id === itemId); + const index = this.basket.findIndex((e: BasketItem) => e.priceId === itemId); if (index < 0) { return; @@ -83,7 +85,7 @@ document.addEventListener("alpine:init", () => { if (this.basket[index].quantity === 0) { this.basket = this.basket.filter( - (e: BasketItem) => e.id !== this.basket[index].id, + (e: BasketItem) => e.priceId !== this.basket[index].id, ); } }, @@ -104,11 +106,10 @@ document.addEventListener("alpine:init", () => { */ createItem(id: number, name: string, price: number): BasketItem { const newItem = { - id, + priceId: id, name, quantity: 0, - // biome-ignore lint/style/useNamingConvention: the python code is snake_case - unit_price: price, + unitPrice: price, } as BasketItem; this.basket.push(newItem); @@ -125,7 +126,7 @@ document.addEventListener("alpine:init", () => { * @param price The unit price of the product */ addFromCatalog(id: number, name: string, price: number) { - let item = this.basket.find((e: BasketItem) => e.id === id); + let item = this.basket.find((e: BasketItem) => e.priceId === id); // if the item is not in the basket, we create it // else we add + 1 to it diff --git a/eboutic/templates/eboutic/eboutic_checkout.jinja b/eboutic/templates/eboutic/eboutic_checkout.jinja index 5bf89f98..369c5d44 100644 --- a/eboutic/templates/eboutic/eboutic_checkout.jinja +++ b/eboutic/templates/eboutic/eboutic_checkout.jinja @@ -32,9 +32,9 @@ {% for item in basket.items.all() %} - {{ item.product_name }} + {{ item.label }} {{ item.quantity }} - {{ item.product_unit_price }} € + {{ item.unit_price }} € {% endfor %} diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja index 9ac4042c..bad712a2 100644 --- a/eboutic/templates/eboutic/eboutic_main.jinja +++ b/eboutic/templates/eboutic/eboutic_main.jinja @@ -41,7 +41,7 @@ {% endif %}
    - {# Starting money #} + {# Starting money #}
  • {% trans %}Current account amount: {% endtrans %} @@ -51,15 +51,15 @@
  • -