Add product admin
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, File, Form, HTTPException, Request
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
|
from allmende_payment_system.api import types
|
||||||
from allmende_payment_system.api.dependencies import SessionDep, UserDep
|
from allmende_payment_system.api.dependencies import SessionDep, UserDep
|
||||||
from allmende_payment_system.models import Permission, User, UserGroup
|
from allmende_payment_system.models import Area, Permission, Product, User, UserGroup
|
||||||
from allmende_payment_system.tools import get_jinja_renderer
|
from allmende_payment_system.tools import get_jinja_renderer
|
||||||
|
|
||||||
admin_router = APIRouter(prefix="/admin")
|
admin_router = APIRouter(prefix="/admin")
|
||||||
@@ -156,3 +158,112 @@ async def delete_group(
|
|||||||
).scalar_one()
|
).scalar_one()
|
||||||
session.delete(group)
|
session.delete(group)
|
||||||
return RedirectResponse(url="/admin/groups", status_code=status.HTTP_303_SEE_OTHER)
|
return RedirectResponse(url="/admin/groups", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
|
||||||
|
# PRODUCTS
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/products")
|
||||||
|
async def get_products(
|
||||||
|
request: Request,
|
||||||
|
session: SessionDep,
|
||||||
|
user: UserDep,
|
||||||
|
):
|
||||||
|
products = session.scalars(
|
||||||
|
select(Product).order_by(Product.area_id, Product.name)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
templates = get_jinja_renderer()
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"products.html.jinja",
|
||||||
|
context={"request": request, "products": products},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/products/edit/{product_id}")
|
||||||
|
async def edit_product_get(
|
||||||
|
request: Request, session: SessionDep, user: UserDep, product_id: int
|
||||||
|
):
|
||||||
|
product = session.execute(select(Product).where(Product.id == product_id)).scalar()
|
||||||
|
|
||||||
|
areas = session.scalars(select(Area)).all()
|
||||||
|
|
||||||
|
templates = get_jinja_renderer()
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"product_edit.html.jinja",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"product": product,
|
||||||
|
"edit_mode": True,
|
||||||
|
"areas": areas,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post("/products/edit/{product_id}")
|
||||||
|
async def edit_product_post(
|
||||||
|
request: Request,
|
||||||
|
session: SessionDep,
|
||||||
|
user: UserDep,
|
||||||
|
product_id: int,
|
||||||
|
product_data: Annotated[types.Product, Form()],
|
||||||
|
):
|
||||||
|
if not user.has_permission("product", "edit"):
|
||||||
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
|
|
||||||
|
product = session.execute(
|
||||||
|
select(Product).where(Product.id == product_id)
|
||||||
|
).scalar_one()
|
||||||
|
|
||||||
|
for field_name, data in product_data.model_dump().items():
|
||||||
|
setattr(product, field_name, data)
|
||||||
|
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get("/products/new")
|
||||||
|
async def new_product_get(
|
||||||
|
request: Request,
|
||||||
|
session: SessionDep,
|
||||||
|
user: UserDep,
|
||||||
|
):
|
||||||
|
if not user.has_permission("product", "edit"):
|
||||||
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
|
|
||||||
|
areas = session.scalars(select(Area)).all()
|
||||||
|
|
||||||
|
templates = get_jinja_renderer()
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"product_edit.html.jinja",
|
||||||
|
context={"request": request, "edit_mode": False, "areas": areas},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.post("/products/new")
|
||||||
|
async def new_product_post(
|
||||||
|
request: Request,
|
||||||
|
session: SessionDep,
|
||||||
|
user: UserDep,
|
||||||
|
product_data: Annotated[types.Product, Form()],
|
||||||
|
# product_image: Annotated[bytes, File()]
|
||||||
|
):
|
||||||
|
if not user.has_permission("product", "edit"):
|
||||||
|
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||||
|
# print(len(product_image))
|
||||||
|
product = Product()
|
||||||
|
|
||||||
|
for field_name, data in product_data.model_dump().items():
|
||||||
|
setattr(product, field_name, data)
|
||||||
|
|
||||||
|
session.add(product)
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
|
||||||
|
)
|
||||||
|
|||||||
20
src/allmende_payment_system/api/types.py
Normal file
20
src/allmende_payment_system/api/types.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import typing
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
UnitsOfMeasure = typing.Literal[
|
||||||
|
"g",
|
||||||
|
"kg",
|
||||||
|
"l",
|
||||||
|
"piece",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Product(BaseModel):
|
||||||
|
name: str
|
||||||
|
price: Decimal
|
||||||
|
area_id: int
|
||||||
|
vat_rate: Decimal
|
||||||
|
allow_fractional: bool = False
|
||||||
|
unit_of_measure: UnitsOfMeasure
|
||||||
@@ -12,6 +12,8 @@ from sqlalchemy.orm import (
|
|||||||
relationship,
|
relationship,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from allmende_payment_system.api.types import UnitsOfMeasure
|
||||||
|
|
||||||
TABLE_PREFIX = "aps_"
|
TABLE_PREFIX = "aps_"
|
||||||
|
|
||||||
|
|
||||||
@@ -131,14 +133,6 @@ class Area(Base):
|
|||||||
products: Mapped[list["Product"]] = relationship("Product")
|
products: Mapped[list["Product"]] = relationship("Product")
|
||||||
|
|
||||||
|
|
||||||
UnitsOfMeasure = typing.Literal[
|
|
||||||
"g",
|
|
||||||
"kg",
|
|
||||||
"l",
|
|
||||||
"piece",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Product(Base):
|
class Product(Base):
|
||||||
__tablename__ = TABLE_PREFIX + "product"
|
__tablename__ = TABLE_PREFIX + "product"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
Einkaufen
|
Einkaufen
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if request.state.user.has_permission("product", "edit") %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/products" class="nav-link{% if request.url.path.startswith("/admin/products")%} active{% endif %}">
|
||||||
|
Produktverwaltung
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if request.state.user.has_permission("user", "edit") %}
|
{% if request.state.user.has_permission("user", "edit") %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="/admin/users" class="nav-link{% if request.url.path.startswith("/admin/users")%} active{% endif %}">
|
<a href="/admin/users" class="nav-link{% if request.url.path.startswith("/admin/users")%} active{% endif %}">
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{% extends "base.html.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="h4 mb-3">Produkt {% if not edit_mode %}erstellen{% else %}bearbeiten{% endif %}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="unit_of_measure" class="form-label">Einheit</label>
|
||||||
|
<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="kg" {% if edit_mode and product.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="piece" {% if edit_mode and product.unit_of_measure == 'piece' %}selected{% endif %}>Stück</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 %}>
|
||||||
|
<label class="form-check-label" for="allow_fractional">Bruchteile erlauben</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="vat_rate" class="form-label">MwSt-Satz</label>
|
||||||
|
<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="19" {% if edit_mode and product.vat_rate == '19' %}selected{% endif %}>19 %</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="area" class="form-label">Bereich</label>
|
||||||
|
<select class="form-select" id="area" name="area_id" required>
|
||||||
|
{% for area in areas %}
|
||||||
|
<option value="{{ area.id }}" {% if edit_mode and area.id == product.area_id %}selected{% endif %}>{{ area.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="mb-3">
|
||||||
|
<label for="image_path" class="form-label">Bild-Pfad</label>
|
||||||
|
<input class="form-control" type="file" id="image_path" accept="image/*">
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
43
src/allmende_payment_system/templates/products.html.jinja
Normal file
43
src/allmende_payment_system/templates/products.html.jinja
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends "base.html.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="h4 mb-3">Produkte verwalten</h2>
|
||||||
|
<a class="btn btn-primary" href="/admin/products/new">Neues Produkt erstellen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products|length == 0 %}
|
||||||
|
<div class="alert alert-info">Keine Produkte vorhanden.</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Bereich</th>
|
||||||
|
<th>Preis</th>
|
||||||
|
<th>Einheit</th>
|
||||||
|
<th>Steuersatz</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in products %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ product.id }}</td>
|
||||||
|
<td>{{ product.name }}</td>
|
||||||
|
<td>{{ product.area.name }}</td>
|
||||||
|
<td>{{ product.price | format_number}} €</td>
|
||||||
|
<td>{{ product.unit_of_measure | units_of_measure_de}}</td>
|
||||||
|
<td>{{ product.vat_rate | int }} %</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-sm btn-primary" href="/admin/products/edit/{{ product.id }}">Bearbeiten</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user