shop sales pages
modified: Makefile modified: development.ini modified: make_post_sell/models/invoice.py modified: make_post_sell/models/shop.py modified: make_post_sell/routes.py modified: make_post_sell/static/css/common.css modified: make_post_sell/templates/actions_view.j2 new file: make_post_sell/templates/sales.j2 new file: make_post_sell/templates/sales_details.j2 modified: make_post_sell/views/cart.py new file: make_post_sell/views/sales.py
This commit is contained in:
parent
17a700799d
commit
2c78ba4317
11 changed files with 184 additions and 90 deletions
3
Makefile
3
Makefile
|
|
@ -32,4 +32,5 @@ serve: dev
|
|||
|
||||
smtp: env
|
||||
# In the third shell, run a "mock" SMTP server to catch log in emails:
|
||||
sudo env/bin/python -m smtpd -n -c DebuggingServer localhost:25
|
||||
#sudo env/bin/python -m smtpd -n -c DebuggingServer localhost:25
|
||||
sudo env/bin/python -m aiosmtpd -l 127.0.0.1:8025
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ app.root_domain = blanka.foxhop.net
|
|||
app.root_domain_owner_email = russell.ballestrini@gmail.com
|
||||
app.root_url = http://blanka.foxhop.net:6501
|
||||
|
||||
#app.email.relay = localhost:8025
|
||||
|
||||
# TODO: these values should be moved to environment variables & we should create an
|
||||
# example app.env environment file.
|
||||
app.bucket.secure_uploads = plan-period-files
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class Invoice(RBase, Base):
|
|||
handling_option = Column(Unicode(64), nullable=True)
|
||||
handling_cost_in_cents = Column(BigInteger, nullable=True, default=0)
|
||||
|
||||
# the user's address to ship the item to.
|
||||
# denormalized address of user for shipping products.
|
||||
delivery_address = Column(UnicodeText, nullable=True)
|
||||
|
||||
# one to one.
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ class Shop(RBase, Base):
|
|||
order_by="desc(Coupon.created_timestamp)",
|
||||
)
|
||||
|
||||
# lazy='dynamic' returns a query object instead of collection.
|
||||
# Reference: http://docs.sqlalchemy.org/en/latest/orm/collections.html
|
||||
invoices = relationship("Invoice", back_populates="shop", lazy="dynamic")
|
||||
|
||||
# lazy='dynamic' returns a query object instead of collection.
|
||||
# Reference: http://docs.sqlalchemy.org/en/latest/orm/collections.html
|
||||
shop_locations = relationship(
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ def includeme(config):
|
|||
config.add_route("user_cart_public", "/u/cart/{cart_id}/public")
|
||||
config.add_route("user_cart_unpublic", "/u/cart/{cart_id}/unpublic")
|
||||
|
||||
|
||||
# cart routes.
|
||||
config.add_route("cart", "/cart")
|
||||
config.add_route("cart_checkout", "/cart/checkout")
|
||||
|
|
@ -89,6 +88,9 @@ def includeme(config):
|
|||
config.add_route("shop", "/s/{shop_id}")
|
||||
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")
|
||||
|
||||
config.add_route("shop_users", "/s/{shop_id}/users")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ h1 {
|
|||
margin-bottom: .4em;
|
||||
}
|
||||
|
||||
tr, th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@
|
|||
<br/>
|
||||
<a href="/s/{{ request.shop.id }}/users" class="product-edit-button mps-button">👤   Shop Users</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/s/{{ request.shop.id }}/sales" class="product-edit-button mps-button">💰   Shop Sales</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/s/{{ request.shop.id }}/settings" class="product-edit-button mps-button">⚙   Shop Settings</a>
|
||||
|
|
|
|||
28
make_post_sell/templates/sales.j2
Normal file
28
make_post_sell/templates/sales.j2
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Sales for {{ shop.name }}</h1>
|
||||
|
||||
<table style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice ID</th>
|
||||
<th>Customer Name</th>
|
||||
<th>Date</th>
|
||||
<th>Total</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invoice in invoices %}
|
||||
<tr>
|
||||
<td>{{ invoice.id }}</td>
|
||||
<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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
35
make_post_sell/templates/sales_details.j2
Normal file
35
make_post_sell/templates/sales_details.j2
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.j2" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Invoice: {{ 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 %}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
from pyramid.view import view_config
|
||||
|
||||
from transaction import TransactionManager
|
||||
|
||||
from . import (
|
||||
user_required,
|
||||
get_referer_or_home,
|
||||
|
|
@ -457,7 +459,6 @@ def cart_checkout(request):
|
|||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
@view_config(route_name="user_cart_complete_checkout")
|
||||
@user_required()
|
||||
@shop_is_ready_required()
|
||||
|
|
@ -486,99 +487,82 @@ def cart_complete_checkout(request):
|
|||
request.session.flash((error_message, "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
invoices = []
|
||||
successful_checkout = True
|
||||
tm = TransactionManager()
|
||||
|
||||
for shop_id, product_quantity_tuple in cart.shop_product_dict.items():
|
||||
shop = cart.shops[shop_id]
|
||||
invoice = Invoice(user=request.user)
|
||||
invoice.shop = shop
|
||||
invoice.shop_id = shop.id
|
||||
invoice.handling_option = cart.handling_option
|
||||
invoice.handling_cost_in_cents = cart.handling_cost_in_cents
|
||||
try:
|
||||
tm.begin()
|
||||
invoices = []
|
||||
successful_checkout = True
|
||||
|
||||
# Copy the user's active address to the invoice
|
||||
if cart.physical_products:
|
||||
invoice.delivery_address = request.user.active_address.data
|
||||
for shop_id, product_quantity_tuple in cart.shop_product_dict.items():
|
||||
shop = cart.shops[shop_id]
|
||||
invoice = Invoice(request.user)
|
||||
invoice.shop = shop
|
||||
invoice.shop_id = shop.id
|
||||
invoice.handling_option = cart.handling_option
|
||||
invoice.handling_cost_in_cents = cart.handling_cost_in_cents
|
||||
|
||||
for product, quantity in product_quantity_tuple:
|
||||
product.unlock_for_user(request.user)
|
||||
request.dbsession.add(product)
|
||||
invoice.new_line_item(
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
)
|
||||
if cart.physical_products:
|
||||
invoice.delivery_address = request.user.active_address.data
|
||||
|
||||
for coupon in cart.coupons:
|
||||
invoice.new_coupon_redemption(coupon)
|
||||
|
||||
request.dbsession.add(invoice)
|
||||
invoices.append(invoice)
|
||||
|
||||
# Loop over each invoice to handle payments
|
||||
for invoice in invoices:
|
||||
shop = invoice.shop
|
||||
|
||||
# only create a real charge if the invoice has a positive price.
|
||||
if invoice.requires_payment:
|
||||
stripe_user_shop = shop.stripe_user_shop(request.user)
|
||||
|
||||
try:
|
||||
shop.stripe.PaymentIntent.create(
|
||||
amount=invoice.total_in_cents,
|
||||
currency="usd",
|
||||
customer=stripe_user_shop.cus_id,
|
||||
payment_method=stripe_user_shop.active_card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
for product, quantity in product_quantity_tuple:
|
||||
product.unlock_for_user(request.user)
|
||||
request.dbsession.add(product)
|
||||
invoice.new_line_item(
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
)
|
||||
except shop.stripe.error.CardError as e:
|
||||
# rollback changes to database because charge was NOT successful!
|
||||
request.dbsession.rollback()
|
||||
|
||||
err = e.error
|
||||
# Error code will be authentication_required if authentication is needed
|
||||
payment_intent_id = err.payment_intent["id"]
|
||||
payment_intent = shop.stripe.PaymentIntent.retrieve(
|
||||
payment_intent_id
|
||||
)
|
||||
request.flash(
|
||||
"Payment Error: {} {} - {}".format(
|
||||
payment_intent.last_payment_error.code,
|
||||
payment_intent.last_payment_error.decline_code,
|
||||
payment_intent.last_payment_error.message,
|
||||
),
|
||||
"error",
|
||||
)
|
||||
successful_checkout = False
|
||||
break
|
||||
for coupon in cart.coupons:
|
||||
invoice.new_coupon_redemption(coupon)
|
||||
|
||||
if successful_checkout:
|
||||
# Update inventory for physical products
|
||||
cart.update_inventory(request.shop_location)
|
||||
request.dbsession.add(invoice)
|
||||
invoices.append(invoice)
|
||||
|
||||
# flush changes to database because charge was successful!
|
||||
request.dbsession.flush()
|
||||
msg = ("Success, you have completed the purchase!", "success")
|
||||
request.session.flash(msg)
|
||||
# email the authenticated user regarding their purchase.
|
||||
for invoice in invoices:
|
||||
send_purchase_email(
|
||||
request,
|
||||
request.user.email,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
# email the shop owners regarding their sale.
|
||||
send_sale_email(
|
||||
request,
|
||||
invoice.shop,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
# save the cart, so that we end up with a new empty cart.
|
||||
save_cart(request)
|
||||
return HTTPFound("/u/purchases")
|
||||
shop = invoice.shop
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
if invoice.requires_payment:
|
||||
stripe_user_shop = shop.stripe_user_shop(request.user)
|
||||
try:
|
||||
shop.stripe.PaymentIntent.create(
|
||||
amount=invoice.total_in_cents,
|
||||
currency="usd",
|
||||
customer=stripe_user_shop.cus_id,
|
||||
payment_method=stripe_user_shop.active_card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
)
|
||||
except shop.stripe.error.CardError as e:
|
||||
successful_checkout = False
|
||||
raise
|
||||
|
||||
if successful_checkout:
|
||||
tm.commit()
|
||||
cart.update_inventory(request.shop_location)
|
||||
msg = ("Success, you have completed the purchase!", "success")
|
||||
request.session.flash(msg)
|
||||
for invoice in invoices:
|
||||
send_purchase_email(
|
||||
request,
|
||||
request.user.email,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
send_sale_email(
|
||||
request,
|
||||
invoice.shop,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
save_cart(request)
|
||||
return HTTPFound("/u/purchases")
|
||||
else:
|
||||
tm.abort()
|
||||
msg = ("Payment failed. Please check your card details.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
except Exception as e:
|
||||
request.session.flash(("Payment failed: Please check your card details. {}".format(str(e)), "error"))
|
||||
return HTTPFound("/billing")
|
||||
|
|
|
|||
30
make_post_sell/views/sales.py
Normal file
30
make_post_sell/views/sales.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
|
||||
|
||||
@view_config(route_name="shop_sales", renderer="sales.j2")
|
||||
@shop_editor_required()
|
||||
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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue