Introduce product details table
This commit is contained in:
112
alembic/versions/3b61a6fbc6b2_introduce_product_details.py
Normal file
112
alembic/versions/3b61a6fbc6b2_introduce_product_details.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""introduce product details
|
||||||
|
|
||||||
|
Revision ID: 3b61a6fbc6b2
|
||||||
|
Revises: 958d7aee2b21
|
||||||
|
Create Date: 2026-04-22 11:56:31.223186
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "3b61a6fbc6b2"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "958d7aee2b21"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"aps_product_detail",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
|
sa.Column("product_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("description", sa.String(), nullable=True),
|
||||||
|
sa.Column("price", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"unit_of_measure",
|
||||||
|
sa.Enum("g", "kg", "l", "piece", native_enum=False),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("allow_fractional", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("temporarily_unavailable", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("vat_rate", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column("valid_from", sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["product_id"],
|
||||||
|
["aps_product.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Copy data from aps_product to aps_product_detail
|
||||||
|
# Bind the current connection to a temporary session
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Fetch all products
|
||||||
|
products = conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT id, name, price, unit_of_measure, allow_fractional, vat_rate FROM aps_product"
|
||||||
|
)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
# Insert data into aps_product_detail
|
||||||
|
for product in products:
|
||||||
|
conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"""
|
||||||
|
INSERT INTO aps_product_detail
|
||||||
|
(name, product_id, description, price, unit_of_measure, allow_fractional, temporarily_unavailable, vat_rate, valid_from)
|
||||||
|
VALUES (:name, :product_id, :description, :price, :unit_of_measure, :allow_fractional, :temporarily_unavailable, :vat_rate, :valid_from)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"name": product.name,
|
||||||
|
"product_id": product.id,
|
||||||
|
"description": "",
|
||||||
|
"price": product.price,
|
||||||
|
"unit_of_measure": product.unit_of_measure,
|
||||||
|
"allow_fractional": product.allow_fractional,
|
||||||
|
"temporarily_unavailable": False,
|
||||||
|
"vat_rate": product.vat_rate,
|
||||||
|
"valid_from": datetime.datetime(1970, 1, 1, 0, 0, 0),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with op.batch_alter_table("aps_product") as batch_op:
|
||||||
|
batch_op.drop_column("vat_rate")
|
||||||
|
batch_op.drop_column("name")
|
||||||
|
|
||||||
|
batch_op.drop_column("allow_fractional")
|
||||||
|
batch_op.drop_column("unit_of_measure")
|
||||||
|
batch_op.drop_column("price")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
"aps_product",
|
||||||
|
sa.Column("price", sa.NUMERIC(precision=10, scale=2), nullable=False),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"aps_product",
|
||||||
|
sa.Column("unit_of_measure", sa.VARCHAR(length=5), nullable=False),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"aps_product", sa.Column("allow_fractional", sa.BOOLEAN(), nullable=False)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"aps_product",
|
||||||
|
sa.Column("vat_rate", sa.NUMERIC(precision=10, scale=2), nullable=False),
|
||||||
|
)
|
||||||
|
op.add_column("aps_product", sa.Column("name", sa.VARCHAR(), nullable=False))
|
||||||
|
op.drop_table("aps_product_detail")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
"""Initial model
|
"""Initial model
|
||||||
|
|
||||||
Revision ID: 958d7aee2b21
|
Revision ID: 958d7aee2b21
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2026-01-23 10:10:36.825333
|
Create Date: 2026-01-23 10:10:36.825333
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@@ -12,7 +13,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '958d7aee2b21'
|
revision: str = "958d7aee2b21"
|
||||||
down_revision: Union[str, Sequence[str], None] = None
|
down_revision: Union[str, Sequence[str], None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -21,96 +22,148 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('aps_account',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_account",
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
sa.UniqueConstraint('name')
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_area',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_area",
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('description', sa.String(), nullable=True),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
sa.Column('image_path', sa.String(), nullable=True),
|
sa.Column("description", sa.String(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("image_path", sa.String(), nullable=True),
|
||||||
sa.UniqueConstraint('name')
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_user',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_user",
|
||||||
sa.Column('username', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('display_name', sa.String(), nullable=False),
|
sa.Column("username", sa.String(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("display_name", sa.String(), nullable=False),
|
||||||
sa.UniqueConstraint('username')
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("username"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_user_group',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_user_group",
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('description', sa.String(), nullable=True),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("description", sa.String(), nullable=True),
|
||||||
sa.UniqueConstraint('name')
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_order',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_order",
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['aps_user.id'], ),
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_id"],
|
||||||
|
["aps_user.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_permission',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_permission",
|
||||||
sa.Column('scope', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('action', sa.String(), nullable=False),
|
sa.Column("scope", sa.String(), nullable=False),
|
||||||
sa.Column('user_group_id', sa.Integer(), nullable=False),
|
sa.Column("action", sa.String(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_group_id'], ['aps_user_group.id'], ),
|
sa.Column("user_group_id", sa.Integer(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_group_id"],
|
||||||
|
["aps_user_group.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_product',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_product",
|
||||||
sa.Column('name', sa.String(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=False),
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
sa.Column('unit_of_measure', sa.Enum('g', 'kg', 'l', 'piece', native_enum=False), nullable=False),
|
sa.Column("price", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
sa.Column('allow_fractional', sa.Boolean(), nullable=False),
|
sa.Column(
|
||||||
sa.Column('vat_rate', sa.Numeric(precision=10, scale=2), nullable=False),
|
"unit_of_measure",
|
||||||
sa.Column('area_id', sa.Integer(), nullable=False),
|
sa.Enum("g", "kg", "l", "piece", native_enum=False),
|
||||||
sa.Column('image_path', sa.String(), nullable=True),
|
nullable=False,
|
||||||
sa.ForeignKeyConstraint(['area_id'], ['aps_area.id'], ),
|
),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("allow_fractional", sa.Boolean(), nullable=False),
|
||||||
sa.UniqueConstraint('name')
|
sa.Column("vat_rate", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column("area_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("image_path", sa.String(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["area_id"],
|
||||||
|
["aps_area.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_user_account_association',
|
op.create_table(
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
"aps_user_account_association",
|
||||||
sa.Column('account_id', sa.Integer(), nullable=False),
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['account_id'], ['aps_account.id'], ),
|
sa.Column("account_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['aps_user.id'], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint('user_id', 'account_id')
|
["account_id"],
|
||||||
|
["aps_account.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_id"],
|
||||||
|
["aps_user.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("user_id", "account_id"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_user_user_group_association',
|
op.create_table(
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
"aps_user_user_group_association",
|
||||||
sa.Column('user_group_id', sa.Integer(), nullable=False),
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_group_id'], ['aps_user_group.id'], ),
|
sa.Column("user_group_id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['aps_user.id'], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint('user_id', 'user_group_id')
|
["user_group_id"],
|
||||||
|
["aps_user_group.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_id"],
|
||||||
|
["aps_user.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("user_id", "user_group_id"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_order_item',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_order_item",
|
||||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('product_id', sa.Integer(), nullable=False),
|
sa.Column("order_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('quantity', sa.Numeric(), nullable=False),
|
sa.Column("product_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('total_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
sa.Column("quantity", sa.Numeric(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['order_id'], ['aps_order.id'], ),
|
sa.Column("total_amount", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['product_id'], ['aps_product.id'], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint('id')
|
["order_id"],
|
||||||
|
["aps_order.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["product_id"],
|
||||||
|
["aps_product.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table('aps_transaction',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"aps_transaction",
|
||||||
sa.Column('type', sa.Enum('order', 'deposit', 'withdrawal', 'expense', native_enum=False), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=True),
|
sa.Column(
|
||||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
"type",
|
||||||
sa.Column('total_amount', sa.Numeric(precision=10, scale=2), nullable=False),
|
sa.Enum("order", "deposit", "withdrawal", "expense", native_enum=False),
|
||||||
sa.Column('order_id', sa.Integer(), nullable=True),
|
nullable=False,
|
||||||
sa.Column('account_id', sa.Integer(), nullable=False),
|
),
|
||||||
sa.ForeignKeyConstraint(['account_id'], ['aps_account.id'], ),
|
sa.Column("quantity", sa.Numeric(precision=10, scale=2), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['order_id'], ['aps_order.id'], ),
|
sa.Column("timestamp", sa.DateTime(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("total_amount", sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column("order_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("account_id", sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["account_id"],
|
||||||
|
["aps_account.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["order_id"],
|
||||||
|
["aps_order.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@@ -118,15 +171,15 @@ def upgrade() -> None:
|
|||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('aps_transaction')
|
op.drop_table("aps_transaction")
|
||||||
op.drop_table('aps_order_item')
|
op.drop_table("aps_order_item")
|
||||||
op.drop_table('aps_user_user_group_association')
|
op.drop_table("aps_user_user_group_association")
|
||||||
op.drop_table('aps_user_account_association')
|
op.drop_table("aps_user_account_association")
|
||||||
op.drop_table('aps_product')
|
op.drop_table("aps_product")
|
||||||
op.drop_table('aps_permission')
|
op.drop_table("aps_permission")
|
||||||
op.drop_table('aps_order')
|
op.drop_table("aps_order")
|
||||||
op.drop_table('aps_user_group')
|
op.drop_table("aps_user_group")
|
||||||
op.drop_table('aps_user')
|
op.drop_table("aps_user")
|
||||||
op.drop_table('aps_area')
|
op.drop_table("aps_area")
|
||||||
op.drop_table('aps_account')
|
op.drop_table("aps_account")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, File, Form, HTTPException, Request
|
from fastapi import APIRouter, File, Form, HTTPException, Request
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
@@ -13,6 +14,7 @@ from allmende_payment_system.models import (
|
|||||||
Area,
|
Area,
|
||||||
Permission,
|
Permission,
|
||||||
Product,
|
Product,
|
||||||
|
ProductDetails,
|
||||||
Transaction,
|
Transaction,
|
||||||
User,
|
User,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
@@ -234,9 +236,14 @@ async def edit_product_post(
|
|||||||
select(Product).where(Product.id == product_id)
|
select(Product).where(Product.id == product_id)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
|
||||||
for field_name, data in product_data.model_dump().items():
|
new_details = ProductDetails(
|
||||||
setattr(product, field_name, data)
|
valid_from=func.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for field_name, data in product_data.model_dump().items():
|
||||||
|
setattr(new_details, field_name, data)
|
||||||
|
|
||||||
|
product.details_history.append(new_details)
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@@ -273,12 +280,15 @@ async def new_product_post(
|
|||||||
if not user.has_permission("product", "edit"):
|
if not user.has_permission("product", "edit"):
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
|
|
||||||
product = Product()
|
product = Product(area_id=product_data.area_id)
|
||||||
|
session.add(product)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
product_details = ProductDetails()
|
||||||
|
product.details_history.append(product_details)
|
||||||
|
|
||||||
for field_name, data in product_data.model_dump().items():
|
for field_name, data in product_data.model_dump().items():
|
||||||
setattr(product, field_name, data)
|
setattr(product_details, field_name, data)
|
||||||
|
|
||||||
session.add(product)
|
|
||||||
|
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
|
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async def add_to_cart(request: Request, session: SessionDep, user: UserDep):
|
|||||||
product = session.scalars(query).one()
|
product = session.scalars(query).one()
|
||||||
|
|
||||||
quantity = Decimal(form_data["quantity"])
|
quantity = Decimal(form_data["quantity"])
|
||||||
total_amount = product.price * quantity
|
total_amount = product.current_details.price * quantity
|
||||||
|
|
||||||
order_item = OrderItem(
|
order_item = OrderItem(
|
||||||
product=product, quantity=quantity, total_amount=total_amount
|
product=product, quantity=quantity, total_amount=total_amount
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import datetime
|
|||||||
import decimal
|
import decimal
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Numeric, Table, select
|
from sqlalchemy import Column, ForeignKey, Numeric, Table, func, select
|
||||||
from sqlalchemy.exc import NoResultFound
|
from sqlalchemy.exc import NoResultFound
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
DeclarativeBase,
|
DeclarativeBase,
|
||||||
@@ -135,23 +135,59 @@ class Area(Base):
|
|||||||
products: Mapped[list["Product"]] = relationship("Product")
|
products: Mapped[list["Product"]] = relationship("Product")
|
||||||
|
|
||||||
|
|
||||||
class Product(Base):
|
class ProductDetails(Base):
|
||||||
__tablename__ = TABLE_PREFIX + "product"
|
"""Describes details of the product that can change over time like price, unit of measure, vat rate, etc."""
|
||||||
|
|
||||||
|
__tablename__ = TABLE_PREFIX + "product_detail"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
||||||
|
|
||||||
|
product_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "product.id"))
|
||||||
|
product: Mapped["Product"] = relationship("Product")
|
||||||
|
|
||||||
|
description: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
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)
|
allow_fractional: Mapped[bool] = mapped_column(nullable=False, default=True)
|
||||||
|
|
||||||
|
temporarily_unavailable: Mapped[bool] = mapped_column(nullable=False, default=False)
|
||||||
|
|
||||||
# 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))
|
||||||
|
|
||||||
|
valid_from: Mapped[datetime.datetime] = mapped_column(
|
||||||
|
nullable=False, default=datetime.datetime(1970, 1, 1, 0, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Product(Base):
|
||||||
|
__tablename__ = TABLE_PREFIX + "product"
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
||||||
area: Mapped["Area"] = relationship("Area", back_populates="products")
|
area: Mapped["Area"] = relationship("Area", back_populates="products")
|
||||||
|
|
||||||
image_path: Mapped[str] = mapped_column(nullable=True)
|
image_path: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
details_history: Mapped[list["ProductDetails"]] = relationship(
|
||||||
|
"ProductDetails", cascade="all, delete-orphan", back_populates="product"
|
||||||
|
)
|
||||||
|
current_details: Mapped["ProductDetails"] = relationship(
|
||||||
|
"ProductDetails",
|
||||||
|
primaryjoin="and_(Product.id == ProductDetails.product_id, ProductDetails.valid_from <= func.now())",
|
||||||
|
order_by="ProductDetails.valid_from.desc()",
|
||||||
|
uselist=False,
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, area_id: int | None = None, **kwargs):
|
||||||
|
details = ProductDetails(**kwargs, valid_from=func.now())
|
||||||
|
product = cls(area_id=area_id)
|
||||||
|
product.details_history.append(details)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
class Order(Base):
|
class Order(Base):
|
||||||
__tablename__ = TABLE_PREFIX + "order"
|
__tablename__ = TABLE_PREFIX + "order"
|
||||||
@@ -217,7 +253,7 @@ class OrderItem(Base):
|
|||||||
raise ValueError("Quantity must be positive.")
|
raise ValueError("Quantity must be positive.")
|
||||||
|
|
||||||
self.quantity = new_quantity
|
self.quantity = new_quantity
|
||||||
self.total_amount = self.product.price * new_quantity
|
self.total_amount = self.product.current_details.price * new_quantity
|
||||||
|
|
||||||
|
|
||||||
TransactionTypes = typing.Literal[
|
TransactionTypes = typing.Literal[
|
||||||
|
|||||||
@@ -12,19 +12,12 @@
|
|||||||
<div class="col-md-6 col-lg-4">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<a href="#" class="text-decoration-none text-dark">
|
<a href="#" class="text-decoration-none text-dark">
|
||||||
{#<!-- Product Image -->
|
|
||||||
<img
|
|
||||||
src="/static/img/{{ product.image_path if product.image_path else 'placeholder.jpg' }}"
|
|
||||||
alt="{{ product.name }}"
|
|
||||||
class="card-img-top img-fluid rounded-top"
|
|
||||||
style="height: 100px; object-fit: cover;"
|
|
||||||
>#}
|
|
||||||
<!-- Product Details -->
|
<!-- Product Details -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title mb-2">{{ product.name }}</h5>
|
<h5 class="card-title mb-2">{{ product.current_details.name }}</h5>
|
||||||
<p class="card-text text-muted small mb-3">{{ product.description }}</p>
|
<p class="card-text text-muted small mb-3">{{ product.current_details.description }}</p>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="fw-bold">{{ product.price|format_number }} € pro {{ product.unit_of_measure|units_of_measure_de }}</span>
|
<span class="fw-bold">{{ product.current_details.price|format_number }} € pro {{ product.current_details.unit_of_measure|units_of_measure_de }}</span>
|
||||||
<button class="btn btn-sm btn-outline-primary"
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#productModal{{ product.id }}">
|
data-bs-target="#productModal{{ product.id }}">
|
||||||
@@ -43,45 +36,38 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<form method="POST" action="/shop/cart/add">
|
<form method="POST" action="/shop/cart/add">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="productModalLabel{{ product.id }}">{{ product.name }}</h5>
|
<h5 class="modal-title" id="productModalLabel{{ product.id }}">{{ product.current_details.name }}</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{#<!-- Product Image -->
|
|
||||||
<img
|
|
||||||
src="/static/img/{{ product.image_path if product.image_path else 'placeholder.jpg' }}"
|
|
||||||
alt="{{ product.name }}"
|
|
||||||
class="img-fluid rounded mb-3"
|
|
||||||
style="max-height: 200px; width: 100%; object-fit: cover;"
|
|
||||||
>#}
|
|
||||||
|
|
||||||
<!-- Product Description -->
|
<!-- Product Description -->
|
||||||
<p class="text-muted mb-3">{{ product.description }}</p>
|
<p class="text-muted mb-3">{{ product.current_details.description }}</p>
|
||||||
|
|
||||||
<!-- Price -->
|
<!-- Price -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6 class="fw-bold">Preis: {{ product.price|format_number }} € pro {{ product.unit_of_measure|units_of_measure_de }}</h6>
|
<h6 class="fw-bold">Preis: {{ product.current_details.price|format_number }} € pro {{ product.current_details.unit_of_measure|units_of_measure_de }}</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quantity Selector -->
|
<!-- Quantity Selector -->
|
||||||
<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{% if product.unit_of_measure != 'piece' %} (in {{product.unit_of_measure}}){% endif %}</label>
|
<label for="quantity{{ product.id }}" class="form-label">Menge{% if product.current_details.unit_of_measure != 'piece' %} (in {{product.current_details.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"
|
||||||
{% if product.allow_fractional %}min="0.01"{% else %}min="1"{% endif %}
|
{% if product.current_details.allow_fractional %}min="0.01"{% else %}min="1"{% endif %}
|
||||||
max="999"
|
max="999"
|
||||||
{% if product.allow_fractional %}step="0.01"{% else %}step="1"{% endif %}
|
{% if product.current_details.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>
|
||||||
<!-- Total Price -->
|
<!-- Total Price -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6>Gesamt: <span id="total{{ product.id }}" class="fw-bold">{{ '%.2f' | format(product.price) }} €</span></h6>
|
<h6>Gesamt: <span id="total{{ product.id }}" class="fw-bold">{{ '%.2f' | format(product.current_details.price) }} €</span></h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +84,7 @@
|
|||||||
{# TODO: Make this a global function #}
|
{# TODO: Make this a global function #}
|
||||||
<script>
|
<script>
|
||||||
function updateTotal{{ product.id }}(quantity) {
|
function updateTotal{{ product.id }}(quantity) {
|
||||||
const price = {{ product.price }};
|
const price = {{ product.current_details.price }};
|
||||||
const total = (price * quantity).toFixed(2);
|
const total = (price * quantity).toFixed(2);
|
||||||
document.getElementById('total{{ product.id }}').textContent = total + ' €';
|
document.getElementById('total{{ product.id }}').textContent = total + ' €';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{% set total.value = total.value + subtotal %}
|
{% set total.value = total.value + subtotal %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="fw-semibold">{{ item.product.name }}</div>
|
<div class="fw-semibold">{{ item.product.current_details.name }}</div>
|
||||||
<div class="text-muted small">{{ item.description or '' }}</div>
|
<div class="text-muted small">{{ item.description or '' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="width:180px;">
|
<td class="text-center" style="width:180px;">
|
||||||
@@ -38,19 +38,19 @@
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="quantity"
|
name="quantity"
|
||||||
value="{% if item.product.allow_fractional %}{{ item.quantity | format_number }}{% else %}{{ item.quantity | int }}{% endif %}"
|
value="{% if item.product.current_details.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 %}
|
{% if item.product.current_details.allow_fractional %}min="0.01" step="0.01"{% else %}min="1" step="1"{% endif %}
|
||||||
class="form-control form-control-sm me-2"
|
class="form-control form-control-sm me-2"
|
||||||
style="width:80px;"
|
style="width:80px;"
|
||||||
required>
|
required>
|
||||||
{% if item.product.unit_of_measure != 'piece' %}<span class="text-muted small ms-1 me-2">{{ item.product.unit_of_measure }}</span>{% endif %}
|
{% if item.product.current_details.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 }}{% if item.product.unit_of_measure != 'piece' %} {{ item.product.unit_of_measure }}{% endif %}
|
{{ item.quantity | format_number }}{% if item.product.current_details.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.current_details.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="/shop/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 %}
|
||||||
|
|||||||
@@ -7,34 +7,34 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name" {% if edit_mode %}value="{{ product.name }}"{% endif %} required>
|
<input type="text" class="form-control" id="name" name="name" {% if edit_mode %}value="{{ product.current_details.name }}"{% endif %} required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="price" class="form-label">Preis</label>
|
<label for="price" class="form-label">Preis</label>
|
||||||
<input type="number" step="0.01" class="form-control" id="price" name="price" {% if edit_mode %}value="{{ product.price }}"{% endif %} required>
|
<input type="number" step="0.01" class="form-control" id="price" name="price" {% if edit_mode %}value="{{ product.current_details.price }}"{% endif %} required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="unit_of_measure" class="form-label">Einheit</label>
|
<label for="unit_of_measure" class="form-label">Einheit</label>
|
||||||
<select class="form-select" id="unit_of_measure" name="unit_of_measure" required>
|
<select class="form-select" id="unit_of_measure" name="unit_of_measure" required>
|
||||||
<option value="g" {% if edit_mode and product.unit_of_measure == 'g' %}selected{% endif %}>g</option>
|
<option value="g" {% if edit_mode and product.current_details.unit_of_measure == 'g' %}selected{% endif %}>g</option>
|
||||||
<option value="kg" {% if edit_mode and product.unit_of_measure == 'kg' %}selected{% endif %}>kg</option>
|
<option value="kg" {% if edit_mode and product.current_details.unit_of_measure == 'kg' %}selected{% endif %}>kg</option>
|
||||||
<option value="l" {% if edit_mode and product.unit_of_measure == 'l' %}selected{% endif %}>l</option>
|
<option value="l" {% if edit_mode and product.current_details.unit_of_measure == 'l' %}selected{% endif %}>l</option>
|
||||||
<option value="piece" {% if edit_mode and product.unit_of_measure == 'piece' %}selected{% endif %}>Stück</option>
|
<option value="piece" {% if edit_mode and product.current_details.unit_of_measure == 'piece' %}selected{% endif %}>Stück</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="allow_fractional" name="allow_fractional" {% if edit_mode and product.allow_fractional %}checked{% endif %}>
|
<input type="checkbox" class="form-check-input" id="allow_fractional" name="allow_fractional" {% if edit_mode and product.current_details.allow_fractional %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="allow_fractional">Bruchteile erlauben</label>
|
<label class="form-check-label" for="allow_fractional">Bruchteile erlauben</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="vat_rate" class="form-label">MwSt-Satz</label>
|
<label for="vat_rate" class="form-label">MwSt-Satz</label>
|
||||||
<select class="form-select" id="vat_rate" name="vat_rate" required>
|
<select class="form-select" id="vat_rate" name="vat_rate" required>
|
||||||
<option value="7" {% if edit_mode and product.vat_rate == '7' %}selected{% endif %}>7 %</option>
|
<option value="7" {% if edit_mode and product.current_details.vat_rate == '7' %}selected{% endif %}>7 %</option>
|
||||||
<option value="19" {% if edit_mode and product.vat_rate == '19' %}selected{% endif %}>19 %</option>
|
<option value="19" {% if edit_mode and product.current_details.vat_rate == '19' %}selected{% endif %}>19 %</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ product.id }}</td>
|
<td>{{ product.id }}</td>
|
||||||
<td>{{ product.name }}</td>
|
<td>{{ product.current_details.name }}</td>
|
||||||
<td>{{ product.area.name }}</td>
|
<td>{{ product.area.name }}</td>
|
||||||
<td>{{ product.price | format_number}} €</td>
|
<td>{{ product.current_details.price | format_number}} €</td>
|
||||||
<td>{{ product.unit_of_measure | units_of_measure_de}}</td>
|
<td>{{ product.current_details.unit_of_measure | units_of_measure_de}}</td>
|
||||||
<td>{{ product.vat_rate | int }} %</td>
|
<td>{{ product.current_details.vat_rate | int }} %</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<a class="btn btn-sm btn-primary" href="/admin/products/edit/{{ product.id }}">Bearbeiten</a>
|
<a class="btn btn-sm btn-primary" href="/admin/products/edit/{{ product.id }}">Bearbeiten</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from allmende_payment_system.models import (
|
|||||||
Area,
|
Area,
|
||||||
Permission,
|
Permission,
|
||||||
Product,
|
Product,
|
||||||
|
ProductDetails,
|
||||||
User,
|
User,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
)
|
)
|
||||||
@@ -219,9 +220,49 @@ def test_add_product(test_db, client, admin_user):
|
|||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert response.status_code == 303
|
assert response.status_code == 303
|
||||||
product = test_db.execute(
|
product = test_db.execute(select(Product)).scalar()
|
||||||
select(Product).where(Product.name == "Test Product")
|
|
||||||
).scalar()
|
|
||||||
assert product is not None
|
assert product is not None
|
||||||
assert product.name == "Test Product"
|
assert len(product.details_history) == 1
|
||||||
assert product.price == Decimal("9.99")
|
assert product.current_details.name == "Test Product"
|
||||||
|
assert product.current_details.price == Decimal("9.99")
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_product(test_db, client, admin_user):
|
||||||
|
area = Area(name="Test Area", description="An area for testing")
|
||||||
|
test_db.add(area)
|
||||||
|
product = Product(
|
||||||
|
area=area,
|
||||||
|
)
|
||||||
|
product.details_history.append(
|
||||||
|
ProductDetails(
|
||||||
|
name="Test Product",
|
||||||
|
price=Decimal("9.99"),
|
||||||
|
unit_of_measure="piece",
|
||||||
|
vat_rate=Decimal("19.00"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
test_db.add(product)
|
||||||
|
test_db.flush()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/admin/products/edit/{product.id}",
|
||||||
|
data={
|
||||||
|
"name": "Updated Product",
|
||||||
|
"vat_rate": "7.00",
|
||||||
|
"unit_of_measure": "kg",
|
||||||
|
"price": 19.99,
|
||||||
|
"area_id": area.id,
|
||||||
|
},
|
||||||
|
user=admin_user,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code == 303
|
||||||
|
|
||||||
|
updated_product = test_db.execute(
|
||||||
|
select(Product).where(Product.id == product.id)
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
assert len(updated_product.details_history) == 2
|
||||||
|
assert updated_product.current_details.name == "Updated Product"
|
||||||
|
assert updated_product.current_details.price == Decimal("19.99")
|
||||||
|
assert updated_product.current_details.price == Decimal("19.99")
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ def create_user_with_account(test_db, username: str, balance: float | None = Non
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def add_finalized_order_to_user(test_db, user, product) -> Order:
|
def add_finalized_order_to_user(test_db, user, product: Product) -> Order:
|
||||||
order = Order(user=user)
|
order = Order(user=user)
|
||||||
total_amount = product.price
|
total_amount = product.current_details.price
|
||||||
order.items.append(
|
order.items.append(
|
||||||
OrderItem(product=product, quantity=1, total_amount=total_amount)
|
OrderItem(product=product, quantity=1, total_amount=total_amount)
|
||||||
)
|
)
|
||||||
@@ -49,7 +49,7 @@ def add_finalized_order_to_user(test_db, user, product) -> Order:
|
|||||||
def product(test_db):
|
def product(test_db):
|
||||||
area = Area(**fake.area())
|
area = Area(**fake.area())
|
||||||
test_db.add(area)
|
test_db.add(area)
|
||||||
product = Product(**fake.product())
|
product = Product.create(**fake.product())
|
||||||
product.area = area
|
product.area = area
|
||||||
test_db.add(product)
|
test_db.add(product)
|
||||||
test_db.flush()
|
test_db.flush()
|
||||||
@@ -96,15 +96,17 @@ def test_edit_item_in_cart(client: APSTestClient, test_db, product):
|
|||||||
cart = test_db.scalar(select(Order))
|
cart = test_db.scalar(select(Order))
|
||||||
assert len(cart.items) == 1
|
assert len(cart.items) == 1
|
||||||
assert cart.items[0].quantity == 3
|
assert cart.items[0].quantity == 3
|
||||||
assert cart.items[0].total_amount == product.price * 3
|
assert cart.items[0].total_amount == product.current_details.price * 3
|
||||||
|
|
||||||
|
|
||||||
def test_remove_item_from_cart(client: APSTestClient, test_db, product):
|
def test_remove_item_from_cart(client: APSTestClient, test_db, product: Product):
|
||||||
|
|
||||||
user = create_user_with_account(test_db, "test")
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
user.shopping_cart.items.append(
|
user.shopping_cart.items.append(
|
||||||
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
|
OrderItem(
|
||||||
|
product=product, quantity=2, total_amount=product.current_details.price * 2
|
||||||
|
)
|
||||||
)
|
)
|
||||||
test_db.flush()
|
test_db.flush()
|
||||||
|
|
||||||
@@ -122,11 +124,15 @@ def test_remove_item_from_cart(client: APSTestClient, test_db, product):
|
|||||||
assert len(test_db.scalars(select(OrderItem)).all()) == 0
|
assert len(test_db.scalars(select(OrderItem)).all()) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_remove_item_from_cart_wrong_user(client: APSTestClient, test_db, product):
|
def test_remove_item_from_cart_wrong_user(
|
||||||
|
client: APSTestClient, test_db, product: Product
|
||||||
|
):
|
||||||
user = create_user_with_account(test_db, "test")
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
user.shopping_cart.items.append(
|
user.shopping_cart.items.append(
|
||||||
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
|
OrderItem(
|
||||||
|
product=product, quantity=2, total_amount=product.current_details.price * 2
|
||||||
|
)
|
||||||
)
|
)
|
||||||
test_db.flush()
|
test_db.flush()
|
||||||
|
|
||||||
@@ -144,7 +150,7 @@ def test_remove_item_from_cart_wrong_user(client: APSTestClient, test_db, produc
|
|||||||
assert len(cart.items) == 1
|
assert len(cart.items) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_finalize_order(client: APSTestClient, test_db, product):
|
def test_finalize_order(client: APSTestClient, test_db, product: Product):
|
||||||
|
|
||||||
user = create_user_with_account(test_db, "test", balance=100.0)
|
user = create_user_with_account(test_db, "test", balance=100.0)
|
||||||
second_account = Account(name=f"Another account")
|
second_account = Account(name=f"Another account")
|
||||||
@@ -155,7 +161,9 @@ def test_finalize_order(client: APSTestClient, test_db, product):
|
|||||||
user.accounts.append(second_account)
|
user.accounts.append(second_account)
|
||||||
|
|
||||||
user.shopping_cart.items.append(
|
user.shopping_cart.items.append(
|
||||||
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
|
OrderItem(
|
||||||
|
product=product, quantity=2, total_amount=product.current_details.price * 2
|
||||||
|
)
|
||||||
)
|
)
|
||||||
test_db.flush()
|
test_db.flush()
|
||||||
|
|
||||||
@@ -170,10 +178,12 @@ def test_finalize_order(client: APSTestClient, test_db, product):
|
|||||||
assert len(user.orders) == 2 # shopping cart + finalized order
|
assert len(user.orders) == 2 # shopping cart + finalized order
|
||||||
|
|
||||||
assert user.accounts[0].balance == Decimal(100.0)
|
assert user.accounts[0].balance == Decimal(100.0)
|
||||||
assert user.accounts[1].balance == Decimal(50.0) - (product.price * 2)
|
assert user.accounts[1].balance == Decimal(50.0) - (
|
||||||
|
product.current_details.price * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_view_order(client: APSTestClient, test_db, product):
|
def test_view_order(client: APSTestClient, test_db, product: Product):
|
||||||
user = create_user_with_account(test_db, "test")
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
order = add_finalized_order_to_user(test_db, user, product)
|
order = add_finalized_order_to_user(test_db, user, product)
|
||||||
@@ -181,10 +191,10 @@ def test_view_order(client: APSTestClient, test_db, product):
|
|||||||
response = client.get(f"/shop/order/{order.id}")
|
response = client.get(f"/shop/order/{order.id}")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert f"Einkauf #{order.id}" in response.text
|
assert f"Einkauf #{order.id}" in response.text
|
||||||
assert product.name in response.text
|
assert product.current_details.name in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_view_order_wrong_user(client: APSTestClient, test_db, product):
|
def test_view_order_wrong_user(client: APSTestClient, test_db, product: Product):
|
||||||
user = create_user_with_account(test_db, "test")
|
user = create_user_with_account(test_db, "test")
|
||||||
|
|
||||||
order = add_finalized_order_to_user(test_db, user, product)
|
order = add_finalized_order_to_user(test_db, user, product)
|
||||||
|
|||||||
Reference in New Issue
Block a user