Modify cart items
This commit is contained in:
@@ -78,8 +78,43 @@ async def add_to_cart(request: Request, session: SessionDep, user: UserDep):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.get("/shop/cart/remove/{item_id}")
|
||||||
|
async def remove_from_cart(
|
||||||
|
request: Request, session: SessionDep, user: UserDep, item_id: int
|
||||||
|
):
|
||||||
|
|
||||||
|
cart = user.shopping_cart
|
||||||
|
for item in cart.items:
|
||||||
|
if item.id == item_id:
|
||||||
|
item.order = None
|
||||||
|
session.delete(item)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Item not found in cart.")
|
||||||
|
|
||||||
|
return RedirectResponse(url=f"/shop/cart", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.post("/shop/cart/update/{item_id}")
|
||||||
|
async def update_cart_item(
|
||||||
|
request: Request, session: SessionDep, user: UserDep, item_id: int
|
||||||
|
):
|
||||||
|
|
||||||
|
form_data = await request.form()
|
||||||
|
|
||||||
|
cart = user.shopping_cart
|
||||||
|
for item in cart.items:
|
||||||
|
if item.id == item_id:
|
||||||
|
item.update_quantity(Decimal(form_data["quantity"]))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Item not found in cart.")
|
||||||
|
|
||||||
|
return RedirectResponse(url=f"/shop/cart", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
|
|
||||||
@shop_router.get("/shop/order/{order_id}")
|
@shop_router.get("/shop/order/{order_id}")
|
||||||
async def add_to_cart(
|
async def get_order_details(
|
||||||
request: Request, session: SessionDep, user: UserDep, order_id: int
|
request: Request, session: SessionDep, user: UserDep, order_id: int
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class Product(Base):
|
|||||||
|
|
||||||
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False)
|
unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False)
|
||||||
|
allow_fractional: Mapped[bool] = mapped_column(nullable=False, default=True)
|
||||||
|
|
||||||
# TODO: limit this to actually used vat rates?
|
# TODO: limit this to actually used vat rates?
|
||||||
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
|
|
||||||
@@ -162,6 +164,13 @@ class OrderItem(Base):
|
|||||||
Numeric(10, 2), nullable=False
|
Numeric(10, 2), nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_quantity(self, new_quantity: decimal.Decimal):
|
||||||
|
if new_quantity <= 0:
|
||||||
|
raise ValueError("Quantity must be positive.")
|
||||||
|
|
||||||
|
self.quantity = new_quantity
|
||||||
|
self.total_amount = self.product.price * new_quantity
|
||||||
|
|
||||||
|
|
||||||
TransactionTypes = typing.Literal[
|
TransactionTypes = typing.Literal[
|
||||||
"order",
|
"order",
|
||||||
|
|||||||
@@ -67,14 +67,15 @@
|
|||||||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||||
<input type="hidden" name="area_id" value="{{ area.id }}">
|
<input type="hidden" name="area_id" value="{{ area.id }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="quantity{{ product.id }}" class="form-label">Menge</label>
|
<label for="quantity{{ product.id }}" class="form-label">Menge{% if product.unit_of_measure != 'piece' %} (in {{product.unit_of_measure}}){% endif %}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="quantity{{ product.id }}"
|
id="quantity{{ product.id }}"
|
||||||
name="quantity"
|
name="quantity"
|
||||||
value="1"
|
value="1"
|
||||||
min="1"
|
{% if product.allow_fractional %}min="0.01"{% else %}min="1"{% endif %}
|
||||||
max="999"
|
max="999"
|
||||||
|
{% if product.allow_fractional %}step="0.01"{% else %}step="1"{% endif %}
|
||||||
oninput="updateTotal{{ product.id }}(this.value)"
|
oninput="updateTotal{{ product.id }}(this.value)"
|
||||||
style="max-width: 150px;">
|
style="max-width: 150px;">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<!-- Latest Transactions Section -->
|
<!-- Latest Transactions Section -->
|
||||||
<div class="transactions-section">
|
<div class="transactions-section">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2 class="h3 mb-0">Latest Transactions</h2>
|
<h2 class="h3 mb-0">Letzte Buchungen</h2>
|
||||||
<a href="#" class="btn btn-outline-primary btn-sm">View All</a>
|
<a href="#" class="btn btn-outline-primary btn-sm">View All</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
{% set total = namespace(value=0) %}
|
{% set total = namespace(value=0) %}
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
{% set price = item.price if item.price is defined else (item.unit_price if item.unit_price is defined else 0) %}
|
{% set price = item.price if item.price is defined else (item.unit_price if item.unit_price is defined else 0) %}
|
||||||
{% set qty = item.quantity if item.quantity is defined else (item.qty if item.qty is defined else 1) %}
|
{% set subtotal = price * item.quantity %}
|
||||||
{% set subtotal = price * qty %}
|
|
||||||
{% set total.value = total.value + subtotal %}
|
{% set total.value = total.value + subtotal %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -35,18 +34,26 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="width:180px;">
|
<td class="text-center" style="width:180px;">
|
||||||
{% if is_cart %}
|
{% if is_cart %}
|
||||||
<form method="post" action="/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
|
<form method="post" action="/shop/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
|
||||||
<input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;">
|
<input
|
||||||
|
type="number"
|
||||||
|
name="quantity"
|
||||||
|
value="{% if item.product.allow_fractional %}{{ item.quantity | format_number }}{% else %}{{ item.quantity | int }}{% endif %}"
|
||||||
|
{% if item.product.allow_fractional %}min="0.01" step="0.01"{% else %}min="1" step="1"{% endif %}
|
||||||
|
class="form-control form-control-sm me-2"
|
||||||
|
style="width:80px;"
|
||||||
|
required>
|
||||||
|
{% if item.product.unit_of_measure != 'piece' %}<span class="text-muted small ms-1 me-2">{{ item.product.unit_of_measure }}</span>{% endif %}
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
|
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ item.quantity | format_number }}
|
{{ item.quantity | format_number }}{% if item.product.unit_of_measure != 'piece' %} {{ item.product.unit_of_measure }}{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ item.product.price | format_number }} €</td>
|
<td class="text-end">{{ item.product.price | format_number }} €</td>
|
||||||
<td class="text-end">{{ item.total_amount | format_number }} €</td>
|
<td class="text-end">{{ item.total_amount | format_number }} €</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
{% if is_cart %}<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
|
{% if is_cart %}<a href="/shop/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -64,7 +71,6 @@
|
|||||||
{% if is_cart %}
|
{% if is_cart %}
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
<div>
|
<div>
|
||||||
<a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a>
|
|
||||||
<a href="/shop/finalize_order" class="btn btn-primary">Jetzt Buchen</a>
|
<a href="/shop/finalize_order" class="btn btn-primary">Jetzt Buchen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fake_data import fake
|
from fake_data import fake
|
||||||
|
from sqlalchemy import select
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
from allmende_payment_system.database import ensure_user
|
from allmende_payment_system.database import ensure_user
|
||||||
@@ -67,6 +68,80 @@ def test_add_item_to_cart(client: TestClient, test_db, product):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_item_in_cart(client: TestClient, test_db, product):
|
||||||
|
form_data = {"product_id": product.id, "quantity": 2, "area_id": product.area.id}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/shop/cart/add",
|
||||||
|
data=form_data,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
form_data = {"quantity": 3}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/shop/cart/update/{test_db.scalar(select(OrderItem)).id}",
|
||||||
|
data=form_data,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 1
|
||||||
|
assert cart.items[0].quantity == 3
|
||||||
|
assert cart.items[0].total_amount == product.price * 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_item_from_cart(client: TestClient, test_db, product):
|
||||||
|
|
||||||
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
|
user.shopping_cart.items.append(
|
||||||
|
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
|
||||||
|
)
|
||||||
|
test_db.flush()
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 1
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/shop/cart/remove/{user.shopping_cart.items[0].id}", follow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 0
|
||||||
|
|
||||||
|
assert len(test_db.scalars(select(OrderItem)).all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_item_from_cart_wrong_user(client: TestClient, test_db, product):
|
||||||
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
|
user.shopping_cart.items.append(
|
||||||
|
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
|
||||||
|
)
|
||||||
|
test_db.flush()
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 1
|
||||||
|
|
||||||
|
id_ = user.shopping_cart.items[0].id
|
||||||
|
|
||||||
|
with mock.patch.dict("os.environ", {"APS_username": "other_user"}):
|
||||||
|
response = client.get(f"/shop/cart/remove/{id_}", follow_redirects=False)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
cart = test_db.scalar(select(Order))
|
||||||
|
assert len(cart.items) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_finalize_order(client: TestClient, test_db, product):
|
def test_finalize_order(client: TestClient, test_db, product):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user