generated from westfarn/web_django_template
Add customer accounts, shipment tracking, and purchase reviews.
CI / test (pull_request) Successful in 35s
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
This commit is contained in:
@@ -6,9 +6,11 @@ import csv
|
||||
import io
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from shipping.models import Shipment
|
||||
from shop.models import Order
|
||||
@@ -135,6 +137,10 @@ def buy_label(shipment: Shipment, *, rate_id: str = "") -> Shipment:
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
except Exception:
|
||||
logger.exception("tracking refresh failed after label buy for %s", shipment.pk)
|
||||
return shipment
|
||||
|
||||
|
||||
@@ -182,3 +188,169 @@ def pirate_ship_csv(orders=None) -> str:
|
||||
]
|
||||
)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def public_tracking_url(carrier: str, tracking_number: str) -> str:
|
||||
code = quote((tracking_number or "").strip())
|
||||
if not code:
|
||||
return ""
|
||||
name = (carrier or "").upper()
|
||||
if "USPS" in name:
|
||||
return f"https://tools.usps.com/go/TrackConfirmAction?tLabels={code}"
|
||||
if "UPS" in name:
|
||||
return f"https://www.ups.com/track?tracknum={code}"
|
||||
if "FEDEX" in name or "FDX" in name:
|
||||
return f"https://www.fedex.com/fedextrack/?trknbr={code}"
|
||||
if "DHL" in name:
|
||||
return f"https://www.dhl.com/en/express/tracking.html?AWB={code}"
|
||||
return f"https://www.google.com/search?q={quote((tracking_number or '') + ' tracking')}"
|
||||
|
||||
|
||||
def _normalize_tracking_status(raw: str) -> str:
|
||||
value = (raw or "").strip().lower().replace(" ", "_")
|
||||
aliases = {
|
||||
"pretransit": Shipment.TrackingStatus.PRE_TRANSIT,
|
||||
"pre_transit": Shipment.TrackingStatus.PRE_TRANSIT,
|
||||
"in_transit": Shipment.TrackingStatus.IN_TRANSIT,
|
||||
"out_for_delivery": Shipment.TrackingStatus.OUT_FOR_DELIVERY,
|
||||
"delivered": Shipment.TrackingStatus.DELIVERED,
|
||||
"available_for_pickup": Shipment.TrackingStatus.AVAILABLE_FOR_PICKUP,
|
||||
"return_to_sender": Shipment.TrackingStatus.RETURN_TO_SENDER,
|
||||
"failure": Shipment.TrackingStatus.FAILURE,
|
||||
"cancelled": Shipment.TrackingStatus.CANCELLED,
|
||||
"canceled": Shipment.TrackingStatus.CANCELLED,
|
||||
"error": Shipment.TrackingStatus.ERROR,
|
||||
"unknown": Shipment.TrackingStatus.UNKNOWN,
|
||||
}
|
||||
return aliases.get(value, Shipment.TrackingStatus.UNKNOWN)
|
||||
|
||||
|
||||
def apply_tracker_payload(shipment: Shipment, payload: dict) -> Shipment:
|
||||
"""Apply EasyPost tracker (or compatible) JSON onto a shipment."""
|
||||
data = payload or {}
|
||||
tracking = (data.get("tracking_code") or data.get("tracking_number") or "").strip()
|
||||
if tracking:
|
||||
shipment.tracking_number = tracking
|
||||
tracker_id = (data.get("id") or "").strip()
|
||||
if tracker_id.startswith("trk_"):
|
||||
shipment.tracker_id = tracker_id
|
||||
carrier = (data.get("carrier") or "").strip()
|
||||
if carrier and not shipment.carrier:
|
||||
shipment.carrier = carrier
|
||||
shipment.tracking_status = _normalize_tracking_status(data.get("status") or "")
|
||||
public_url = (data.get("public_url") or "").strip()
|
||||
shipment.tracking_url = public_url or public_tracking_url(
|
||||
shipment.carrier, shipment.tracking_number
|
||||
)
|
||||
events = []
|
||||
for item in data.get("tracking_details") or []:
|
||||
loc = item.get("tracking_location") or {}
|
||||
place = " ".join(
|
||||
part
|
||||
for part in [loc.get("city") or "", loc.get("state") or ""]
|
||||
if part
|
||||
).strip()
|
||||
events.append(
|
||||
{
|
||||
"status": item.get("status") or "",
|
||||
"message": item.get("message") or "",
|
||||
"datetime": item.get("datetime") or "",
|
||||
"location": place,
|
||||
}
|
||||
)
|
||||
if events:
|
||||
shipment.tracking_events = events
|
||||
shipment.last_tracked_at = timezone.now()
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"tracking_number",
|
||||
"tracker_id",
|
||||
"carrier",
|
||||
"tracking_status",
|
||||
"tracking_url",
|
||||
"tracking_events",
|
||||
"last_tracked_at",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
|
||||
def refresh_tracking(shipment: Shipment) -> Shipment:
|
||||
"""Pull latest scan events from EasyPost, or stub status without an API key.
|
||||
|
||||
Works for EasyPost-bought labels and for tracking numbers pasted from
|
||||
Pirate Ship / Shippo / the carrier — EasyPost's tracker API looks up
|
||||
USPS, UPS, FedEx, and DHL by number.
|
||||
"""
|
||||
tracking = (shipment.tracking_number or "").strip()
|
||||
if not tracking:
|
||||
return shipment
|
||||
key = _easypost_key()
|
||||
if not key:
|
||||
if not shipment.tracking_status or shipment.tracking_status == Shipment.TrackingStatus.UNKNOWN:
|
||||
shipment.tracking_status = Shipment.TrackingStatus.PRE_TRANSIT
|
||||
shipment.tracking_url = shipment.tracking_url or public_tracking_url(
|
||||
shipment.carrier, tracking
|
||||
)
|
||||
shipment.last_tracked_at = timezone.now()
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"tracking_status",
|
||||
"tracking_url",
|
||||
"last_tracked_at",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
payload = {"tracker": {"tracking_code": tracking}}
|
||||
if shipment.carrier:
|
||||
payload["tracker"]["carrier"] = shipment.carrier
|
||||
response = requests.post(
|
||||
"https://api.easypost.com/v2/trackers",
|
||||
auth=(key, ""),
|
||||
json=payload,
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return apply_tracker_payload(shipment, response.json())
|
||||
|
||||
|
||||
def attach_tracking(
|
||||
shipment: Shipment, *, tracking_number: str, carrier: str = ""
|
||||
) -> Shipment:
|
||||
tracking_number = (tracking_number or "").strip()
|
||||
if not tracking_number:
|
||||
raise ShippingError("Tracking number is required.")
|
||||
shipment.tracking_number = tracking_number
|
||||
if carrier:
|
||||
shipment.carrier = carrier.strip()
|
||||
if shipment.status != Shipment.Status.LABELED:
|
||||
shipment.status = Shipment.Status.LABELED
|
||||
shipment.save(
|
||||
update_fields=["tracking_number", "carrier", "status", "updated_at"]
|
||||
)
|
||||
return refresh_tracking(shipment)
|
||||
|
||||
|
||||
def sync_open_tracking() -> int:
|
||||
"""Refresh labeled, not-yet-delivered shipments (dispatch_due)."""
|
||||
done = 0
|
||||
qs = (
|
||||
Shipment.objects.filter(status=Shipment.Status.LABELED)
|
||||
.exclude(tracking_number="")
|
||||
.exclude(
|
||||
tracking_status__in=[
|
||||
Shipment.TrackingStatus.DELIVERED,
|
||||
Shipment.TrackingStatus.CANCELLED,
|
||||
]
|
||||
)[:50]
|
||||
)
|
||||
for shipment in qs:
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
done += 1
|
||||
except Exception:
|
||||
logger.exception("tracking sync failed for %s", shipment.pk)
|
||||
return done
|
||||
|
||||
Reference in New Issue
Block a user