feat: add email notifications, order references, stock badges, and sale flags
- Add customer email field and PayPal payment reminder emails with order reference (HEL-YEAR-ID format) - Display stock availability with color-coded badges (available/low/unavailable) - Add sale/clearance flag with animated red badge overlay - Implement automatic fallback placeholder for missing/broken product images - Add email column to order management view Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
207
wawi/app.py
207
wawi/app.py
@@ -166,6 +166,7 @@ def init_db() -> None:
|
||||
db.commit()
|
||||
ensure_price_column(db)
|
||||
ensure_image_column(db)
|
||||
ensure_sale_column(db)
|
||||
ensure_orders_columns(db)
|
||||
ensure_payment_columns(db)
|
||||
ensure_indexes(db)
|
||||
@@ -190,6 +191,16 @@ def ensure_image_column(db: sqlite3.Connection) -> None:
|
||||
db.commit()
|
||||
|
||||
|
||||
def ensure_sale_column(db: sqlite3.Connection) -> None:
|
||||
"""Fügt sale-Spalte nachträglich hinzu."""
|
||||
cols = db.execute("PRAGMA table_info(items)").fetchall()
|
||||
if any(c["name"] == "sale" for c in cols):
|
||||
return
|
||||
db.execute("ALTER TABLE items ADD COLUMN sale INTEGER NOT NULL DEFAULT 0")
|
||||
db.commit()
|
||||
logger.info("Sale-Spalte für items erstellt")
|
||||
|
||||
|
||||
def ensure_orders_columns(db: sqlite3.Connection) -> None:
|
||||
"""Sorgt für alle nachträglich eingeführten Orders‑Spalten."""
|
||||
cols = db.execute("PRAGMA table_info(orders)").fetchall()
|
||||
@@ -206,6 +217,8 @@ def ensure_orders_columns(db: sqlite3.Connection) -> None:
|
||||
db.execute("ALTER TABLE orders ADD COLUMN canceled_by TEXT")
|
||||
if "canceled_at" not in names:
|
||||
db.execute("ALTER TABLE orders ADD COLUMN canceled_at TEXT")
|
||||
if "email" not in names:
|
||||
db.execute("ALTER TABLE orders ADD COLUMN email TEXT")
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -393,6 +406,7 @@ def index():
|
||||
q = (request.args.get("q") or "").strip()
|
||||
sort = (request.args.get("sort") or "gezaehlt").strip().lower()
|
||||
direction = (request.args.get("dir") or "desc").strip().lower()
|
||||
current_year = datetime.now().year
|
||||
|
||||
allowed = {"artikel", "groesse", "soll", "gezaehlt", "verkaeufe"}
|
||||
if sort not in allowed:
|
||||
@@ -400,7 +414,7 @@ def index():
|
||||
if direction not in {"asc", "desc"}:
|
||||
direction = "desc"
|
||||
|
||||
params: list[Any] = []
|
||||
params: list[Any] = [str(current_year)]
|
||||
where = ""
|
||||
if q:
|
||||
where = "WHERE artikel LIKE ? OR groesse LIKE ?"
|
||||
@@ -408,9 +422,20 @@ def index():
|
||||
params.extend([like, like])
|
||||
|
||||
sql = f"""
|
||||
SELECT *
|
||||
SELECT
|
||||
items.*,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN orders.done = 1
|
||||
AND orders.canceled = 0
|
||||
AND orders.completed_at IS NOT NULL
|
||||
AND substr(orders.completed_at, 1, 4) = ?
|
||||
THEN orders.menge
|
||||
ELSE 0
|
||||
END), 0) AS verkaeufe_aktuell
|
||||
FROM items
|
||||
LEFT JOIN orders ON items.artikel = orders.artikel AND items.groesse = orders.groesse
|
||||
{where}
|
||||
GROUP BY items.id
|
||||
ORDER BY {sort} {direction}, artikel ASC, groesse ASC
|
||||
"""
|
||||
rows = get_db().execute(sql, params).fetchall()
|
||||
@@ -433,6 +458,7 @@ def index():
|
||||
total=total,
|
||||
total_bestand=total_bestand,
|
||||
open_orders=open_orders,
|
||||
current_year=current_year,
|
||||
)
|
||||
|
||||
|
||||
@@ -448,6 +474,7 @@ def new_item():
|
||||
uploaded = save_upload(request.files.get("bild_file"))
|
||||
if uploaded:
|
||||
bild_url = uploaded
|
||||
sale = 1 if request.form.get("sale") else 0
|
||||
soll = int(request.form.get("soll") or 0)
|
||||
gezaehlt = int(request.form.get("gezaehlt") or 0)
|
||||
verkaeufe = int(request.form.get("verkaeufe") or 0)
|
||||
@@ -456,12 +483,12 @@ def new_item():
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO items (artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO items (artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, now_iso(), now_iso()),
|
||||
(artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, now_iso(), now_iso()),
|
||||
)
|
||||
db.execute("UPDATE items SET preis = ?, bild_url = ? WHERE artikel = ?", (preis, bild_url, artikel))
|
||||
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ? WHERE artikel = ?", (preis, bild_url, sale, artikel))
|
||||
db.commit()
|
||||
return redirect(url_for("bp.index"))
|
||||
|
||||
@@ -485,6 +512,7 @@ def edit_item(item_id: int):
|
||||
uploaded = save_upload(request.files.get("bild_file"))
|
||||
if uploaded:
|
||||
bild_url = uploaded
|
||||
sale = 1 if request.form.get("sale") else 0
|
||||
soll = int(request.form.get("soll") or 0)
|
||||
gezaehlt = int(request.form.get("gezaehlt") or 0)
|
||||
verkaeufe = int(request.form.get("verkaeufe") or 0)
|
||||
@@ -492,12 +520,12 @@ def edit_item(item_id: int):
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE items
|
||||
SET artikel = ?, groesse = ?, preis = ?, bild_url = ?, soll = ?, gezaehlt = ?, verkaeufe = ?, updated_at = ?
|
||||
SET artikel = ?, groesse = ?, preis = ?, bild_url = ?, sale = ?, soll = ?, gezaehlt = ?, verkaeufe = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe, now_iso(), item_id),
|
||||
(artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, now_iso(), item_id),
|
||||
)
|
||||
db.execute("UPDATE items SET preis = ?, bild_url = ? WHERE artikel = ?", (preis, bild_url, artikel))
|
||||
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ? WHERE artikel = ?", (preis, bild_url, sale, artikel))
|
||||
db.commit()
|
||||
return redirect(url_for("bp.index"))
|
||||
|
||||
@@ -677,7 +705,7 @@ def build_bestand() -> list[dict]:
|
||||
"""Aggregiert DB‑Zeilen in die Struktur der Live‑Bestand Ansicht."""
|
||||
rows = get_db().execute(
|
||||
"""
|
||||
SELECT artikel, groesse, preis, bild_url, soll, gezaehlt, verkaeufe
|
||||
SELECT artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe
|
||||
FROM items
|
||||
ORDER BY artikel, groesse
|
||||
"""
|
||||
@@ -690,12 +718,15 @@ def build_bestand() -> list[dict]:
|
||||
continue
|
||||
item = data.setdefault(
|
||||
artikel,
|
||||
{"artikel": artikel, "preis": 0, "bild_url": "", "rows": [], "totals": {"soll": 0, "gezaehlt": 0, "abweichung": 0, "fehlbestand": 0, "verkaeufe": 0}},
|
||||
{"artikel": artikel, "preis": 0, "bild_url": "", "sale": 0, "rows": [], "totals": {"soll": 0, "gezaehlt": 0, "abweichung": 0, "fehlbestand": 0, "verkaeufe": 0}},
|
||||
)
|
||||
if not item["preis"]:
|
||||
item["preis"] = float(r["preis"] or 0)
|
||||
if not item["bild_url"]:
|
||||
item["bild_url"] = (r["bild_url"] or "").strip()
|
||||
# Sale-Status (1 wenn mindestens eine Größe sale=1 hat)
|
||||
if r["sale"]:
|
||||
item["sale"] = 1
|
||||
soll = int(r["soll"] or 0)
|
||||
gezaehlt = int(r["gezaehlt"] or 0)
|
||||
verkaeufe = int(r["verkaeufe"] or 0)
|
||||
@@ -743,7 +774,7 @@ def order():
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
data = request.get_json(silent=True) or request.form
|
||||
required = ["name", "handy", "mannschaft", "artikel", "groesse", "menge"]
|
||||
required = ["name", "handy", "email", "mannschaft", "artikel", "groesse", "menge"]
|
||||
if any(not (data.get(k) or "").strip() for k in required):
|
||||
return jsonify({"error": "Pflichtfelder fehlen."}), 400
|
||||
if int(data.get("menge") or 0) <= 0:
|
||||
@@ -754,14 +785,15 @@ def order():
|
||||
payment_method = "bar"
|
||||
|
||||
db = get_db()
|
||||
db.execute(
|
||||
cursor = db.execute(
|
||||
"""
|
||||
INSERT INTO orders (name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid')
|
||||
INSERT INTO orders (name, handy, email, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid')
|
||||
""",
|
||||
(
|
||||
data.get("name"),
|
||||
data.get("handy"),
|
||||
data.get("email"),
|
||||
data.get("mannschaft"),
|
||||
data.get("artikel"),
|
||||
data.get("groesse"),
|
||||
@@ -771,8 +803,9 @@ def order():
|
||||
payment_method,
|
||||
),
|
||||
)
|
||||
order_id = cursor.lastrowid
|
||||
db.commit()
|
||||
logger.info(f"Neue Bestellung: {data.get('artikel')} ({data.get('groesse')}) x{data.get('menge')} von {data.get('name')}")
|
||||
logger.info(f"Neue Bestellung #{order_id}: {data.get('artikel')} ({data.get('groesse')}) x{data.get('menge')} von {data.get('name')}")
|
||||
|
||||
to_addr = os.environ.get("ORDER_TO", "bjoern@welker.me")
|
||||
smtp_host = os.environ.get("SMTP_HOST")
|
||||
@@ -821,6 +854,69 @@ def order():
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
# PayPal payment reminder to customer
|
||||
if payment_method == "paypal":
|
||||
customer_email = data.get("email", "").strip()
|
||||
if customer_email and smtp_host and smtp_user and smtp_pass:
|
||||
paypal_account = os.environ.get("PAYPAL_ACCOUNT", "")
|
||||
|
||||
customer_msg = EmailMessage()
|
||||
customer_msg["Subject"] = "Zahlungshinweis für deine Hellas-Bestellung"
|
||||
customer_msg["From"] = smtp_from
|
||||
customer_msg["To"] = customer_email
|
||||
|
||||
# Calculate total price (need to get item price from DB)
|
||||
item = db.execute(
|
||||
"SELECT preis FROM items WHERE artikel = ? AND groesse = ? LIMIT 1",
|
||||
(data.get("artikel"), data.get("groesse"))
|
||||
).fetchone()
|
||||
preis = float(item["preis"]) if item else 0
|
||||
menge = int(data.get("menge") or 0)
|
||||
total = preis * menge
|
||||
|
||||
# Referenznummer im Format HEL-2026-123
|
||||
year = datetime.now().year
|
||||
ref_number = f"HEL-{year}-{order_id}"
|
||||
|
||||
customer_body = (
|
||||
f"Hallo {data.get('name')},\n\n"
|
||||
f"vielen Dank für deine Bestellung bei Hellas 1899!\n\n"
|
||||
f"Bestelldetails:\n"
|
||||
f"• Bestellnummer: {ref_number}\n"
|
||||
f"• Artikel: {data.get('artikel')}\n"
|
||||
f"• Größe: {data.get('groesse')}\n"
|
||||
f"• Menge: {menge}\n"
|
||||
f"• Preis: {total:.2f} €\n\n"
|
||||
f"⚠️ WICHTIG - Zahlungshinweis:\n"
|
||||
f"Du hast PayPal als Zahlungsart gewählt. Bitte überweise den Betrag an:\n\n"
|
||||
f"PayPal-Konto: {paypal_account}\n"
|
||||
f"Betrag: {total:.2f} €\n"
|
||||
f"Verwendungszweck: {ref_number}\n\n"
|
||||
f"WICHTIG: Bitte wähle unbedingt die Option \"Familie & Freunde\", damit keine zusätzlichen Gebühren anfallen!\n"
|
||||
f"Bitte gib die Bestellnummer {ref_number} als Verwendungszweck an, damit wir deine Zahlung zuordnen können.\n\n"
|
||||
f"Sobald der Betrag eingegangen ist, wird deine Bestellung bearbeitet und du wirst benachrichtigt, wenn sie zur Abholung bereit ist.\n\n"
|
||||
f"Viele Grüße\n"
|
||||
f"Dein Hellas 1899 Team\n"
|
||||
)
|
||||
customer_msg.set_content(customer_body)
|
||||
|
||||
def _send_customer():
|
||||
"""Sendet Zahlungs-Erinnerung an Kunden asynchron."""
|
||||
try:
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(customer_msg, to_addrs=[customer_email])
|
||||
logger.info(f"PayPal-Zahlungshinweis gesendet an {customer_email}")
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(f"SMTP-Authentifizierung fehlgeschlagen (Kunden-Email): {e}")
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error(f"SMTP-Fehler beim Kunden-Email-Versand: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unerwarteter Fehler beim Kunden-Email-Versand: {e}", exc_info=True)
|
||||
|
||||
threading.Thread(target=_send_customer, daemon=True).start()
|
||||
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@@ -831,7 +927,7 @@ def orders():
|
||||
"""Bestellliste in der Verwaltung."""
|
||||
rows = get_db().execute(
|
||||
"""
|
||||
SELECT id, name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status, paid_at, paid_by
|
||||
SELECT id, name, handy, email, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status, paid_at, paid_by
|
||||
FROM orders
|
||||
ORDER BY id DESC
|
||||
LIMIT 500
|
||||
@@ -874,11 +970,12 @@ def complete_order(order_id: int):
|
||||
"""
|
||||
UPDATE items
|
||||
SET gezaehlt = CASE WHEN gezaehlt - ? < 0 THEN 0 ELSE gezaehlt - ? END,
|
||||
soll = CASE WHEN soll - ? < 0 THEN 0 ELSE soll - ? END,
|
||||
verkaeufe = verkaeufe + ?,
|
||||
updated_at = ?
|
||||
WHERE artikel = ? AND groesse = ?
|
||||
""",
|
||||
(menge, menge, menge, now_iso(), order["artikel"], order["groesse"]),
|
||||
(menge, menge, menge, menge, menge, now_iso(), order["artikel"], order["groesse"]),
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
@@ -930,6 +1027,80 @@ def cancel_order(order_id: int):
|
||||
return redirect(url_for("bp.orders"))
|
||||
|
||||
|
||||
@bp.route("/abrechnung")
|
||||
@login_required
|
||||
def abrechnung():
|
||||
"""Abrechnungs-Ansicht für das aktuelle Jahr."""
|
||||
db = get_db()
|
||||
current_year = datetime.now().year
|
||||
year = request.args.get("year", str(current_year))
|
||||
|
||||
# Alle erledigten Bestellungen im gewählten Jahr oder vor 2026
|
||||
if year == "ALT":
|
||||
orders = db.execute(
|
||||
"""
|
||||
SELECT o.*, i.preis
|
||||
FROM orders o
|
||||
LEFT JOIN items i ON o.artikel = i.artikel AND o.groesse = i.groesse
|
||||
WHERE o.done = 1 AND o.canceled = 0
|
||||
AND o.completed_at IS NOT NULL
|
||||
AND substr(o.completed_at, 1, 4) < '2026'
|
||||
ORDER BY o.completed_at DESC
|
||||
""",
|
||||
).fetchall()
|
||||
else:
|
||||
orders = db.execute(
|
||||
"""
|
||||
SELECT o.*, i.preis
|
||||
FROM orders o
|
||||
LEFT JOIN items i ON o.artikel = i.artikel AND o.groesse = i.groesse
|
||||
WHERE o.done = 1 AND o.canceled = 0
|
||||
AND o.completed_at IS NOT NULL
|
||||
AND substr(o.completed_at, 1, 4) = ?
|
||||
ORDER BY o.completed_at DESC
|
||||
""",
|
||||
(year,),
|
||||
).fetchall()
|
||||
|
||||
# Gesamtumsatz berechnen
|
||||
total_revenue = sum((o["preis"] or 0) * (o["menge"] or 0) for o in orders)
|
||||
total_items = sum(o["menge"] or 0 for o in orders)
|
||||
|
||||
# Nach Zahlungsart gruppieren
|
||||
paypal_orders = [o for o in orders if o["payment_method"] == "paypal"]
|
||||
bar_orders = [o for o in orders if o["payment_method"] == "bar"]
|
||||
paypal_revenue = sum((o["preis"] or 0) * (o["menge"] or 0) for o in paypal_orders)
|
||||
bar_revenue = sum((o["preis"] or 0) * (o["menge"] or 0) for o in bar_orders)
|
||||
|
||||
# Nach Artikel gruppieren
|
||||
artikel_stats = {}
|
||||
for o in orders:
|
||||
key = o["artikel"]
|
||||
if key not in artikel_stats:
|
||||
artikel_stats[key] = {"menge": 0, "umsatz": 0}
|
||||
artikel_stats[key]["menge"] += o["menge"] or 0
|
||||
artikel_stats[key]["umsatz"] += (o["preis"] or 0) * (o["menge"] or 0)
|
||||
|
||||
artikel_list = [
|
||||
{"artikel": k, "menge": v["menge"], "umsatz": v["umsatz"]}
|
||||
for k, v in sorted(artikel_stats.items(), key=lambda x: x[1]["umsatz"], reverse=True)
|
||||
]
|
||||
|
||||
return render_template(
|
||||
"abrechnung.html",
|
||||
year=year,
|
||||
current_year=current_year,
|
||||
orders=orders,
|
||||
total_revenue=total_revenue,
|
||||
total_items=total_items,
|
||||
paypal_count=len(paypal_orders),
|
||||
bar_count=len(bar_orders),
|
||||
paypal_revenue=paypal_revenue,
|
||||
bar_revenue=bar_revenue,
|
||||
artikel_list=artikel_list,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/users/reset/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def reset_user_password(user_id: int):
|
||||
|
||||
19
wawi/static/style.css
Executable file → Normal file
19
wawi/static/style.css
Executable file → Normal file
@@ -198,25 +198,6 @@ input[type="text"], input[type="number"] {
|
||||
}
|
||||
.to-top:hover { transform: translateY(-2px); }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge.success {
|
||||
background: rgba(123, 213, 141, 0.2);
|
||||
color: var(--ok);
|
||||
border: 1px solid rgba(123, 213, 141, 0.4);
|
||||
}
|
||||
.badge.warning {
|
||||
background: rgba(243, 213, 42, 0.2);
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(243, 213, 42, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.btn.small { padding: 6px 8px; }
|
||||
|
||||
116
wawi/templates/abrechnung.html
Executable file
116
wawi/templates/abrechnung.html
Executable file
@@ -0,0 +1,116 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="toolbar">
|
||||
<h2 style="margin: 0; font-size: 20px;">Abrechnung {{ year }}</h2>
|
||||
<form class="search" method="get" action="{{ url_for('bp.abrechnung') }}">
|
||||
<select name="year">
|
||||
{% for y in range(current_year, 2025, -1) %}
|
||||
<option value="{{ y }}" {% if year == y|string %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
<option value="ALT" {% if year == "ALT" %}selected{% endif %}>Vor 2026 (gesamt)</option>
|
||||
</select>
|
||||
<button class="btn" type="submit">Anzeigen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
||||
<div class="stat">
|
||||
<div class="label">Gesamtumsatz</div>
|
||||
<div class="value">{{ "%.2f"|format(total_revenue) }} €</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Verkaufte Artikel</div>
|
||||
<div class="value">{{ total_items }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">PayPal ({{ paypal_count }}x)</div>
|
||||
<div class="value">{{ "%.2f"|format(paypal_revenue) }} €</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="label">Bar ({{ bar_count }}x)</div>
|
||||
<div class="value">{{ "%.2f"|format(bar_revenue) }} €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<div style="padding: 16px; border-bottom: 1px solid rgba(255,255,255,.08);">
|
||||
<h3 style="margin: 0; font-size: 16px;">Umsatz nach Artikel</h3>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artikel</th>
|
||||
<th>Verkaufte Menge</th>
|
||||
<th>Umsatz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if artikel_list %}
|
||||
{% for a in artikel_list %}
|
||||
<tr>
|
||||
<td>{{ a.artikel }}</td>
|
||||
<td>{{ a.menge }}</td>
|
||||
<td>{{ "%.2f"|format(a.umsatz) }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="empty">Keine Daten für {{ year }}.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="padding: 16px; border-bottom: 1px solid rgba(255,255,255,.08);">
|
||||
<h3 style="margin: 0; font-size: 16px;">Alle erledigten Bestellungen ({{ orders|length }})</h3>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Name</th>
|
||||
<th>Mannschaft</th>
|
||||
<th>Artikel</th>
|
||||
<th>Größe</th>
|
||||
<th>Menge</th>
|
||||
<th>Preis/Stk</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Zahlungsart</th>
|
||||
<th>Zahlung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if orders %}
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>{{ o.completed_at }}</td>
|
||||
<td>{{ o.name }}</td>
|
||||
<td>{{ o.mannschaft }}</td>
|
||||
<td>{{ o.artikel }}</td>
|
||||
<td>{{ o.groesse }}</td>
|
||||
<td>{{ o.menge }}</td>
|
||||
<td>{{ "%.2f"|format(o.preis or 0) }} €</td>
|
||||
<td><strong>{{ "%.2f"|format((o.preis or 0) * (o.menge or 0)) }} €</strong></td>
|
||||
<td>
|
||||
{% if o.payment_method == "paypal" %}PayPal
|
||||
{% else %}Bar
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if o.payment_status == "paid" %}<span class="badge success">Bezahlt</span>
|
||||
{% else %}<span class="badge warning">Unbezahlt</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="10" class="empty">Keine erledigten Bestellungen für {{ year }}.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -25,6 +25,10 @@
|
||||
Bild hochladen (optional)
|
||||
<input type="file" name="bild_file" accept="image/*" />
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 8px; padding-top: 8px;">
|
||||
<input type="checkbox" name="sale" value="1" {% if item and item.sale %}checked{% endif %} style="width: auto; height: 18px;" />
|
||||
<span style="color: var(--text);">Sale / Abverkauf 🔥</span>
|
||||
</label>
|
||||
<label>
|
||||
Soll
|
||||
<input type="number" name="soll" min="0" value="{{ item.soll if item else 0 }}" />
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<th>Datum</th>
|
||||
<th>Name</th>
|
||||
<th>Handy</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Mannschaft</th>
|
||||
<th>Artikel</th>
|
||||
<th>Größe</th>
|
||||
@@ -33,6 +34,7 @@
|
||||
<td>{{ o.created_at }}</td>
|
||||
<td>{{ o.name }}</td>
|
||||
<td>{{ o.handy }}</td>
|
||||
<td>{{ o.email or "–" }}</td>
|
||||
<td>{{ o.mannschaft }}</td>
|
||||
<td>{{ o.artikel }}</td>
|
||||
<td>{{ o.groesse }}</td>
|
||||
@@ -43,7 +45,8 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if o.payment_status == "paid" %}<span class="badge success">Bezahlt</span>
|
||||
{% if o.canceled %}–
|
||||
{% elif o.payment_status == "paid" %}<span class="badge success">Bezahlt</span>
|
||||
{% else %}<span class="badge warning">Unbezahlt</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -68,7 +71,7 @@
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button class="btn small danger" type="submit">Stornieren</button>
|
||||
<button class="btn small danger" type="submit">Storno</button>
|
||||
</form>
|
||||
{% else %}
|
||||
–
|
||||
@@ -76,7 +79,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="order-history">
|
||||
<td colspan="12">
|
||||
<td colspan="13">
|
||||
<details class="inline-details">
|
||||
<summary>Historie</summary>
|
||||
<div class="history-grid">
|
||||
@@ -92,7 +95,7 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="12" class="empty">Keine Bestellungen.</td>
|
||||
<td colspan="13" class="empty">Keine Bestellungen.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user