mirror of
https://github.com/Crocmagnon/checkout.git
synced 2024-11-22 08:08:04 +01:00
Implement floating price items
This commit is contained in:
parent
072c762ad0
commit
56c2c7045d
9 changed files with 218 additions and 26 deletions
|
@ -15,7 +15,6 @@ def live_server(settings, live_server):
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def firefox_options(firefox_options):
|
def firefox_options(firefox_options):
|
||||||
firefox_options.add_argument("-headless")
|
|
||||||
return firefox_options
|
return firefox_options
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ from django.utils.translation import gettext as _
|
||||||
from purchase.layout import BasketItemField
|
from purchase.layout import BasketItemField
|
||||||
from purchase.models import Basket, Product
|
from purchase.models import Basket, Product
|
||||||
|
|
||||||
PREFIX = "product-"
|
PRICED_PREFIX = "product-"
|
||||||
|
UNPRICED_PREFIX = "unpriced_product-"
|
||||||
|
|
||||||
|
|
||||||
class BasketForm(forms.ModelForm):
|
class BasketForm(forms.ModelForm):
|
||||||
|
@ -31,8 +32,8 @@ class BasketForm(forms.ModelForm):
|
||||||
for item in basket.items.all():
|
for item in basket.items.all():
|
||||||
products[item.product] = item.quantity
|
products[item.product] = item.quantity
|
||||||
fields = []
|
fields = []
|
||||||
for product in Product.objects.all():
|
for product in Product.objects.with_fixed_price():
|
||||||
field_name = f"{PREFIX}{product.id}"
|
field_name = f"{PRICED_PREFIX}{product.id}"
|
||||||
self.fields.update(
|
self.fields.update(
|
||||||
{
|
{
|
||||||
field_name: forms.IntegerField(
|
field_name: forms.IntegerField(
|
||||||
|
@ -47,6 +48,7 @@ class BasketForm(forms.ModelForm):
|
||||||
Div(
|
Div(
|
||||||
*fields,
|
*fields,
|
||||||
css_class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-4",
|
css_class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-4",
|
||||||
|
css_id="products",
|
||||||
),
|
),
|
||||||
InlineRadios("payment_method"),
|
InlineRadios("payment_method"),
|
||||||
)
|
)
|
||||||
|
@ -56,8 +58,8 @@ class BasketForm(forms.ModelForm):
|
||||||
name: str
|
name: str
|
||||||
products = {product.id: product for product in Product.objects.all()}
|
products = {product.id: product for product in Product.objects.all()}
|
||||||
for name, value in self.cleaned_data.items():
|
for name, value in self.cleaned_data.items():
|
||||||
if name.startswith(PREFIX):
|
if name.startswith(PRICED_PREFIX):
|
||||||
product_id = int(name.removeprefix(PREFIX))
|
product_id = int(name.removeprefix(PRICED_PREFIX))
|
||||||
product = products[product_id]
|
product = products[product_id]
|
||||||
if value > 0:
|
if value > 0:
|
||||||
instance.items.update_or_create(
|
instance.items.update_or_create(
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-27 13:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("purchase", "0012_rename_value_cache_etag_cache_last_modified"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="basketitem",
|
||||||
|
name="unique_product_per_basket",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="product",
|
||||||
|
name="unit_price_cents",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
help_text="Unit price in cents. Use zero to denote that the product has no fixed price.",
|
||||||
|
verbose_name="unit price (cents)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,7 +4,7 @@ import hashlib
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Avg, Count, F, Sum, UniqueConstraint
|
from django.db.models import Avg, Count, F, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
|
@ -77,6 +77,12 @@ class ProductQuerySet(models.QuerySet):
|
||||||
def with_sold(self):
|
def with_sold(self):
|
||||||
return self.annotate(sold=Coalesce(Sum("basket_items__quantity"), 0))
|
return self.annotate(sold=Coalesce(Sum("basket_items__quantity"), 0))
|
||||||
|
|
||||||
|
def with_fixed_price(self):
|
||||||
|
return self.exclude(unit_price_cents=0)
|
||||||
|
|
||||||
|
def with_no_fixed_price(self):
|
||||||
|
return self.filter(unit_price_cents=0)
|
||||||
|
|
||||||
|
|
||||||
class ProductManager(models.Manager):
|
class ProductManager(models.Manager):
|
||||||
def get_by_natural_key(self, name):
|
def get_by_natural_key(self, name):
|
||||||
|
@ -88,7 +94,9 @@ class Product(Model):
|
||||||
image = models.ImageField(null=True, blank=True, verbose_name=_("image"))
|
image = models.ImageField(null=True, blank=True, verbose_name=_("image"))
|
||||||
unit_price_cents = models.PositiveIntegerField(
|
unit_price_cents = models.PositiveIntegerField(
|
||||||
verbose_name=_("unit price (cents)"),
|
verbose_name=_("unit price (cents)"),
|
||||||
help_text=_("unit price in cents"),
|
help_text=_(
|
||||||
|
"Unit price in cents. Use zero to denote that the product has no fixed price.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
display_order = models.PositiveIntegerField(
|
display_order = models.PositiveIntegerField(
|
||||||
default=default_product_display_order,
|
default=default_product_display_order,
|
||||||
|
@ -115,6 +123,10 @@ class Product(Model):
|
||||||
base=16,
|
base=16,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_fixed_price(self) -> bool:
|
||||||
|
return self.unit_price_cents > 0
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
if not self.image:
|
if not self.image:
|
||||||
|
@ -223,9 +235,6 @@ class BasketItem(Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("basket item")
|
verbose_name = _("basket item")
|
||||||
verbose_name_plural = _("basket items")
|
verbose_name_plural = _("basket items")
|
||||||
constraints = [
|
|
||||||
UniqueConstraint("product", "basket", name="unique_product_per_basket"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Cache(SingletonModel):
|
class Cache(SingletonModel):
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
{% extends "common/base.html" %}
|
{% extends "common/base.html" %}
|
||||||
{% load static %}
|
{% load i18n static crispy_forms_tags purchase django_htmx %}
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_tags purchase %}
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
<link rel="stylesheet" href="{% static "purchase/css/basket_form.css" %}">
|
<link rel="stylesheet" href="{% static "purchase/css/basket_form.css" %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -18,7 +17,43 @@
|
||||||
<h1>{% translate "New basket" %}</h1>
|
<h1>{% translate "New basket" %}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
|
<form
|
||||||
|
hx-get="{% url "purchase:additional_unpriced_product" %}"
|
||||||
|
hx-target="#products"
|
||||||
|
hx-swap="beforeend"
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
<select class="form-select" name="product_to_add" id="product_to_add">
|
||||||
|
{% for product in products %}
|
||||||
|
<option value="{{ product.pk }}">{{ product.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="submit"
|
||||||
|
id="add_product"
|
||||||
|
>
|
||||||
|
{% translate "Add product" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% for item in basket.items.all %}
|
||||||
|
{% if item.product.unit_price_cents == 0 %}
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
hx-get="{% url "purchase:additional_unpriced_product" %}?product_to_add={{ item.product.pk }}&value={{ item.unit_price_cents }}"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="#products"
|
||||||
|
hx-swap="beforeend"
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% if basket %}
|
{% if basket %}
|
||||||
<a href="{% url "purchase:new" %}" class="btn btn-secondary">{% translate "New" %}</a>
|
<a href="{% url "purchase:new" %}" class="btn btn-secondary">{% translate "New" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrascript %}
|
||||||
|
<script src="{% static 'vendor/htmx-1.8.6/htmx.min.js' %}" defer></script>
|
||||||
|
{% django_htmx_script %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% load crispy_forms_field %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100 bg-success text-white" data-product-id="{{ product.pk }}">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img">
|
||||||
|
{% else %}
|
||||||
|
<div class="card-img product-img-placeholder"
|
||||||
|
style="background-color: hsl({{ product.color_hue }}, 60%, 80%)">
|
||||||
|
<span>
|
||||||
|
{{ product.name|slice:"1" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">{{ product.name }}</h4>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" step="0.01" name="unpriced_product-{{ product.pk }}" min="0" class="numberinput form-control" required="" id="unpriced_id_product-{{ product.pk }}"
|
||||||
|
value="{{ value|default:0 }}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,6 +6,7 @@ from pytest_django.live_server_helper import LiveServer
|
||||||
from selenium.webdriver import ActionChains, Keys
|
from selenium.webdriver import ActionChains, Keys
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.remote.webdriver import WebDriver
|
from selenium.webdriver.remote.webdriver import WebDriver
|
||||||
|
from selenium.webdriver.support.select import Select
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
|
|
||||||
from common.models import User
|
from common.models import User
|
||||||
|
@ -34,6 +35,11 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
ProductFactory(),
|
ProductFactory(),
|
||||||
ProductFactory(),
|
ProductFactory(),
|
||||||
]
|
]
|
||||||
|
unpriced_products = [
|
||||||
|
ProductFactory(unit_price_cents=0),
|
||||||
|
ProductFactory(unit_price_cents=0),
|
||||||
|
ProductFactory(unit_price_cents=0),
|
||||||
|
]
|
||||||
payment_methods = [
|
payment_methods = [
|
||||||
PaymentMethodFactory(),
|
PaymentMethodFactory(),
|
||||||
PaymentMethodFactory(),
|
PaymentMethodFactory(),
|
||||||
|
@ -85,6 +91,29 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
chain.double_click(quantity_input).perform()
|
chain.double_click(quantity_input).perform()
|
||||||
quantity_input.send_keys("4")
|
quantity_input.send_keys("4")
|
||||||
|
|
||||||
|
# Add non-fixed priced product
|
||||||
|
select = Select(selenium.find_element(By.ID, "product_to_add"))
|
||||||
|
unpriced_product = unpriced_products[1]
|
||||||
|
select.select_by_value(str(unpriced_product.pk))
|
||||||
|
selenium.find_element(By.ID, "add_product").click()
|
||||||
|
selenium.find_element(By.ID, "add_product").click()
|
||||||
|
elements = selenium.find_elements(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
f"[data-product-id='{unpriced_product.pk}']",
|
||||||
|
)
|
||||||
|
for elem in elements:
|
||||||
|
assert (
|
||||||
|
elem.find_element(By.CLASS_NAME, "card-title").text == unpriced_product.name
|
||||||
|
)
|
||||||
|
price_input = elements[0].find_element(By.CLASS_NAME, "numberinput")
|
||||||
|
chain = ActionChains(selenium)
|
||||||
|
chain.double_click(price_input).perform()
|
||||||
|
price_input.send_keys("237")
|
||||||
|
price_input = elements[1].find_element(By.CLASS_NAME, "numberinput")
|
||||||
|
chain = ActionChains(selenium)
|
||||||
|
chain.double_click(price_input).perform()
|
||||||
|
price_input.send_keys("401")
|
||||||
|
|
||||||
# Don't add payment method
|
# Don't add payment method
|
||||||
# Save
|
# Save
|
||||||
selenium.find_element(By.ID, "submit-id-submit").click()
|
selenium.find_element(By.ID, "submit-id-submit").click()
|
||||||
|
@ -93,7 +122,7 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
assert Basket.objects.count() == 1
|
assert Basket.objects.count() == 1
|
||||||
basket = Basket.objects.priced().first()
|
basket = Basket.objects.priced().first()
|
||||||
assert basket.payment_method is None
|
assert basket.payment_method is None
|
||||||
assert basket.items.count() == 2
|
assert basket.items.count() == 4
|
||||||
assert basket.items.get(product=products[0]).quantity == 2
|
assert basket.items.get(product=products[0]).quantity == 2
|
||||||
assert (
|
assert (
|
||||||
basket.items.get(product=products[0]).unit_price_cents
|
basket.items.get(product=products[0]).unit_price_cents
|
||||||
|
@ -104,6 +133,14 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
basket.items.get(product=products[1]).unit_price_cents
|
basket.items.get(product=products[1]).unit_price_cents
|
||||||
== products[1].unit_price_cents
|
== products[1].unit_price_cents
|
||||||
)
|
)
|
||||||
|
unpriced_basket_items = basket.items.filter(product=unpriced_product).order_by(
|
||||||
|
"unit_price_cents",
|
||||||
|
)
|
||||||
|
assert len(unpriced_basket_items) == 2
|
||||||
|
assert unpriced_basket_items[0].quantity == 1
|
||||||
|
assert unpriced_basket_items[0].unit_price_cents == 237
|
||||||
|
assert unpriced_basket_items[1].quantity == 1
|
||||||
|
assert unpriced_basket_items[1].unit_price_cents == 401
|
||||||
|
|
||||||
# Assert redirected to basket update view
|
# Assert redirected to basket update view
|
||||||
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
|
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
|
||||||
|
@ -141,6 +178,12 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
quantity = int(quantity_input.get_attribute("value"))
|
quantity = int(quantity_input.get_attribute("value"))
|
||||||
assert quantity == 0
|
assert quantity == 0
|
||||||
|
|
||||||
|
elements = selenium.find_elements(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
f"[data-product-id='{unpriced_product.pk}']",
|
||||||
|
)
|
||||||
|
assert len(elements) == 2, "Unpriced products should be displayed"
|
||||||
|
|
||||||
# Click on - on product 2
|
# Click on - on product 2
|
||||||
displayed_product = displayed_products[1]
|
displayed_product = displayed_products[1]
|
||||||
displayed_product.find_element(By.CLASS_NAME, "btn-danger").click()
|
displayed_product.find_element(By.CLASS_NAME, "btn-danger").click()
|
||||||
|
@ -162,7 +205,7 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
assert Basket.objects.count() == 1
|
assert Basket.objects.count() == 1
|
||||||
basket = Basket.objects.priced().first()
|
basket = Basket.objects.priced().first()
|
||||||
assert basket.payment_method == payment_methods[1]
|
assert basket.payment_method == payment_methods[1]
|
||||||
assert basket.items.count() == 2
|
assert basket.items.count() == 4
|
||||||
assert basket.items.get(product=products[0]).quantity == 2
|
assert basket.items.get(product=products[0]).quantity == 2
|
||||||
assert (
|
assert (
|
||||||
basket.items.get(product=products[0]).unit_price_cents
|
basket.items.get(product=products[0]).unit_price_cents
|
||||||
|
@ -173,6 +216,14 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
||||||
basket.items.get(product=products[1]).unit_price_cents
|
basket.items.get(product=products[1]).unit_price_cents
|
||||||
== products[1].unit_price_cents
|
== products[1].unit_price_cents
|
||||||
)
|
)
|
||||||
|
unpriced_basket_items = basket.items.filter(product=unpriced_product).order_by(
|
||||||
|
"unit_price_cents",
|
||||||
|
)
|
||||||
|
assert len(unpriced_basket_items) == 2
|
||||||
|
assert unpriced_basket_items[0].quantity == 1
|
||||||
|
assert unpriced_basket_items[0].unit_price_cents == 237
|
||||||
|
assert unpriced_basket_items[1].quantity == 1
|
||||||
|
assert unpriced_basket_items[1].unit_price_cents == 401
|
||||||
|
|
||||||
# Assert redirected to same view
|
# Assert redirected to same view
|
||||||
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
|
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from purchase.views import delete_basket, list_baskets, new_basket, update_basket
|
from purchase.views import delete_basket, list_baskets, new_basket, update_basket
|
||||||
|
from purchase.views.basket import additional_unpriced_product
|
||||||
from purchase.views.reports import by_hour_plot_view, products_plots_view, reports
|
from purchase.views.reports import by_hour_plot_view, products_plots_view, reports
|
||||||
|
|
||||||
app_name = "purchase"
|
app_name = "purchase"
|
||||||
|
@ -9,6 +10,11 @@ urlpatterns = [
|
||||||
path("new/", new_basket, name="new"),
|
path("new/", new_basket, name="new"),
|
||||||
path("<int:pk>/update/", update_basket, name="update"),
|
path("<int:pk>/update/", update_basket, name="update"),
|
||||||
path("<int:pk>/delete/", delete_basket, name="delete"),
|
path("<int:pk>/delete/", delete_basket, name="delete"),
|
||||||
|
path(
|
||||||
|
"additional_unpriced_product/",
|
||||||
|
additional_unpriced_product,
|
||||||
|
name="additional_unpriced_product",
|
||||||
|
),
|
||||||
path("reports/", reports, name="reports"),
|
path("reports/", reports, name="reports"),
|
||||||
# plots
|
# plots
|
||||||
path("reports/products_plots/", products_plots_view, name="products_plots"),
|
path("reports/products_plots/", products_plots_view, name="products_plots"),
|
||||||
|
|
|
@ -1,25 +1,33 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.datastructures import MultiValueDict
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import condition, require_http_methods
|
from django.views.decorators.http import condition, require_http_methods
|
||||||
|
from django_htmx.http import trigger_client_event
|
||||||
|
|
||||||
from purchase.forms import BasketForm
|
from purchase.forms import UNPRICED_PREFIX, BasketForm
|
||||||
from purchase.models import Basket, reports_etag, reports_last_modified
|
from purchase.models import Basket, Product, reports_etag, reports_last_modified
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
@permission_required("purchase.add_basket")
|
@permission_required("purchase.add_basket")
|
||||||
def new_basket(request: HttpRequest) -> HttpResponse:
|
def new_basket(request: WSGIRequest) -> HttpResponse:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = BasketForm(request.POST)
|
form = BasketForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
instance = form.save()
|
basket = form.save()
|
||||||
|
update_with_unpriced_products(basket, request.POST)
|
||||||
if request.user.has_perm("purchase.change_basket"):
|
if request.user.has_perm("purchase.change_basket"):
|
||||||
url = instance.get_absolute_url()
|
url = basket.get_absolute_url()
|
||||||
else:
|
else:
|
||||||
url = reverse("purchase:new")
|
url = reverse("purchase:new")
|
||||||
messages.success(request, _("Successfully created basket."))
|
messages.success(request, _("Successfully created basket."))
|
||||||
|
@ -27,26 +35,61 @@ def new_basket(request: HttpRequest) -> HttpResponse:
|
||||||
else:
|
else:
|
||||||
form = BasketForm()
|
form = BasketForm()
|
||||||
|
|
||||||
return TemplateResponse(request, "purchase/basket_form.html", {"form": form})
|
return TemplateResponse(
|
||||||
|
request,
|
||||||
|
"purchase/basket_form.html",
|
||||||
|
{"form": form, "products": Product.objects.with_no_fixed_price()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_with_unpriced_products(basket: Basket, post_data: MultiValueDict):
|
||||||
|
no_fixed_price = {
|
||||||
|
product.id: product for product in Product.objects.with_no_fixed_price()
|
||||||
|
}
|
||||||
|
basket.items.filter(product__in=no_fixed_price.values()).delete()
|
||||||
|
for product_id, product in no_fixed_price.items():
|
||||||
|
if prices := post_data.getlist(f"{UNPRICED_PREFIX}{product_id}"):
|
||||||
|
for price in prices:
|
||||||
|
basket.items.create(product=product, quantity=1, unit_price_cents=price)
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
@permission_required("purchase.change_basket")
|
@permission_required("purchase.change_basket")
|
||||||
def update_basket(request: HttpRequest, pk: int) -> HttpResponse:
|
def update_basket(request: WSGIRequest, pk: int) -> HttpResponse:
|
||||||
basket = get_object_or_404(Basket.objects.priced(), pk=pk)
|
basket = get_object_or_404(Basket.objects.priced(), pk=pk)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = BasketForm(request.POST, instance=basket)
|
form = BasketForm(request.POST, instance=basket)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
basket = form.save()
|
basket = form.save()
|
||||||
|
update_with_unpriced_products(basket, request.POST)
|
||||||
messages.success(request, _("Successfully updated basket."))
|
messages.success(request, _("Successfully updated basket."))
|
||||||
return redirect(basket.get_absolute_url())
|
return redirect(basket.get_absolute_url())
|
||||||
else:
|
else:
|
||||||
form = BasketForm(instance=basket)
|
form = BasketForm(instance=basket)
|
||||||
|
|
||||||
return TemplateResponse(
|
response = render(
|
||||||
request,
|
request,
|
||||||
"purchase/basket_form.html",
|
"purchase/basket_form.html",
|
||||||
{"form": form, "basket": basket},
|
{
|
||||||
|
"form": form,
|
||||||
|
"basket": basket,
|
||||||
|
"products": Product.objects.with_no_fixed_price(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
trigger_client_event(response, "load-unpriced", after="swap")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def additional_unpriced_product(request: WSGIRequest) -> HttpResponse:
|
||||||
|
product_id = request.GET.get("product_to_add")
|
||||||
|
value = request.GET.get("value", 0)
|
||||||
|
product = get_object_or_404(Product.objects.with_no_fixed_price(), pk=product_id)
|
||||||
|
context = {"product": product, "value": value}
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"purchase/snippets/basket_unpriced_item.html",
|
||||||
|
context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue