Merge branch 'user-purchases-page' into 'master'

user purchase page upgraded with invoice links

See merge request engineering/make-post-sell/make_post_sell!34
This commit is contained in:
Russell Ballestrini 2024-12-12 14:20:48 +00:00
commit 6cfd614829
11 changed files with 132 additions and 150 deletions

View file

@ -108,6 +108,8 @@ class User(RBase, Base):
argument="UserAddress", lazy="dynamic", back_populates="user"
)
invoices = relationship(argument="Invoice", lazy="dynamic", back_populates="user")
def __init__(self, email):
self.name = unicode(f"user-{generate_password(size=8)}")
self.created_timestamp = now_timestamp()

View file

@ -61,7 +61,8 @@ def includeme(config):
config.add_route("cart_handling_option", "/cart/{cart_id}/handling-option")
config.add_route("view_invoice", "/invoices/{invoice_id}")
config.add_route("view_invoice", "/invoice/{invoice_id}")
config.add_route("view_invoice2", "/i/{invoice_id}")
# coupon routes.
config.add_route("coupon_new1", "/coupon/new")
@ -89,7 +90,6 @@ def includeme(config):
config.add_route("shop_products", "/s/{shop_id}/products")
config.add_route("shop_sales", "/s/{shop_id}/sales")
config.add_route("shop_sales_details", "/s/{shop_id}/sales/{invoice_id}")
config.add_route("shop_settings", "/s/{shop_id}/settings")

View file

@ -19,8 +19,24 @@ h1 {
margin-bottom: .4em;
}
tr, th {
text-align: left;
table {
width: 100%;
}
table tr:nth-child(even) {
/* Light gray background for even rows */
background-color: #f2f2f2;
}
table tr:nth-child(odd) {
/* White background for odd rows (optional, for contrast) */
background-color: #ffffff;
}
tr, th, td {
padding: 8px;
border: 0px;
text-align: left;
}
label {

View file

@ -1,40 +1,35 @@
{% extends "base.j2" -%}
{% extends "base.j2" %}
{% block content -%}
<h1>Invoice Details</h1>
<section class="invoice-details">
<h2>Invoice ID: {{ invoice.id }}</h2>
<p><strong>User:</strong> {{ invoice.user.name }}</p>
<p><strong>Shop:</strong> {{ invoice.shop.name }}</p>
<p><strong>Created:</strong> {{ invoice.human_created_timestamp }}</p>
{% block content %}
<h1>Invoice Identifier<br>{{ invoice.id }}</h1>
<p><strong>Customer Name:</strong> {{ invoice.user.name }}</p>
<p><strong>Date:</strong> {{ invoice.human_created_timestamp }}</p>
<p><strong>Total:</strong> ${{ '%0.2f' % invoice.total }}</p>
<p><strong>Shipping/Pickup:</strong> {{ invoice.handling_option }}</p>
<p><strong>Handling Cost:</strong> ${{ '%0.2f' % ((invoice.handling_cost_in_cents or 0) / 100) }}</p>
{% if invoice.delivery_address %}
<p><strong>Delivery Address:</strong> {{ invoice.delivery_address }}</p>
<p><strong>Handling Option:</strong> {{ invoice.handling_option }}</p>
<p><strong>Handling Cost:</strong> ${{ '%0.2f' % (invoice.handling_cost_in_cents / 100) }}</p>
<p><strong>Total:</strong> ${{ '%0.2f' % invoice.total }}</p>
<h3>Line Items</h3>
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in line_items %}
<tr>
<td>{{ item.product.title }}</td>
<td>{{ item.quantity }}</td>
<td>${{ '%0.2f' % (item.price.price_in_cents / 100) }}</td>
<td>${{ '%0.2f' % ((item.price.price_in_cents * item.quantity) / 100) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
<h2>Line Items</h2>
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in invoice.line_items %}
<tr>
<td><a href="/p/{{ item.product.id }}">{{ item.product.title }}</a></td>
<td>{{ item.quantity }}</td>
<td>${{ '%0.2f' % (item.price.price_in_cents / 100) }}</td>
<td>${{ '%0.2f' % ((item.price.price_in_cents * item.quantity) / 100) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -1,36 +0,0 @@
{% extends "base.j2" -%}
{% block content -%}
{% if the_title %}
<center>
<h1>
{{ the_title }}
</h1>
</center>
{% endif %}
<section class="serp">
{% if products %}
{% for product in products %}
<div class="serp-item">
{% if "thumbnail1" in product.extensions %}
<a href="{{ product.absolute_url(request) }}" rel="nofollow">
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="serp-thumbnail" />
</a>
{% endif %}
<b><a href="{{ product.absolute_url(request) }}" class="shop_theme_link_color">{{ product.title }}</a></b>
{% if product.is_sellable %}
<br />
<a href="{{ product.absolute_url(request) }}" rel="nofollow">${{ '{:,.2f}'.format(product.price) }}</a>
{% endif %}
<br>
<a href="{{ product.absolute_url(request) }}" rel="nofollow">{{ product.shop.name }}</a>
</div>
{% endfor %}
{% endif %}
</section>
{%- endblock -%}

View file

@ -3,13 +3,13 @@
{% block content %}
<h1>Sales for {{ shop.name }}</h1>
<table style="width:100%;">
<table>
<thead>
<tr>
<th>Customer Name</th>
<th>Date</th>
<th>Total</th>
<th>Details</th>
<th>Invoice</th>
</tr>
</thead>
<tbody>
@ -18,7 +18,7 @@
<td>{{ invoice.user.name }}</td>
<td>{{ invoice.human_created_timestamp }}</td>
<td>${{ '%0.2f' % invoice.total }}</td>
<td><a href="{{ request.route_url('shop_sales_details', shop_id=shop.id, invoice_id=invoice.id) }}">Invoice Details</a></td>
<td><a href="/i/{{ invoice.id }}">Invoice</a></td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,35 +0,0 @@
{% extends "base.j2" %}
{% block content %}
<h1>Invoice Identifier<br>{{ invoice.id }}</h1>
<p><strong>Customer Name:</strong> {{ invoice.user.name }}</p>
<p><strong>Date:</strong> {{ invoice.human_created_timestamp }}</p>
<p><strong>Total:</strong> ${{ '%0.2f' % invoice.total }}</p>
<p><strong>Shipping/Pickup:</strong> {{ invoice.handling_option }}</p>
<p><strong>Handling Cost:</strong> ${{ '%0.2f' % ((invoice.handling_cost_in_cents or 0) / 100) }}</p>
{% if invoice.delivery_address %}
<p><strong>Delivery Address:</strong> {{ invoice.delivery_address }}</p>
{% endif %}
<h2>Line Items</h2>
<table>
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in invoice.line_items %}
<tr">
<td><a href="/p/{{ item.product.id }}">{{ item.product.title }}</a></td>
<td>{{ item.quantity }}</td>
<td>${{ '%0.2f' % (item.price.price_in_cents / 100) }}</td>
<td>${{ '%0.2f' % ((item.price.price_in_cents * item.quantity) / 100) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "base.j2" %}
{% block content %}
<h1>My Purchases</h1>
{% if invoices %}
<h2>My Invoices</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Total</th>
<th>Invoice</th>
</tr>
</thead>
<tbody>
{% for invoice in invoices %}
<tr>
<td>{{ invoice.human_created_timestamp }}</td>
<td>${{ '%0.2f' % invoice.total }}</td>
<td><a href="/i/{{ invoice.id }}">Invoice</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>You have no purchases yet.</p>
{% endif %}
<br>
<h2>My Products</h2>
<section class="serp">
{% if products %}
{% for product in products %}
<div class="serp-item">
{% if "thumbnail1" in product.extensions %}
<a href="{{ product.absolute_url(request) }}" rel="nofollow">
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="serp-thumbnail" />
</a>
{% endif %}
<b><a href="{{ product.absolute_url(request) }}" class="shop_theme_link_color">{{ product.title }}</a></b>
{% if product.is_sellable %}
<br />
<a href="{{ product.absolute_url(request) }}" rel="nofollow">${{ '{:,.2f}'.format(product.price) }}</a>
{% endif %}
<br>
<a href="{{ product.absolute_url(request) }}" rel="nofollow">{{ product.shop.name }}</a>
</div>
{% endfor %}
{% else %}
<p>You have no products yet.</p>
{% endif %}
</section>
{% endblock %}

View file

@ -2,13 +2,11 @@ from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from ..models.invoice import get_invoice_by_id
from . import (
user_required,
get_referer_or_home,
)
from . import user_required, get_referer_or_home
@view_config(route_name="view_invoice", renderer="invoice.j2")
@view_config(route_name="view_invoice2", renderer="invoice.j2")
@user_required()
def view_invoice(request):
invoice_id = request.matchdict.get("invoice_id")
@ -18,7 +16,7 @@ def view_invoice(request):
request.session.flash(("Invoice not found.", "error"))
return HTTPFound(get_referer_or_home(request))
# Check if the user is the owner of the invoice or a shop editor
# check if the user is the owner of the invoice or a shop editor.
if request.user != invoice.user and not request.user.can_edit_shop(invoice.shop):
request.session.flash(
("You do not have permission to view this invoice.", "error")
@ -27,5 +25,4 @@ def view_invoice(request):
return {
"invoice": invoice,
"line_items": invoice.line_items.all(),
}

View file

@ -1,8 +1,4 @@
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPForbidden
from ..models.invoice import get_invoice_by_id
from ..models.user import get_user_by_id
from ..models.shop import get_shop_by_id
from ..models.invoice import Invoice
from . import shop_editor_required
from sqlalchemy import desc
@ -14,17 +10,3 @@ def shop_sales(request):
shop = request.shop
invoices = shop.invoices.order_by(desc(Invoice.created_timestamp)).all()
return {"invoices": invoices, "shop": shop}
@view_config(route_name="shop_sales_details", renderer="sales_details.j2")
@shop_editor_required()
def shop_sales_details(request):
shop_id = request.matchdict["shop_id"]
invoice_id = request.matchdict["invoice_id"]
shop = get_shop_by_id(request.dbsession, shop_id)
invoice = get_invoice_by_id(request.dbsession, invoice_id)
if not invoice or invoice.shop != shop:
raise HTTPForbidden("Access denied.")
return {"invoice": invoice}

View file

@ -2,15 +2,11 @@ from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound
from . import (
user_required,
shop_is_ready_required,
)
from . import user_required, shop_is_ready_required
from ..models.user import (
is_user_name_available,
is_user_name_valid,
)
from ..models.user import is_user_name_available, is_user_name_valid
from ..models.invoice import Invoice
@view_config(route_name="user_shops", renderer="shops.j2")
@ -21,14 +17,24 @@ def user_shops(request):
}
@view_config(route_name="user_purchases", renderer="products.j2")
@view_config(route_name="user_purchases", renderer="user_purchases.j2")
@user_required()
@shop_is_ready_required()
def user_products(request):
# only display products for the request's shop.
products = [p for p in request.user.products if p.shop == request.shop]
def user_purchases(request):
user = request.user
shop = request.shop # Get the current shop from the request
products = [p for p in request.user.products if p.shop == shop]
invoices = (
user.invoices.filter(Invoice.shop == shop)
.order_by(Invoice.created_timestamp.desc())
.all()
)
return {
"products": products,
"invoices": invoices,
"the_title": "My Purchases",
}