generated from westfarn/web_django_template
Ship Print Forge shop site (colors, photos, 3D viewer) (#2)
## Summary - Rebrand the Django client template as Print Forge (`client_site` → `print_forge`) with shop + shipping enabled. - Product listings support multiple photos, color variants (shared price/description/STL, per-color stock and photos), and a photo-first / 3D-second gallery. - Public pages use 3D printer / printed-toy photography instead of leftover t-shirt mockups; beta CI deploys on `master`. Closes #1 Infra: [server-infra#27](ai_ml_operations/server-infra#27) (easy deploy beta). ## Test plan - [ ] Product page shows photo first, 3D model second; color swatches swap photos and stock - [ ] Portal can upload multiple photos and per-color qty/images; STL stays shared - [ ] Public home/about/gallery have no t-shirt mockups - [ ] `manage.py test` passes - [ ] After server-infra#27: beta deploy to `print-forge-preview.aimloperations.com` Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Sales dashboard aggregates for FEATURE_SHOP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, F, Max, Q, Sum
|
||||
from django.utils import timezone
|
||||
|
||||
from shop.models import Order, OrderItem
|
||||
|
||||
SOLD_STATUSES = (Order.Status.PAID, Order.Status.FULFILLED)
|
||||
SALES_WINDOW_DAYS = 30
|
||||
|
||||
|
||||
def _money(value) -> Decimal:
|
||||
return (value or Decimal("0")).quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
def _window_start(days: int):
|
||||
today = timezone.localdate()
|
||||
start_date = today - timedelta(days=days - 1)
|
||||
start_dt = timezone.make_aware(
|
||||
datetime.combine(start_date, time.min),
|
||||
timezone.get_current_timezone(),
|
||||
)
|
||||
return today, start_date, start_dt
|
||||
|
||||
|
||||
def _sold_orders(start_dt=None):
|
||||
qs = Order.objects.filter(status__in=SOLD_STATUSES)
|
||||
if start_dt is None:
|
||||
return qs
|
||||
return qs.filter(
|
||||
Q(paid_at__gte=start_dt) | Q(paid_at__isnull=True, created_at__gte=start_dt)
|
||||
)
|
||||
|
||||
|
||||
def _sale_date(order) -> date:
|
||||
when = order.paid_at or order.created_at
|
||||
return timezone.localtime(when).date()
|
||||
|
||||
|
||||
def _bar_pct(value: Decimal | int | float, peak: Decimal | int | float) -> int:
|
||||
if not peak:
|
||||
return 0
|
||||
if not value:
|
||||
return 0
|
||||
return max(8, int(round((float(value) / float(peak)) * 100)))
|
||||
|
||||
|
||||
def sales_summary(*, days: int = SALES_WINDOW_DAYS) -> dict:
|
||||
"""Order count and revenue for the rolling sales window."""
|
||||
_, _, start_dt = _window_start(days)
|
||||
totals = _sold_orders(start_dt).aggregate(
|
||||
order_count=Count("id"), revenue=Sum("amount")
|
||||
)
|
||||
return {
|
||||
"order_count": int(totals["order_count"] or 0),
|
||||
"revenue": _money(totals["revenue"]),
|
||||
}
|
||||
|
||||
|
||||
def sales_dashboard(*, days: int = SALES_WINDOW_DAYS) -> dict:
|
||||
"""Paid/fulfilled order stats, daily series, and top products."""
|
||||
_, start_date, start_dt = _window_start(days)
|
||||
sold = _sold_orders(start_dt)
|
||||
totals = sold.aggregate(order_count=Count("id"), revenue=Sum("amount"))
|
||||
order_count = int(totals["order_count"] or 0)
|
||||
revenue = _money(totals["revenue"])
|
||||
units = int(
|
||||
OrderItem.objects.filter(order__in=sold).aggregate(total=Sum("quantity"))[
|
||||
"total"
|
||||
]
|
||||
or 0
|
||||
)
|
||||
aov = _money(revenue / order_count) if order_count else Decimal("0.00")
|
||||
currency = (settings.STRIPE_CURRENCY or "usd").lower()
|
||||
|
||||
by_day: dict = {
|
||||
start_date + timedelta(days=offset): {
|
||||
"count": 0,
|
||||
"revenue": Decimal("0.00"),
|
||||
}
|
||||
for offset in range(days)
|
||||
}
|
||||
for order in sold.only("paid_at", "created_at", "amount"):
|
||||
day = _sale_date(order)
|
||||
bucket = by_day.get(day)
|
||||
if bucket is None:
|
||||
continue
|
||||
bucket["count"] += 1
|
||||
bucket["revenue"] += order.amount or Decimal("0")
|
||||
|
||||
peak_count = max((row["count"] for row in by_day.values()), default=0)
|
||||
peak_revenue = max((row["revenue"] for row in by_day.values()), default=Decimal("0"))
|
||||
daily_sales = []
|
||||
daily_revenue = []
|
||||
for index, (day, row) in enumerate(by_day.items()):
|
||||
tick = index == 0 or index == days - 1 or day.weekday() == 0
|
||||
label = f"{day.strftime('%b')} {day.day}"
|
||||
daily_sales.append(
|
||||
{
|
||||
"date": day,
|
||||
"label": label,
|
||||
"count": row["count"],
|
||||
"revenue": _money(row["revenue"]),
|
||||
"pct": _bar_pct(row["count"], peak_count),
|
||||
"tick": tick,
|
||||
"tick_label": label if tick else "",
|
||||
}
|
||||
)
|
||||
daily_revenue.append(
|
||||
{
|
||||
"date": day,
|
||||
"label": label,
|
||||
"count": row["count"],
|
||||
"revenue": _money(row["revenue"]),
|
||||
"pct": _bar_pct(row["revenue"], peak_revenue),
|
||||
"tick": tick,
|
||||
"tick_label": label if tick else "",
|
||||
}
|
||||
)
|
||||
|
||||
product_rows = list(
|
||||
OrderItem.objects.filter(order__in=sold)
|
||||
.values("sku")
|
||||
.annotate(
|
||||
units=Sum("quantity"),
|
||||
revenue=Sum(F("unit_price") * F("quantity")),
|
||||
product_name=Max("product__name"),
|
||||
item_name=Max("name"),
|
||||
)
|
||||
.order_by("-units", "-revenue")[:8]
|
||||
)
|
||||
peak_units = max((int(row["units"] or 0) for row in product_rows), default=0)
|
||||
top_products = []
|
||||
for row in product_rows:
|
||||
units_sold = int(row["units"] or 0)
|
||||
top_products.append(
|
||||
{
|
||||
"sku": row["sku"],
|
||||
"name": row["product_name"] or row["item_name"] or row["sku"],
|
||||
"units": units_sold,
|
||||
"revenue": _money(row["revenue"]),
|
||||
"bar_pct": _bar_pct(units_sold, peak_units),
|
||||
}
|
||||
)
|
||||
|
||||
recent_orders = list(sold.order_by("-paid_at", "-created_at")[:8])
|
||||
|
||||
return {
|
||||
"days": days,
|
||||
"currency": currency,
|
||||
"order_count": order_count,
|
||||
"revenue": revenue,
|
||||
"units_sold": units,
|
||||
"aov": aov,
|
||||
"daily_sales": daily_sales,
|
||||
"daily_revenue": daily_revenue,
|
||||
"top_products": top_products,
|
||||
"recent_orders": recent_orders,
|
||||
"has_sales": order_count > 0,
|
||||
}
|
||||
Reference in New Issue
Block a user