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,184 @@
|
||||
"""EasyPost-style rates/labels plus Pirate Ship CSV export."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from shipping.models import Shipment
|
||||
from shop.models import Order
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShippingError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _easypost_key() -> str:
|
||||
return (getattr(settings, "EASYPOST_API_KEY", "") or "").strip()
|
||||
|
||||
|
||||
def quote_rates(shipment: Shipment) -> list[dict]:
|
||||
"""Return carrier rates. Uses EasyPost when configured; otherwise a USPS stub."""
|
||||
key = _easypost_key()
|
||||
if not key:
|
||||
rates = [
|
||||
{
|
||||
"id": "stub-usps-ground",
|
||||
"carrier": "USPS",
|
||||
"service": "GroundAdvantage",
|
||||
"rate": "5.40",
|
||||
"currency": "USD",
|
||||
},
|
||||
{
|
||||
"id": "stub-usps-priority",
|
||||
"carrier": "USPS",
|
||||
"service": "Priority",
|
||||
"rate": "9.80",
|
||||
"currency": "USD",
|
||||
},
|
||||
]
|
||||
shipment.rates = rates
|
||||
shipment.status = Shipment.Status.RATED
|
||||
shipment.save(update_fields=["rates", "status", "updated_at"])
|
||||
return rates
|
||||
|
||||
addr = shipment.order.shipping_address or {}
|
||||
response = requests.post(
|
||||
"https://api.easypost.com/v2/shipments",
|
||||
auth=(key, ""),
|
||||
json={
|
||||
"shipment": {
|
||||
"to_address": {
|
||||
"name": shipment.order.customer_name or shipment.order.email,
|
||||
"street1": addr.get("line1") or "",
|
||||
"street2": addr.get("line2") or "",
|
||||
"city": addr.get("city") or "",
|
||||
"state": addr.get("state") or "",
|
||||
"zip": addr.get("zip") or "",
|
||||
"country": addr.get("country") or "US",
|
||||
},
|
||||
"from_address": {
|
||||
"name": settings.SITE_NAME,
|
||||
"street1": getattr(settings, "SHIP_FROM_LINE1", "") or "",
|
||||
"city": getattr(settings, "SHIP_FROM_CITY", "") or "",
|
||||
"state": getattr(settings, "SHIP_FROM_STATE", "") or "",
|
||||
"zip": getattr(settings, "SHIP_FROM_ZIP", "") or "",
|
||||
"country": "US",
|
||||
},
|
||||
"parcel": {"weight": shipment.weight_oz},
|
||||
}
|
||||
},
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
rates = data.get("rates") or []
|
||||
shipment.provider_shipment_id = data.get("id") or ""
|
||||
shipment.rates = rates
|
||||
shipment.status = Shipment.Status.RATED
|
||||
shipment.save(
|
||||
update_fields=["provider_shipment_id", "rates", "status", "updated_at"]
|
||||
)
|
||||
return rates
|
||||
|
||||
|
||||
def buy_label(shipment: Shipment, *, rate_id: str = "") -> Shipment:
|
||||
rates = shipment.rates or quote_rates(shipment)
|
||||
chosen = None
|
||||
if rate_id:
|
||||
chosen = next((r for r in rates if str(r.get("id")) == str(rate_id)), None)
|
||||
if chosen is None and rates:
|
||||
chosen = rates[0]
|
||||
if chosen is None:
|
||||
raise ShippingError("No shipping rates available.")
|
||||
|
||||
key = _easypost_key()
|
||||
if key and shipment.provider_shipment_id:
|
||||
response = requests.post(
|
||||
f"https://api.easypost.com/v2/shipments/{shipment.provider_shipment_id}/buy",
|
||||
auth=(key, ""),
|
||||
json={"rate": {"id": chosen.get("id")}},
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
postage = data.get("postage_label") or {}
|
||||
tracking = data.get("tracking_code") or ""
|
||||
shipment.label_url = postage.get("label_url") or ""
|
||||
shipment.tracking_number = tracking
|
||||
else:
|
||||
shipment.tracking_number = f"STUB{shipment.order.number[-6:]}"
|
||||
shipment.label_url = ""
|
||||
|
||||
shipment.carrier = chosen.get("carrier") or ""
|
||||
shipment.service = chosen.get("service") or ""
|
||||
try:
|
||||
shipment.rate_amount = Decimal(str(chosen.get("rate") or "0"))
|
||||
except Exception:
|
||||
shipment.rate_amount = None
|
||||
shipment.status = Shipment.Status.LABELED
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"carrier",
|
||||
"service",
|
||||
"tracking_number",
|
||||
"label_url",
|
||||
"rate_amount",
|
||||
"status",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
|
||||
def create_shipment_for_order(order: Order, *, weight_oz: int = 16) -> Shipment:
|
||||
if order.status not in {Order.Status.PAID, Order.Status.FULFILLED}:
|
||||
raise ShippingError("Ship paid orders only.")
|
||||
return Shipment.objects.create(order=order, weight_oz=weight_oz)
|
||||
|
||||
|
||||
def pirate_ship_csv(orders=None) -> str:
|
||||
"""CSV Pirate Ship can map: name, address, city, state, zip, email."""
|
||||
if orders is None:
|
||||
shipped = Shipment.objects.filter(
|
||||
status=Shipment.Status.LABELED
|
||||
).values_list("order_id", flat=True)
|
||||
orders = Order.objects.filter(status=Order.Status.PAID).exclude(pk__in=shipped)
|
||||
buffer = io.StringIO()
|
||||
writer = csv.writer(buffer)
|
||||
writer.writerow(
|
||||
[
|
||||
"Order Number",
|
||||
"Name",
|
||||
"Address 1",
|
||||
"Address 2",
|
||||
"City",
|
||||
"State",
|
||||
"Zip",
|
||||
"Country",
|
||||
"Email",
|
||||
]
|
||||
)
|
||||
for order in orders:
|
||||
addr = order.shipping_address or {}
|
||||
writer.writerow(
|
||||
[
|
||||
order.number,
|
||||
order.customer_name or order.email,
|
||||
addr.get("line1") or "",
|
||||
addr.get("line2") or "",
|
||||
addr.get("city") or "",
|
||||
addr.get("state") or "",
|
||||
addr.get("zip") or "",
|
||||
addr.get("country") or "US",
|
||||
order.email,
|
||||
]
|
||||
)
|
||||
return buffer.getvalue()
|
||||
Reference in New Issue
Block a user