Add coupon functionality

This commit is contained in:
Nathan Chapman 2022-04-01 11:11:50 -06:00
parent 194fb8d655
commit 775df2501a
43 changed files with 714 additions and 124 deletions

View File

@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-03-23 21:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0003_trackingnumber_created_at_trackingnumber_updated_at'),
]
operations = [
migrations.AddField(
model_name='order',
name='coupon',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.coupon'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.0.2 on 2022-03-28 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_order_coupon'),
]
operations = [
migrations.AlterModelOptions(
name='product',
options={'ordering': ['sorting', 'name']},
),
migrations.AddField(
model_name='product',
name='sorting',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -49,6 +49,7 @@ class Product(models.Model):
) )
visible_in_listings = models.BooleanField(default=False) visible_in_listings = models.BooleanField(default=False)
sorting = models.PositiveIntegerField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -61,6 +62,9 @@ class Product(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.pk}) return reverse('dashboard:product-detail', kwargs={'pk': self.pk})
class Meta:
ordering = ['sorting', 'name']
class ProductPhoto(models.Model): class ProductPhoto(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE)
@ -107,9 +111,12 @@ class Coupon(models.Model):
@property @property
def is_valid(self): def is_valid(self):
today = timezone.localtime(timezone.now()).date() today = timezone.localtime(timezone.now())
return True if today >= self.valid_from and today <= self.valid_to else False return True if today >= self.valid_from and today <= self.valid_to else False
def get_absolute_url(self):
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
@ -175,6 +182,13 @@ class Order(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
coupon = models.ForeignKey(
Coupon,
related_name='orders',
on_delete=models.SET_NULL,
null=True
)
total_net_amount = models.DecimalField( total_net_amount = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
@ -196,6 +210,17 @@ class Order(models.Model):
def get_total_quantity(self): def get_total_quantity(self):
return sum([line.quantity for line in self]) return sum([line.quantity for line in self])
def get_discount(self):
if self.coupon:
if self.coupon.discount_value_type == DiscountValueType.FIXED:
return self.coupon.discount_value
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount
return Decimal('0')
def get_total_price_after_discount(self):
return round(self.total_net_amount - self.get_discount(), 2)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.pk})

View File

@ -1,10 +1,33 @@
import logging import logging
from django import forms from django import forms
from core.models import Order, OrderLine, ShippingMethod, TrackingNumber from core.models import Order, OrderLine, ShippingMethod, TrackingNumber, Coupon, ProductPhoto
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CouponForm(forms.ModelForm):
class Meta:
model = Coupon
fields = (
'type',
'name',
'code',
'valid_from',
'valid_to',
'discount_value_type',
'discount_value',
'products',
)
widgets = {
'valid_from': forms.DateInput(attrs = {
'type': 'date'
}),
'valid_to': forms.DateInput(attrs = {
'type': 'date'
}),
}
class OrderLineFulfillForm(forms.ModelForm): class OrderLineFulfillForm(forms.ModelForm):
# send_shipment_details_to_customer = forms.BooleanField(initial=True) # send_shipment_details_to_customer = forms.BooleanField(initial=True)
@ -38,3 +61,9 @@ OrderTrackingFormset = forms.inlineformset_factory(
Order, TrackingNumber, form=OrderTrackingForm, Order, TrackingNumber, form=OrderTrackingForm,
extra=1, can_delete=False extra=1, can_delete=False
) )
class ProductPhotoForm(forms.ModelForm):
class Meta:
model = ProductPhoto
fields = ('image',)

View File

@ -9,7 +9,7 @@
</header> </header>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header panel__header--flex">
<h4>Shipping methods</h4> <h4>Shipping methods</h4>
<a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a> <a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a>
</div> </div>
@ -25,7 +25,7 @@
</section> </section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header panel__header--flex">
<h4>Staff</h4> <h4>Staff</h4>
<a href="" class="action-button order__fulfill">+ New staff</a> <a href="" class="action-button order__fulfill">+ New staff</a>
</div> </div>

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/coupon.png' %}" alt=""> Coupon</h1>
</header>
<section class="coupon__detail object__panel">
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:coupon-detail' coupon.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,18 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Create coupon</h1>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:coupon-create' %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Create coupon"> or <a href="{% url 'dashboard:coupon-list' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/coupon.png' %}" alt=""> {{ coupon.name }}</h1>
<div class="object__menu">
<a href="" class="action-button action-button--warning">Delete</a>
<a href="" class="action-button">Edit</a>
</div>
</header>
<section class="product__detail object__panel">
<div>
<p>{{ coupon.get_type_display }}</p>
<p>{{ coupon.code }}</p>
<p>{{ coupon.valid_from }}</p>
<p>{{ coupon.valid_to }}</p>
<p>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</p>
</div>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,18 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Update Coupon</h1>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:coupon-update' coupon.pk %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:coupon-detail' coupon.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/coupon.png' %}" alt=""> Coupons</h1>
<div class="object__menu">
<a href="{% url 'dashboard:coupon-create' %}" class="action-button order__fulfill">+ New coupon</a>
</div>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col5" href="coupon-detail">
<span>Name</span>
<span>Code</span>
<span>Starts</span>
<span>Ends</span>
<span>Value</span>
</div>
{% for coupon in coupon_list %}
<a class="object__item object__item--link object__item--col5" href="{% url 'dashboard:coupon-detail' coupon.pk %}">
<span>{{ coupon.name }}</span>
<span>{{ coupon.code }}</span>
<span>{{ coupon.valid_from|date:"SHORT_DATE_FORMAT" }}</span>
<span>{{ coupon.valid_to|date:"SHORT_DATE_FORMAT" }}</span>
<span>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</span>
</a>
{% empty %}
<span class="object__item">No coupons</span>
{% endfor %}
</section>
</article>
{% endblock content %}

View File

@ -5,10 +5,12 @@
<article> <article>
<header class="object__header"> <header class="object__header">
<h1><img src="{% static "images/customers.png" %}" alt=""> Customer: {{customer.get_full_name}}</h1> <h1><img src="{% static "images/customers.png" %}" alt=""> Customer: {{customer.get_full_name}}</h1>
<div class="object__menu">
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a> <a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a>
</div>
</header> </header>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header">
<h4>Info</h4> <h4>Info</h4>
</div> </div>
<div class="panel__item"> <div class="panel__item">
@ -52,14 +54,14 @@
</section> </section>
{% with order_list=customer.orders.all %} {% with order_list=customer.orders.all %}
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header" href="order-detail"> <div class="object__item panel__header object__item--col4" href="order-detail">
<span>Order #</span> <span>Order #</span>
<span>Date</span> <span>Date</span>
<span>Status</span> <span>Status</span>
<span>Total</span> <span>Total</span>
</div> </div>
{% for order in order_list %} {% for order in order_list %}
<a class="object__item" href="{% url 'dashboard:order-detail' order.pk %}"> <a class="object__item object__item--col4 object__item--link" href="{% url 'dashboard:order-detail' order.pk %}">
<span>#{{order.pk}}</span> <span>#{{order.pk}}</span>
<span>{{order.created_at|date:"D, M j Y"}}</span> <span>{{order.created_at|date:"D, M j Y"}}</span>
<span class="order__status--display"> <span class="order__status--display">

View File

@ -2,9 +2,11 @@
{% block content %} {% block content %}
<article class="product"> <article class="product">
<header class="object__header">
<h1>Update Customer</h1> <h1>Update Customer</h1>
<section> </header>
<form method="POST" action="{% url 'dashboard:customer-update' customer.pk %}"> <section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
{% csrf_token %} {% csrf_token %}
{{form.as_p}} {{form.as_p}}
<p class="form__submit"> <p class="form__submit">

View File

@ -3,15 +3,17 @@
{% block content %} {% block content %}
<article> <article>
<header class="object__header">
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1> <h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
</header>
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header" href="customer-detail"> <div class="object__item panel__header object__item--col3" href="customer-detail">
<span>Name</span> <span>Name</span>
<span>Email</span> <span>Email</span>
<span>Orders</span> <span>Orders</span>
</div> </div>
{% for customer in user_list %} {% for customer in user_list %}
<a class="object__item" href="{% url 'dashboard:customer-detail' customer.pk %}"> <a class="object__item object__item--link object__item--col3" href="{% url 'dashboard:customer-detail' customer.pk %}">
<span>{{customer.get_full_name}}</span> <span>{{customer.get_full_name}}</span>
<span>{{customer.email}}</span> <span>{{customer.email}}</span>
<span>{{customer.num_orders}}</span> <span>{{customer.num_orders}}</span>

View File

@ -17,7 +17,7 @@
</div> </div>
</header> </header>
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header"> <div class="object__item panel__header object__item--col5">
<span>Product</span> <span>Product</span>
<span>SKU</span> <span>SKU</span>
<span>Quantity</span> <span>Quantity</span>
@ -25,7 +25,7 @@
<span>Total</span> <span>Total</span>
</div> </div>
{% for item in order.lines.all %} {% for item in order.lines.all %}
<div class="object__item"> <div class="object__item object__item--col5">
{% with product=item.product %} {% with product=item.product %}
<figure class="item__figure"> <figure class="item__figure">
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}"> <img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
@ -40,13 +40,13 @@
{% empty %} {% empty %}
<p>No items in order yet.</p> <p>No items in order yet.</p>
{% endfor %} {% endfor %}
<div class="object__item"> <div class="object__item object__item--col5">
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill &rarr;</a> <a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill &rarr;</a>
</div> </div>
</section> </section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header object__item--col5">
<h4>Shipping</h4> <h4>Shipping</h4>
<a href="{% url 'dashboard:order-ship' order.pk %}" class="action-button order__fulfill">Ship order &rarr;</a> <a href="{% url 'dashboard:order-ship' order.pk %}" class="action-button order__fulfill">Ship order &rarr;</a>
</div> </div>
@ -66,7 +66,7 @@
</section> </section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header">
<h4>Customer</h4> <h4>Customer</h4>
</div> </div>
{% with customer=order.customer %} {% with customer=order.customer %}
@ -98,7 +98,22 @@
</section> </section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header">
<h4>Payment</h4>
</div>
<div class="panel__item">
<p>
<span>Subtotal: {{order.total_net_amount}}</span><br>
{% if order.coupon %}
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %}
<span>Total: {{order.get_total_price_after_discount}}</span>
</p>
</div>
</section>
<section class="object__panel">
<div class="object__item panel__header">
<h4>Transaction</h4> <h4>Transaction</h4>
</div> </div>
<div class="panel__item"> <div class="panel__item">

View File

@ -16,14 +16,14 @@
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<div class="object__item object__item--header"> <div class="object__item panel__header object__item--col4">
<span>Product</span> <span>Product</span>
<span>SKU</span> <span>SKU</span>
<span>Quantity to fulfill</span> <span>Quantity to fulfill</span>
<span>Grind</span> <span>Grind</span>
</div> </div>
{% for form in form %} {% for form in form %}
<div class="object__item"> <div class="object__item object__item--col4">
{% with product=form.instance.product %} {% with product=form.instance.product %}
{{form.id}} {{form.id}}
<figure class="item__figure"> <figure class="item__figure">
@ -36,7 +36,7 @@
{% endwith %} {% endwith %}
</div> </div>
{% endfor %} {% endfor %}
<div class="object__item"> <div class="object__item object__item--col5">
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Fulfill"> <a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Fulfill">
</div> </div>
</section> </section>

View File

@ -7,7 +7,7 @@
<h1><img src="{% static "images/box.png" %}" alt=""> Orders</h1> <h1><img src="{% static "images/box.png" %}" alt=""> Orders</h1>
</header> </header>
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header" href="order-detail"> <div class="object__item panel__header object__item--col5" href="order-detail">
<span>Order #</span> <span>Order #</span>
<span>Date</span> <span>Date</span>
<span>Customer</span> <span>Customer</span>
@ -15,7 +15,7 @@
<span>Total</span> <span>Total</span>
</div> </div>
{% for order in order_list %} {% for order in order_list %}
<a class="object__item" href="{% url 'dashboard:order-detail' order.pk %}"> <a class="object__item object__item--link object__item--col5" href="{% url 'dashboard:order-detail' order.pk %}">
<span>#{{order.pk}}</span> <span>#{{order.pk}}</span>
<span>{{order.created_at|date:"D, M j Y"}}</span> <span>{{order.created_at|date:"D, M j Y"}}</span>
<span>{{order.customer.get_full_name}}</span> <span>{{order.customer.get_full_name}}</span>

View File

@ -2,8 +2,10 @@
{% block content %} {% block content %}
<article> <article>
<h1>Fulfill Order #{{order.pk}}</h1> <header class="object__header">
<section> <h1>Ship Order #{{order.pk}}</h1>
</header>
<section class="object__panel">
<form method="POST" action=""> <form method="POST" action="">
{% csrf_token %} {% csrf_token %}
{{ form.management_form }} {{ form.management_form }}
@ -16,18 +18,15 @@
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<div class="object__item object__item--header"> <div class="object__item panel__header panel__header--flex">
<span>Product</span> <span>Product</span>
<span>SKU</span>
<span>Quantity to fulfill</span>
<span>Grind</span>
</div> </div>
{% for formitem in form %} {% for formitem in form %}
<div class="object__item"> <div class="object__item">
{{formitem}} {{formitem}}
</div> </div>
{% endfor %} {% endfor %}
<div class="object__item"> <div class="object__item object__item--col5">
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Ship order and send tracking info to customer"> <a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Ship order and send tracking info to customer">
</div> </div>
</section> </section>

View File

@ -0,0 +1,18 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Add photo</h1>
</header>
<section class="object__panel">
<form class="panel__item" enctype="multipart/form-data" method="POST" action="{% url 'dashboard:prodphoto-create' product.pk %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Add photo"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -23,5 +23,24 @@
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p> <p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p>
</div> </div>
</section> </section>
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Photos</h4>
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="action-button order__fulfill">+ Upload new photo</a>
</div>
<div class="panel__item gallery">
{% for photo in product.productphoto_set.all %}
<figure class="gallery__item">
<img src="{{ photo.image.url }}" alt="">
<figcaption>
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="action-button action-button--warning" value="Delete photo">
</form>
</figcaption>
</figure>
{% endfor %}
</div>
</section>
</article> </article>
{% endblock content %} {% endblock content %}

View File

@ -8,14 +8,14 @@
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a> <a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
</header> </header>
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header"> <div class="object__item panel__header object__item--col4">
<span></span> <span></span>
<span>Name</span> <span>Name</span>
<span>Visible</span> <span>Visible</span>
<span>Price</span> <span>Price</span>
</div> </div>
{% for product in product_list %} {% for product in product_list %}
<a class="object__item" href="{% url 'dashboard:product-detail' product.pk %}"> <a class="object__item object__item--link object__item--col4" href="{% url 'dashboard:product-detail' product.pk %}">
<figure class="product__figure"> <figure class="product__figure">
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}"> <img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
</figure> </figure>

View File

@ -10,6 +10,14 @@ urlpatterns = [
path('', views.ShippingMethodDetailView.as_view(), name='shipmeth-detail'), path('', views.ShippingMethodDetailView.as_view(), name='shipmeth-detail'),
])), ])),
path('coupons/', views.CouponListView.as_view(), name='coupon-list'),
path('coupons/new/', views.CouponCreateView.as_view(), name='coupon-create'),
path('coupons/<int:pk>/', include([
path('', views.CouponDetailView.as_view(), name='coupon-detail'),
path('update/', views.CouponUpdateView.as_view(), name='coupon-update'),
path('delete/', views.CouponDeleteView.as_view(), name='coupon-delete'),
])),
path('orders/', views.OrderListView.as_view(), name='order-list'), path('orders/', views.OrderListView.as_view(), name='order-list'),
path('orders/<int:pk>/', include([ path('orders/<int:pk>/', include([
path('', views.OrderDetailView.as_view(), name='order-detail'), path('', views.OrderDetailView.as_view(), name='order-detail'),
@ -25,6 +33,11 @@ urlpatterns = [
path('', views.ProductDetailView.as_view(), name='product-detail'), path('', views.ProductDetailView.as_view(), name='product-detail'),
path('update/', views.ProductUpdateView.as_view(), name='product-update'), path('update/', views.ProductUpdateView.as_view(), name='product-update'),
path('delete/', views.ProductDeleteView.as_view(), name='product-delete'), path('delete/', views.ProductDeleteView.as_view(), name='product-delete'),
path('photos/new/', views.ProductPhotoCreateView.as_view(), name='prodphoto-create'),
path('photos/<int:photo_pk>/', include([
path('delete/', views.ProductPhotoDeleteView.as_view(), name='prodphoto-delete'),
])),
])), ])),
path('customers/', views.CustomerListView.as_view(), name='customer-list'), path('customers/', views.CustomerListView.as_view(), name='customer-list'),

View File

@ -23,10 +23,19 @@ from django.db.models.functions import Coalesce
from accounts.models import User from accounts.models import User
from accounts.utils import get_or_create_customer from accounts.utils import get_or_create_customer
from accounts.forms import AddressForm from accounts.forms import AddressForm
from core.models import Product, Order, OrderLine, ShippingMethod, Transaction, TrackingNumber from core.models import (
Product,
ProductPhoto,
Order,
OrderLine,
ShippingMethod,
Transaction,
TrackingNumber,
Coupon
)
from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType
from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset, CouponForm, ProductPhotoForm
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -59,6 +68,8 @@ class DashboardConfigView(TemplateView):
return context return context
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ShippingMethod model = ShippingMethod
template_name = 'dashboard/shipmeth_create_form.html' template_name = 'dashboard/shipmeth_create_form.html'
@ -70,6 +81,35 @@ class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
template_name = 'dashboard/shipmeth_detail.html' template_name = 'dashboard/shipmeth_detail.html'
class CouponListView(LoginRequiredMixin, ListView):
model = Coupon
template_name = 'dashboard/coupon_list.html'
class CouponCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Coupon
template_name = 'dashboard/coupon_create_form.html'
form_class = CouponForm
success_message = '%(name)s created.'
class CouponDetailView(LoginRequiredMixin, DetailView):
model = Coupon
template_name = 'dashboard/coupon_detail.html'
class CouponUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Coupon
template_name = 'dashboard/coupon_form.html'
success_message = '%(name)s saved.'
form_class = CouponForm
class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Coupon
template_name = 'dashboard/coupon_confirm_delete.html'
success_url = reverse_lazy('dashboard:coupon-list')
success_message = 'Coupon deleted.'
class OrderListView(LoginRequiredMixin, ListView): class OrderListView(LoginRequiredMixin, ListView):
model = Order model = Order
template_name = 'dashboard/order_list.html' template_name = 'dashboard/order_list.html'
@ -147,6 +187,7 @@ class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
class ProductListView(LoginRequiredMixin, ListView): class ProductListView(LoginRequiredMixin, ListView):
model = Product model = Product
template_name = 'dashboard/product_list.html' template_name = 'dashboard/product_list.html'
ordering = 'sorting'
# def get_queryset(self): # def get_queryset(self):
# object_list = Product.objects.filter( # object_list = Product.objects.filter(
@ -164,19 +205,52 @@ class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Product model = Product
template_name = 'dashboard/product_update_form.html' template_name = 'dashboard/product_update_form.html'
fields = '__all__' fields = '__all__'
success_message = "%(name)s saved." success_message = '%(name)s saved.'
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Product
template_name = 'dashboard/product_create_form.html'
fields = '__all__'
success_message = '%(name)s created.'
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Product model = Product
template_name = 'dashboard/product_confirm_delete.html' template_name = 'dashboard/product_confirm_delete.html'
success_url = reverse_lazy('dashboard:product-list') success_url = reverse_lazy('dashboard:product-list')
success_message = "Product deleted." success_message = 'Product deleted.'
class ProductPhotoCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ProductPhoto
pk_url_kwarg = 'photo_pk'
template_name = 'dashboard/prodphoto_create_form.html'
form_class = ProductPhotoForm
success_message = 'Photo added.'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['product'] = Product.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
form.instance.product = Product.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = ProductPhoto
pk_url_kwarg = 'photo_pk'
template_name = 'dashboard/prodphoto_confirm_delete.html'
success_message = 'Photo deleted.'
def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Product
template_name = "dashboard/product_create_form.html"
fields = '__all__'
success_message = "%(name)s created."
class CustomerListView(LoginRequiredMixin, ListView): class CustomerListView(LoginRequiredMixin, ListView):

View File

@ -28,6 +28,7 @@ ANYMAIL_CONFIG = {
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', '') SERVER_EMAIL = os.environ.get('SERVER_EMAIL', '')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', '') DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', '')
DEFAULT_CONTACT_EMAIL = os.environ.get('DEFAULT_CONTACT_EMAIL', '')
SECURE_HSTS_SECONDS = os.environ.get('SECURE_HSTS_SECONDS', 3600) SECURE_HSTS_SECONDS = os.environ.get('SECURE_HSTS_SECONDS', 3600)
SECURE_SSL_REDIRECT = os.environ.get('SECURE_SSL_REDIRECT', 'False') == 'True' SECURE_SSL_REDIRECT = os.environ.get('SECURE_SSL_REDIRECT', 'False') == 'True'

View File

@ -0,0 +1,14 @@
const form = document.querySelector('form')
const purchaseTypeInput = form.querySelector('[name=purchase_type]')
const scheduleInput = form.querySelector('[name=schedule]')
scheduleInput.parentElement.style.display = 'none'
purchaseTypeInput.addEventListener('change', event => {
if (event.target.value === 'Subscribe') {
scheduleInput.parentElement.style.display = 'block'
} else if (event.target.value === 'One-time purchase') {
scheduleInput.parentElement.style.display = 'none'
}
})

View File

@ -72,6 +72,7 @@ label {
input[type=text], input[type=text],
input[type=email], input[type=email],
input[type=number], input[type=number],
input[type=date],
input[type=password], input[type=password],
select[multiple=multiple], select[multiple=multiple],
textarea { textarea {
@ -145,7 +146,7 @@ button:hover {
} }
.action-button--warning { .action-button--warning {
background-color: var(--red-color); background-color: var(--red-color) !important;
} }
.action-link { .action-link {
@ -314,12 +315,33 @@ main article {
.object__item { .object__item {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
border-bottom: 0.05rem solid var(--gray-color); border-bottom: 0.05rem solid var(--gray-color);
text-decoration: none; text-decoration: none;
align-items: center; align-items: center;
justify-items: start;
}
.object__item--col3 {
grid-template-columns: repeat(3, 1fr);
}
.object__item--col5 {
grid-template-columns: repeat(5, 1fr);
}
.object__item--col4 {
grid-template-columns: repeat(4, 1fr);
}
.object__item--col8 {
grid-template-columns: repeat(8, 1fr);
}
.panel__header--flex {
display: flex;
justify-content: space-between;
} }
.panel__item:last-child, .panel__item:last-child,
@ -328,11 +350,11 @@ main article {
border-radius: 0 0 0.5rem 0.5rem; border-radius: 0 0 0.5rem 0.5rem;
} }
.object__item:hover { .object__item--link:hover {
background-color: var(--bg-alt-color); background-color: var(--bg-alt-color);
} }
.object__item--header { .panel__header {
font-weight: bold; font-weight: bold;
background-color: var(--bg-alt-color); background-color: var(--bg-alt-color);
border-radius: 0.5rem 0.5rem 0 0; border-radius: 0.5rem 0.5rem 0 0;
@ -497,3 +519,24 @@ main article {
height: 50px; height: 50px;
margin-right: 1rem; margin-right: 1rem;
} }
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 3rem;
}
.gallery__item {
}
.gallery__item,
.gallery__item img {
width: 100%;
}
.gallery__item img {
border: var(--default-border);
object-fit: cover;
aspect-ratio: 1/1;
}

View File

@ -63,9 +63,6 @@ small, .text_small {
font-size: 0.833rem; font-size: 0.833rem;
} }
label {
display: block;
}
@ -82,6 +79,7 @@ textarea {
font: inherit; font: inherit;
margin: 0; margin: 0;
max-width: 100%; max-width: 100%;
box-sizing: border-box;
} }
input { input {
@ -91,6 +89,7 @@ input {
label { label {
display: block;
text-align: left; text-align: left;
font-weight: 700; font-weight: 700;
} }
@ -105,6 +104,7 @@ select {
input[type=text], input[type=text],
input[type=email], input[type=email],
input[type=number], input[type=number],
input[type=date],
input[type=password], input[type=password],
select[multiple=multiple], select[multiple=multiple],
textarea { textarea {
@ -116,19 +116,27 @@ textarea {
outline: 0; outline: 0;
} }
input:focus, input:focus,
textarea:focus { textarea:focus {
border-color: var(--yellow-color); border-color: var(--yellow-color);
} }
select[multiple=multiple] { select[multiple=multiple] {
height: 125px; height: 125px;
} }
input[type=radio],
input[type=checkbox] { input[type=checkbox] {
width: 1em; width: 2rem;
vertical-align: text-top; height: 2rem;
vertical-align: middle;
}
input[type=radio] + label,
input[type=checkbox] + label {
display: inline-block;
margin: 1rem 0;
} }
textarea { textarea {
@ -296,6 +304,11 @@ nav {
justify-content: flex-end; justify-content: flex-end;
} }
.coupon__form {
max-width: 16rem;
align-self: flex-end;
}
.item__figure img { .item__figure img {
vertical-align: middle; vertical-align: middle;
} }
@ -325,7 +338,17 @@ nav {
.order__shipping { .order__shipping {
}
.shipping__details {
margin-bottom: 3rem; margin-bottom: 3rem;
max-width: 32rem;
}
.shipping__details input[type=submit] {
font-size: 1.25rem;
} }
@ -334,10 +357,6 @@ nav {
text-align: right; text-align: right;
} }
.order__details {
/*margin: 3rem 0;*/
}
footer { footer {
margin: 4rem 0 0; margin: 4rem 0 0;
@ -386,7 +405,7 @@ footer {
background-color: var(--bg-alt-color); background-color: var(--bg-alt-color);
} }
.object__item--header { .panel__header {
font-weight: bold; font-weight: bold;
background-color: var(--bg-alt-color); background-color: var(--bg-alt-color);
border-radius: 0.5rem 0.5rem 0 0; border-radius: 0.5rem 0.5rem 0 0;
@ -415,10 +434,11 @@ footer {
._form_1 div { ._form_1 {
text-align: left !important; margin: 0;
} }
._form_1 div form { ._form_1 div form {
margin: 1rem 0 !important; margin: 1rem 0 !important;
padding: 0 !important; padding: 0 !important;
} }

View File

@ -1,14 +1,23 @@
import logging import logging
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from core.models import Product, OrderLine from core.models import Product, OrderLine, Coupon
from .payments import CreateOrder from .payments import CreateOrder
from core import (
DiscountValueType,
VoucherType,
TransactionStatus,
OrderStatus,
ShippingMethodType
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Cart: class Cart:
def __init__(self, request): def __init__(self, request):
self.session = request.session self.session = request.session
self.coupon_code = self.session.get('coupon_code')
cart = self.session.get(settings.CART_SESSION_ID) cart = self.session.get(settings.CART_SESSION_ID)
if not cart: if not cart:
cart = self.session[settings.CART_SESSION_ID] = {} cart = self.session[settings.CART_SESSION_ID] = {}
@ -62,14 +71,16 @@ class Cart:
def clear(self): def clear(self):
del self.session[settings.CART_SESSION_ID] del self.session[settings.CART_SESSION_ID]
del self.coupon_code
self.session.modified = True self.session.modified = True
def build_order_params(self): def build_order_params(self):
return \ return \
{ {
'items': self, 'items': self,
'total_price': f'{self.get_total_price()}', 'total_price': f'{self.get_total_price_after_discount()}',
'item_total': f'{self.get_total_price()}', 'item_total': f'{self.get_total_price()}',
'discount': f'{self.get_discount()}',
'shipping_price': '0', 'shipping_price': '0',
'tax_total': '0', 'tax_total': '0',
'shipping_method': 'US POSTAL SERVICE', 'shipping_method': 'US POSTAL SERVICE',
@ -105,16 +116,19 @@ class Cart:
'country_code': 'US' 'country_code': 'US'
} }
# @property @property
# def coupon(self): def coupon(self):
# if self.coupon_id: if self.coupon_code:
# return Coupon.objects.get(id=self.coupon_id) return Coupon.objects.get(code=self.coupon_code)
# return None return None
# def get_discount(self): def get_discount(self):
# if self.coupon: if self.coupon:
# return (self.coupon.discount / Decimal('100')) * self.get_total_price() if self.coupon.discount_value_type == DiscountValueType.FIXED:
# return Decimal('0') return round(self.coupon.discount_value, 2)
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
return Decimal('0')
# def get_total_price_after_discount(self): def get_total_price_after_discount(self):
# return self.get_total_price() - self.get_discount() return round(self.get_total_price() - self.get_discount(), 2)

View File

@ -5,6 +5,8 @@ from django.core.mail import EmailMessage
from core.models import Order from core.models import Order
from accounts import STATE_CHOICES from accounts import STATE_CHOICES
from .tasks import contact_form_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form): class AddToCartForm(forms.Form):
@ -28,8 +30,27 @@ class AddToCartForm(forms.Form):
(PERCOLATOR, 'Percolator'), (PERCOLATOR, 'Percolator'),
(OTHER, 'Other (enter below)') (OTHER, 'Other (enter below)')
] ]
ONE_TIME = 'One-time purchase'
SUBSCRIBE = 'Subscribe'
PURCHASE_TYPE_CHOICES = [
(ONE_TIME, 'One-time purchase'),
(SUBSCRIBE, 'Subscribe and save 10%'),
]
SEVEN_DAYS = 7
FOURTEEN_DAYS = 14
THIRTY_DAYS = 30
SCHEDULE_CHOICES = [
(SEVEN_DAYS, 'Every 7 days'),
(FOURTEEN_DAYS, 'Every 14 days'),
(THIRTY_DAYS, 'Every 30 days'),
]
quantity = forms.IntegerField(min_value=1, initial=1) quantity = forms.IntegerField(min_value=1, initial=1)
roast = forms.ChoiceField(choices=ROAST_CHOICES) roast = forms.ChoiceField(choices=ROAST_CHOICES)
purchase_type = forms.ChoiceField(choices=PURCHASE_TYPE_CHOICES, initial=ONE_TIME)
schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
@ -53,8 +74,43 @@ class OrderCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Order model = Order
fields = ( fields = (
'coupon',
'total_net_amount', 'total_net_amount',
) )
widgets = { widgets = {
'coupon': forms.HiddenInput(),
'total_net_amount': forms.HiddenInput() 'total_net_amount': forms.HiddenInput()
} }
class CouponApplyForm(forms.Form):
code = forms.CharField(label='Enter coupon')
class ContactForm(forms.Form):
GOOGLE = 'Google Search'
SHOP = 'The coffee shop'
WOM = 'Word of mouth'
PRODUCT = 'Coffee Bag'
STORE = 'Store'
OTHER = 'Other'
REFERAL_CHOICES = [
(GOOGLE, 'Google Search'),
(SHOP, 'Better Living Through Coffee coffee shop'),
(WOM, 'Friend/Relative'),
(PRODUCT, 'Our Coffee Bag'),
(STORE, 'PT Food Coop/other store'),
(OTHER, 'Other (please describe in the Message section below'),
]
first_name = forms.CharField()
last_name = forms.CharField()
email_address = forms.EmailField()
referal = forms.ChoiceField(
label='How did you find our website?',
choices=REFERAL_CHOICES
)
subject = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
def send_email(self):
contact_form_email.delay(self.cleaned_data)

View File

@ -112,10 +112,10 @@ class CreateOrder(PayPalClient):
"currency_code": "USD", "currency_code": "USD",
"value": params['tax_total'] "value": params['tax_total']
}, },
# "shipping_discount": { "discount": {
# "currency_code": "USD", "currency_code": "USD",
# "value": "10" "value": params['discount']
# } }
} }
}, },
"items": processed_items, "items": processed_items,

View File

@ -1,26 +1,22 @@
# from celery import shared_task from celery import shared_task
# from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
# from django.conf import settings from django.conf import settings
# from django.core.mail import EmailMessage, send_mail from django.core.mail import EmailMessage, send_mail
# from templated_email import send_templated_mail from templated_email import send_templated_mail
# from core.models import Order logger = get_task_logger(__name__)
# logger = get_task_logger(__name__)
# CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation' COTACT_FORM_TEMPLATE = 'storefront/contact_form'
# ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
# ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
# @shared_task(name='send_order_confirmation_email') @shared_task(name='contact_form_email')
# def send_order_confirmation_email(order): def contact_form_email(formdata):
# send_templated_mail( send_templated_mail(
# template_name=CONFIRM_ORDER_TEMPLATE, template_name=COTACT_FORM_TEMPLATE,
# from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
# recipient_list=[order['email']], recipient_list=[settings.DEFAULT_CONTACT_EMAIL],
# context=order context=formdata
# ) )
# logger.info(f"Order confirmation email sent to {order['email']}") logger.info(f"Contact form email sent from {formdata['email_address']}")

View File

@ -28,7 +28,22 @@
{% endfor %} {% endfor %}
</section> </section>
<section> <section>
<p class="cart__total_price">Cart total: <strong>${{cart.get_total_price}}</strong></p> <div class="cart__total">
<form action="{% url 'storefront:coupon-apply' %}" method="post" class="coupon__form">
{% csrf_token %}
{{ coupon_apply_form.as_p }}
<p>
<input type="submit" value="Apply" class="action-button">
</p>
</form>
</div>
<p class="cart__total_price">
<span class="">Subtotal: ${{cart.get_total_price|floatformat:"2"}}</span><br>
{% if cart.coupon %}
<span class="">Coupon: {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</span><br>
{% endif %}
<span class="cart__total_price">Cart total: <strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></span>
</p>
<p class="cart__total"> <p class="cart__total">
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a>&emsp;or&emsp;<a class="action-button action-button--large" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a> <a href="{% url 'storefront:product-list' %}">Continue Shopping</a>&emsp;or&emsp;<a class="action-button action-button--large" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>
</p> </p>

View File

@ -4,8 +4,8 @@
{% block content %} {% block content %}
<article> <article>
<h2>Checkout</h2> <h2>Checkout</h2>
<section class="order__details"> <section class="order__shipping">
<div class="order__shipping"> <div class="shipping__details">
<h3>Shipping Address</h3> <h3>Shipping Address</h3>
<form action="" method="POST" class="address__form"> <form action="" method="POST" class="address__form">
{% csrf_token %} {% csrf_token %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Contact PT Coffee</h1>
<section>
<form action="{% url 'storefront:contact' %}" method="post">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Send message">
</form>
</section>
</article>
{% endblock %}

View File

@ -8,7 +8,7 @@
<a href="{% url 'storefront:customer-update' customer.pk %}" class="action-button">Edit profile</a> <a href="{% url 'storefront:customer-update' customer.pk %}" class="action-button">Edit profile</a>
</header> </header>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item panel__header">
<h4>Info</h4> <h4>Info</h4>
</div> </div>
<div class="panel__item"> <div class="panel__item">
@ -53,19 +53,15 @@
</section> </section>
{% with order_list=customer.orders.all %} {% with order_list=customer.orders.all %}
<section class="object__list"> <section class="object__list">
<div class="object__item object__item--header" href="order-detail"> <div class="object__item panel__header" href="order-detail">
<span>Order #</span> <span>Order #</span>
<span>Date</span> <span>Date</span>
<span>Status</span>
<span>Total</span> <span>Total</span>
</div> </div>
{% for order in order_list %} {% for order in order_list %}
<a class="object__item" href=""> <a class="object__item" href="">
<span>#{{order.pk}}</span> <span>#{{order.pk}}</span>
<span>{{order.created_at|date:"D, M j Y"}}</span> <span>{{order.created_at|date:"D, M j Y"}}</span>
<span class="order__status--display">
<div class="status__dot order__status--{{order.status}}"></div>
{{order.get_status_display}}</span>
<span>${{order.total_net_amount}}</span> <span>${{order.total_net_amount}}</span>
</a> </a>
{% empty %} {% empty %}

View File

@ -46,7 +46,13 @@
{{form.as_p}} {{form.as_p}}
{# <input type="submit" value="Place order"> #} {# <input type="submit" value="Place order"> #}
</form> </form>
<h4>Total: ${{cart.get_total_price}}</h4> <p class="cart__total_price">
<span class="">Subtotal: ${{cart.get_total_price|floatformat:"2"}}</span><br>
{% if cart.coupon %}
<span class="">Coupon: {{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</span><br>
{% endif %}
<span class="cart__total_price">Cart total: <strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></span>
</p>
<div id="paypal-button-container"></div> <div id="paypal-button-container"></div>
</div> </div>
</section> </section>

View File

@ -1,7 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block head %}
<script defer src="{% static 'scripts/product_form.js' %}"></script>
{% endblock %}
{% block content %} {% block content %}
<article class="product__item"> <article class="product__item">
<figure class="product__figure"> <figure class="product__figure">
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}"> <img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
@ -20,5 +24,4 @@
</form> </form>
</section> </section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -20,6 +20,3 @@
</article> </article>
{% endblock %} {% endblock %}
{% block footer %}
<div class="_form_1"></div><script src="https://bltc999.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
{% endblock footer %}

View File

@ -3,6 +3,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path('about/', views.AboutView.as_view(), name='about'), path('about/', views.AboutView.as_view(), name='about'),
path('contact/', views.ContactFormView.as_view(), name='contact'),
path('', views.ProductListView.as_view(), name='product-list'), path('', views.ProductListView.as_view(), name='product-list'),
path('products/<int:pk>/', include([ path('products/<int:pk>/', include([
@ -13,7 +14,10 @@ urlpatterns = [
path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'), path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'),
path('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'), path('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'),
path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'),
path('paypal/order/<slug:transaction_id>/capture/', views.paypal_order_transaction_capture, name='paypal-capture'), path('paypal/order/<slug:transaction_id>/capture/', views.paypal_order_transaction_capture, name='paypal-capture'),
path('paypal/webhooks/', views.paypal_webhook_endpoint, name='paypal-webhook'),
path('checkout/address/', views.CheckoutAddressView.as_view(), name='checkout-address'), path('checkout/address/', views.CheckoutAddressView.as_view(), name='checkout-address'),
path('checkout/', views.OrderCreateView.as_view(), name='order-create'), path('checkout/', views.OrderCreateView.as_view(), name='order-create'),

View File

@ -1,9 +1,12 @@
import logging import logging
import requests import requests
import json
from django.conf import settings from django.conf import settings
from django.utils import timezone
from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse from django.http import JsonResponse
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin
@ -11,16 +14,19 @@ from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from accounts.models import User, Address from accounts.models import User, Address
from accounts.utils import get_or_create_customer from accounts.utils import get_or_create_customer
from core.models import Product, Order, Transaction, OrderLine from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm from core.forms import ShippingMethodForm
from .forms import AddToCartForm, OrderCreateForm, AddressForm from .forms import AddToCartForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
from .cart import Cart from .cart import Cart
from .payments import CaptureOrder from .payments import CaptureOrder
@ -41,6 +47,7 @@ class CartView(TemplateView):
} }
) )
context['cart'] = cart context['cart'] = cart
context['coupon_apply_form'] = CouponApplyForm()
return context return context
class CartAddProductView(SingleObjectMixin, FormView): class CartAddProductView(SingleObjectMixin, FormView):
@ -75,10 +82,32 @@ def cart_remove_product_view(request, pk):
return redirect('storefront:cart-detail') return redirect('storefront:cart-detail')
class CouponApplyView(FormView):
template_name = 'contact.html'
form_class = CouponApplyForm
success_url = reverse_lazy('storefront:cart-detail')
def form_valid(self, form):
today = timezone.localtime(timezone.now()).date()
code = form.cleaned_data['code']
try:
coupon = Coupon.objects.get(
code__iexact=code,
valid_from__date__lte=today,
valid_to__date__gte=today
)
if coupon.is_valid:
self.request.session['coupon_code'] = coupon.code
except ObjectDoesNotExist:
self.request.session['coupon_code'] = None
return super().form_valid(form)
class ProductListView(FormMixin, ListView): class ProductListView(FormMixin, ListView):
model = Product model = Product
template_name = 'storefront/product_list.html' template_name = 'storefront/product_list.html'
form_class = AddToCartForm form_class = AddToCartForm
ordering = 'sorting'
queryset = Product.objects.filter( queryset = Product.objects.filter(
visible_in_listings=True visible_in_listings=True
@ -127,6 +156,7 @@ class OrderCreateView(CreateView):
def get_initial(self): def get_initial(self):
cart = Cart(self.request) cart = Cart(self.request)
initial = { initial = {
'coupon': cart.coupon,
'total_net_amount': cart.get_total_price() 'total_net_amount': cart.get_total_price()
} }
@ -182,6 +212,13 @@ def paypal_order_transaction_capture(request, transaction_id):
else: else:
return JsonResponse({'details': 'invalid request'}) return JsonResponse({'details': 'invalid request'})
@csrf_exempt
@require_POST
def paypal_webhook_endpoint(request):
data = json.loads(request.body)
logger.info(data)
return JsonResponse(data)
class PaymentDoneView(TemplateView): class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html' template_name = 'storefront/payment_done.html'
@ -211,3 +248,12 @@ class CustomerUpdateView(UpdateView):
class AboutView(TemplateView): class AboutView(TemplateView):
template_name = 'storefront/about.html' template_name = 'storefront/about.html'
class ContactFormView(FormView, SuccessMessageMixin):
template_name = 'storefront/contact_form.html'
form_class = ContactForm
success_url = reverse_lazy('storefront:product-list')
def form_valid(self, form):
form.send_email()
return super().form_valid(form)

View File

@ -16,10 +16,10 @@
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>
{% for emailaddress in user.emailaddress_set.all %} {% for emailaddress in user.emailaddress_set.all %}
<div class="ctrlHolder"> <div>
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}"> <label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
{{ emailaddress.email }} {{ emailaddress.email }}
{% if emailaddress.verified %} {% if emailaddress.verified %}

View File

@ -42,12 +42,12 @@
</div> </div>
<nav> <nav>
<a href="{% url 'storefront:product-list' %}">Shop</a> <a href="{% url 'storefront:product-list' %}">Shop</a>
<a href="">Wholesale</a> {# <a href="">Wholesale</a> #}
<a href="">Subscribe</a> <a href="">Subscribe</a>
<a href="">Cafe</a> <a href="">Cafe</a>
<a href="">Fair Trade</a> <a href="">Fair Trade</a>
<a href="{% url 'storefront:about' %}">About</a> <a href="{% url 'storefront:about' %}">About</a>
<a href="">Contact</a> <a href="{% url 'storefront:contact' %}">Contact</a>
<a class="site__cart" href="{% url 'storefront:cart-detail' %}"> <a class="site__cart" href="{% url 'storefront:cart-detail' %}">
<span class="cart__length">{{cart|length}}</span> <span class="cart__length">{{cart|length}}</span>
<img class="cart__icon" src="{% static 'images/shopping_cart.svg' %}" alt="Shopping cart"> <img class="cart__icon" src="{% static 'images/shopping_cart.svg' %}" alt="Shopping cart">
@ -71,8 +71,7 @@
<div> <div>
<h4>Problem with your order?<br>Have a question?</h4> <h4>Problem with your order?<br>Have a question?</h4>
<p>Please contact us, were happy to help you over the phone at <a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.</p> <p>Please contact us, were happy to help you over the phone at <a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.</p>
{% block footer %} <div class="_form_1"></div><script src="https://bltc999.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
{% endblock footer %}
</div> </div>
</div> </div>
<div class="site__copyright"> <div class="site__copyright">

View File

@ -41,7 +41,7 @@
<img src="{% static 'images/customer.png' %}" alt=""> <img src="{% static 'images/customer.png' %}" alt="">
Customers Customers
</a> </a>
<a href=""> <a href="{% url 'dashboard:coupon-list' %}">
<img src="{% static 'images/coupon.png' %}" alt=""> <img src="{% static 'images/coupon.png' %}" alt="">
Coupons Coupons
</a> </a>

View File

@ -0,0 +1,16 @@
{% block subject %}{{subject}}{% endblock %}
{% block plain %}
Referred from: {{referal}}
From: {{first_name}} {{last_name}} {{email_address}}
Message: {{message}}
{% endblock %}
{% block html %}
<p><strong>Referred from</strong>:<br>{{referal}}</p>
<p><strong>From</strong>:<br>{{first_name}} {{last_name}} {{email_address|urlize}}</p>
<p><strong>Message</strong>:<br>{{message|linebreaks}}</p>
{% endblock %}