Add public refund creation endpoint and PDF generation

Introduces RefundPublicView for public refund creation via email and invoice/order ID, returning refund info and a base64-encoded PDF slip. Adds RefundCreatePublicSerializer for validation and creation, implements PDF generation in Refund model, and provides a customer-facing HTML template for the return slip. Updates URLs to expose the new endpoint.
This commit is contained in:
2025-11-19 00:53:37 +01:00
parent b8a1a594b2
commit e86839f2da
5 changed files with 416 additions and 38 deletions

View File

@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Return/Refund Slip Order {{ order.number|default:order.code|default:order.id }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root { --fg:#111; --muted:#666; --border:#ddd; --accent:#0f172a; --bg:#fff; }
* { box-sizing: border-box; }
html, body { margin:0; padding:0; background:var(--bg); color:var(--fg); font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial; }
.sheet { max-width: 800px; margin: 24px auto; padding: 24px; border:1px solid var(--border); border-radius: 8px; }
header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:16px; }
.title { font-size:20px; font-weight:700; letter-spacing:.2px; }
.sub { color:var(--muted); font-size:12px; }
.meta { display:grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; padding:12px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; }
.meta div { display:flex; gap:8px; }
.label { width:140px; color:var(--muted); }
table { width:100%; border-collapse: collapse; margin: 12px 0 4px; }
th, td { border:1px solid var(--border); padding:8px; vertical-align: top; }
th { text-align:left; background:#f8fafc; font-weight:600; }
.muted { color:var(--muted); }
.section { margin-top:18px; }
.section h3 { margin:0 0 8px; font-size:14px; text-transform:uppercase; letter-spacing:.4px; color:var(--accent); }
.textarea { border:1px solid var(--border); border-radius:8px; min-height:90px; padding:10px; white-space:pre-wrap; }
.grid-2 { display:grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.line { height:1px; background:var(--border); margin: 8px 0; }
.sign { height:48px; border-bottom:1px solid var(--border); }
.print-tip { color:var(--muted); font-size:12px; margin-top:8px; }
.print-btn { display:inline-block; padding:8px 12px; border:1px solid var(--border); border-radius:6px; background:#f8fafc; cursor:pointer; font-size:13px; }
@media print {
.sheet { border:none; border-radius:0; margin:0; padding:0; }
.print-btn, .print-tip { display:none !important; }
body { font-size:12px; }
th, td { padding:6px; }
}
</style>
</head>
<body>
<div class="sheet">
<header>
<div>
<div class="title">Return / Refund Slip</div>
<div class="sub">Include this page inside the package for the shopkeeper to examine the return.</div>
</div>
<div>
<button class="print-btn" onclick="window.print()">Print</button>
</div>
</header>
<div class="meta">
<div><div class="label">Order number</div><div><strong>{{ order.number|default:order.code|default:order.id }}</strong></div></div>
<div><div class="label">Order date</div><div>{% if order.created_at %}{{ order.created_at|date:"Y-m-d H:i" }}{% else %}{% now "Y-m-d" %}{% endif %}</div></div>
<div><div class="label">Customer name</div><div>{{ order.customer_name|default:order.user.get_full_name|default:order.user.username|default:"" }}</div></div>
<div><div class="label">Customer email</div><div>{{ order.customer_email|default:order.user.email|default:"" }}</div></div>
<div><div class="label">Phone</div><div>{{ order.customer_phone|default:"" }}</div></div>
<div><div class="label">Return created</div><div>{% now "Y-m-d H:i" %}</div></div>
</div>
<div class="section">
<h3>Returned items</h3>
<table>
<thead>
<tr>
<th style="width:44%">Item</th>
<th style="width:16%">SKU</th>
<th style="width:10%">Qty</th>
<th style="width:30%">Reason (per item)</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr>
<td>
<div><strong>{{ it.product_name|default:it.product.title|default:it.name|default:"Item" }}</strong></div>
{% if it.variant or it.options %}
<div class="muted" style="font-size:12px;">
{% if it.variant %}Variant: {{ it.variant }}{% endif %}
{% if it.options %}{% if it.variant %} • {% endif %}Options: {{ it.options }}{% endif %}
</div>
{% endif %}
</td>
<td>{{ it.sku|default:"—" }}</td>
<td>{{ it.quantity|default:1 }}</td>
<td>{% if it.reason %}{{ it.reason }}{% else %}&nbsp;{% endif %}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="muted">No items listed.</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="print-tip">Tip: If the reason differs per item, write it in the last column above.</div>
</div>
<div class="section">
<h3>Return reason (customer)</h3>
<div class="textarea">
{% if return_reason %}{{ return_reason }}{% else %}
{% endif %}
</div>
</div>
<div class="section">
<h3>Shopkeeper inspection</h3>
<div class="grid-2">
<div>
<div class="row">
<strong>Package condition:</strong>
[ ] Intact
[ ] Opened
[ ] Damaged
</div>
<div class="row" style="margin-top:6px;">
<strong>Items condition:</strong>
[ ] New
[ ] Light wear
[ ] Used
[ ] Damaged
</div>
</div>
<div>
<div class="row">
<strong>Resolution:</strong>
[ ] Accept refund
[ ] Deny
[ ] Exchange
</div>
<div class="row" style="margin-top:6px;">
<strong>Restocking fee:</strong> ________ %
</div>
</div>
</div>
<div class="section" style="margin-top:12px;">
<div class="row"><strong>Notes:</strong></div>
<div class="textarea" style="min-height:70px;"></div>
</div>
<div class="grid-2" style="margin-top:16px;">
<div>
<div class="muted" style="font-size:12px;">Processed by (name/signature)</div>
<div class="sign"></div>
</div>
<div>
<div class="muted" style="font-size:12px;">Date</div>
<div class="sign"></div>
</div>
</div>
</div>
<div class="line"></div>
<div class="muted" style="font-size:12px; margin-top:8px;">
Attach this slip inside the package. Keep a copy for your records.
</div>
</div>
</body>
</html>