Add shop, POS sync, event ticketing, and shipping catalog apps.

Clients can run in-house retail without Shopify while keeping the same
Docker/Django/Postgres stack and data-ownership promise. Closes #5.
This commit is contained in:
2026-09-06 06:29:50 -05:00
parent 97b8607bf2
commit 80a5f5dadf
78 changed files with 3395 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Checkout cancelled{% endblock %}
{% block content %}
<section class="section section-lg"><div class="container">
<h1>Checkout cancelled</h1>
<p>Order {{ order.number }} was not paid. You can return to the <a href="{% url 'shop:cart' %}">cart</a>.</p>
</div></section>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Cart · {{ SITE_NAME }}{% endblock %}
{% block content %}
<section class="section section-lg">
<div class="container">
<h1>Cart</h1>
{% for line in lines %}
<p>
<a href="{{ line.product.get_absolute_url }}">{{ line.product.name }}</a>
· {{ line.quantity }} × {{ line.unit_price }} = {{ line.line_total }}
</p>
<form method="post" action="{% url 'shop:cart_update' line.product.slug %}" style="margin:0 0 16px">
{% csrf_token %}
<input name="quantity" type="number" min="0" value="{{ line.quantity }}">
<button type="submit">Update</button>
</form>
{% empty %}
<p>Cart is empty.</p>
{% endfor %}
{% if lines %}
<p><strong>Total:</strong> {{ total }}</p>
<p><a class="button button-primary" href="{% url 'shop:checkout' %}">Checkout</a></p>
{% endif %}
</div>
</section>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Checkout · {{ SITE_NAME }}{% endblock %}
{% block content %}
<section class="section section-lg">
<div class="container">
<h1>Checkout</h1>
<p>Total: {{ total }}</p>
<form method="post">
{% csrf_token %}
<p><label>Email <input name="email" type="email" required></label></p>
<p><label>Name <input name="customer_name"></label></p>
<p><label>Address <input name="address_line1"></label></p>
<p><label>Address 2 <input name="address_line2"></label></p>
<p><label>City <input name="address_city"></label></p>
<p><label>State <input name="address_state"></label></p>
<p><label>ZIP <input name="address_zip"></label></p>
<button class="button button-primary" type="submit">Pay with Stripe</button>
</form>
</div>
</section>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}{{ product.name }} · {{ SITE_NAME }}{% endblock %}
{% block content %}
<section class="section section-lg">
<div class="container">
<p><a href="{% url 'shop:list' %}">← Shop</a></p>
<h1>{{ product.name }}</h1>
<p class="muted">{{ product.price }} {{ product.currency|upper }} · {{ product.sku }}</p>
<div>{{ product.description|linebreaks }}</div>
{% if available is not None %}
<p>{{ available }} in stock</p>
{% endif %}
<form method="post" action="{% url 'shop:cart_add' product.slug %}">
{% csrf_token %}
<label>Qty <input name="quantity" type="number" min="1" value="1"></label>
<button class="button button-primary" type="submit">Add to cart</button>
</form>
</div>
</section>
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Shop · {{ SITE_NAME }}{% endblock %}
{% block content %}
<section class="section section-lg">
<div class="container">
<h1 class="text-uppercase">Shop</h1>
<p><a href="{% url 'shop:cart' %}">View cart</a></p>
{% for product in products %}
<article style="margin:0 0 32px">
<h2><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></h2>
<p class="muted">{{ product.price }} {{ product.currency|upper }} · {{ product.sku }}</p>
<p>{{ product.description|truncatewords:40 }}</p>
</article>
{% empty %}
<p>No products yet.</p>
{% endfor %}
</div>
</section>
{% endblock %}
@@ -0,0 +1,21 @@
{% extends "portal_base.html" %}
{% block title %}{{ order.number }} · Portal{% endblock %}
{% block topbar_title %}{{ order.number }}{% endblock %}
{% block portal_content %}
<p><strong>{{ order.email }}</strong> · {{ order.amount }} {{ order.currency|upper }} · {{ order.get_status_display }}</p>
{% if order.customer_name %}<p>{{ order.customer_name }}</p>{% endif %}
<table class="table">
<thead><tr><th>Item</th><th>SKU</th><th>Qty</th><th>Price</th></tr></thead>
<tbody>
{% for item in order.items.all %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.sku }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unit_price }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><a href="{% url 'shop_portal:order_list' %}">← All orders</a></p>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends "portal_base.html" %}
{% block title %}Orders · Portal{% endblock %}
{% block topbar_title %}Orders{% endblock %}
{% block portal_content %}
<table class="table">
<thead><tr><th>Number</th><th>Email</th><th>Amount</th><th>Status</th></tr></thead>
<tbody>
{% for order in orders %}
<tr>
<td><a href="{% url 'shop_portal:order_detail' order.pk %}">{{ order.number }}</a></td>
<td>{{ order.email }}</td>
<td>{{ order.amount }} {{ order.currency|upper }}</td>
<td>{{ order.get_status_display }}</td>
</tr>
{% empty %}
<tr><td colspan="4" class="empty-state">No orders yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
@@ -0,0 +1,33 @@
{% extends "portal_base.html" %}
{% block title %}{% if product %}Edit{% else %}New{% endif %} product · Portal{% endblock %}
{% block topbar_title %}{% if product %}Edit product{% else %}New product{% endif %}{% endblock %}
{% block portal_content %}
<form method="post" class="form-grid">
{% csrf_token %}
<div class="field"><label>Name</label><input name="name" required value="{{ product.name|default:'' }}"></div>
<div class="field"><label>SKU</label><input name="sku" value="{{ product.sku|default:'' }}" placeholder="auto from name"></div>
<div class="field"><label>Slug</label><input name="slug" value="{{ product.slug|default:'' }}" placeholder="auto from name"></div>
<div class="field"><label>Price</label><input name="price" type="number" step="0.01" min="0" required value="{{ product.price|default:'' }}"></div>
<div class="field">
<label>Fulfillment</label>
<select name="fulfillment">
<option value="stocked" {% if product.fulfillment == 'stocked' or not product %}selected{% endif %}>On-hand stock</option>
<option value="made_to_order" {% if product.fulfillment == 'made_to_order' %}selected{% endif %}>Made to order</option>
</select>
</div>
<div class="field"><label>Stock qty</label><input name="stock_qty" type="number" value="{{ product.stock_qty|default:0 }}"></div>
<div class="field"><label>Print minutes</label><input name="print_minutes" type="number" min="0" value="{{ product.print_minutes|default:0 }}"></div>
<div class="field"><label>Filament grams</label><input name="filament_grams" type="number" min="0" value="{{ product.filament_grams|default:0 }}"></div>
<div class="field"><label>Description</label><textarea name="description" style="min-height:120px">{{ product.description|default:'' }}</textarea></div>
<label><input type="checkbox" name="is_published" {% if product.is_published %}checked{% endif %}> Published</label>
<label><input type="checkbox" name="track_inventory" {% if product.track_inventory or not product %}checked{% endif %}> Track inventory</label>
<button class="btn btn-primary" type="submit">Save</button>
</form>
{% if product %}
<form method="post" action="{% url 'shop_portal:product_stock' product.pk %}" class="form-grid" style="margin-top:24px">
{% csrf_token %}
<div class="field"><label>Adjust stock (+/)</label><input name="delta" type="number" required value="1"></div>
<button class="btn btn-ghost" type="submit">Apply adjustment</button>
</form>
{% endif %}
{% endblock %}
@@ -0,0 +1,22 @@
{% extends "portal_base.html" %}
{% block title %}Products · Portal{% endblock %}
{% block topbar_title %}Products{% endblock %}
{% block portal_content %}
<p><a class="btn btn-primary" href="{% url 'shop_portal:product_new' %}">New product</a></p>
<table class="table">
<thead><tr><th>Name</th><th>SKU</th><th>Price</th><th>Stock</th><th>Status</th></tr></thead>
<tbody>
{% for product in products %}
<tr>
<td><a href="{% url 'shop_portal:product_edit' product.pk %}">{{ product.name }}</a></td>
<td>{{ product.sku }}</td>
<td>{{ product.price }} {{ product.currency|upper }}</td>
<td>{{ product.stock_qty }}</td>
<td>{% if product.is_published %}Published{% else %}Draft{% endif %}</td>
</tr>
{% empty %}
<tr><td colspan="5" class="empty-state">No products yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Order {{ order.number }}{% endblock %}
{% block content %}
<section class="section section-lg"><div class="container">
<h1>Thank you</h1>
<p>Order {{ order.number }} is {{ order.get_status_display|lower }}.</p>
<p>A confirmation will go to {{ order.email }}.</p>
</div></section>
{% endblock %}