Add coupon functionality
This commit is contained in:
parent
194fb8d655
commit
775df2501a
19
src/core/migrations/0004_order_coupon.py
Normal file
19
src/core/migrations/0004_order_coupon.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -49,6 +49,7 @@ class Product(models.Model):
|
||||
)
|
||||
|
||||
visible_in_listings = models.BooleanField(default=False)
|
||||
sorting = models.PositiveIntegerField(blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -61,6 +62,9 @@ class Product(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:product-detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
ordering = ['sorting', 'name']
|
||||
|
||||
|
||||
class ProductPhoto(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
@ -107,9 +111,12 @@ class Coupon(models.Model):
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
coupon = models.ForeignKey(
|
||||
Coupon,
|
||||
related_name='orders',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True
|
||||
)
|
||||
|
||||
total_net_amount = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
@ -196,6 +210,17 @@ class Order(models.Model):
|
||||
def get_total_quantity(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):
|
||||
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
@ -1,10 +1,33 @@
|
||||
import logging
|
||||
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__)
|
||||
|
||||
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):
|
||||
# send_shipment_details_to_customer = forms.BooleanField(initial=True)
|
||||
|
||||
@ -38,3 +61,9 @@ OrderTrackingFormset = forms.inlineformset_factory(
|
||||
Order, TrackingNumber, form=OrderTrackingForm,
|
||||
extra=1, can_delete=False
|
||||
)
|
||||
|
||||
|
||||
class ProductPhotoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ProductPhoto
|
||||
fields = ('image',)
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
</header>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<h4>Shipping methods</h4>
|
||||
<a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a>
|
||||
</div>
|
||||
@ -25,7 +25,7 @@
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<h4>Staff</h4>
|
||||
<a href="" class="action-button order__fulfill">+ New staff</a>
|
||||
</div>
|
||||
|
||||
19
src/dashboard/templates/dashboard/coupon_confirm_delete.html
Normal file
19
src/dashboard/templates/dashboard/coupon_confirm_delete.html
Normal 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 %}
|
||||
18
src/dashboard/templates/dashboard/coupon_create_form.html
Normal file
18
src/dashboard/templates/dashboard/coupon_create_form.html
Normal 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 %}
|
||||
23
src/dashboard/templates/dashboard/coupon_detail.html
Normal file
23
src/dashboard/templates/dashboard/coupon_detail.html
Normal 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 %}
|
||||
18
src/dashboard/templates/dashboard/coupon_form.html
Normal file
18
src/dashboard/templates/dashboard/coupon_form.html
Normal 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 %}
|
||||
33
src/dashboard/templates/dashboard/coupon_list.html
Normal file
33
src/dashboard/templates/dashboard/coupon_list.html
Normal 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 %}
|
||||
@ -5,10 +5,12 @@
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static "images/customers.png" %}" alt=""> Customer: {{customer.get_full_name}}</h1>
|
||||
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Info</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
@ -52,14 +54,14 @@
|
||||
</section>
|
||||
{% with order_list=customer.orders.all %}
|
||||
<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>Date</span>
|
||||
<span>Status</span>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% 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.created_at|date:"D, M j Y"}}</span>
|
||||
<span class="order__status--display">
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<h1>Update Customer</h1>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
|
||||
<header class="object__header">
|
||||
<h1>Update Customer</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
|
||||
@ -3,15 +3,17 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
|
||||
</header>
|
||||
<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>Email</span>
|
||||
<span>Orders</span>
|
||||
</div>
|
||||
{% 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.email}}</span>
|
||||
<span>{{customer.num_orders}}</span>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col5">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity</span>
|
||||
@ -25,7 +25,7 @@
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% for item in order.lines.all %}
|
||||
<div class="object__item">
|
||||
<div class="object__item object__item--col5">
|
||||
{% with product=item.product %}
|
||||
<figure class="item__figure">
|
||||
<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 %}
|
||||
<p>No items in order yet.</p>
|
||||
{% 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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col5">
|
||||
<h4>Shipping</h4>
|
||||
<a href="{% url 'dashboard:order-ship' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||
</div>
|
||||
@ -66,7 +66,7 @@
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Customer</h4>
|
||||
</div>
|
||||
{% with customer=order.customer %}
|
||||
@ -98,7 +98,22 @@
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
|
||||
@ -16,14 +16,14 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col4">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity to fulfill</span>
|
||||
<span>Grind</span>
|
||||
</div>
|
||||
{% for form in form %}
|
||||
<div class="object__item">
|
||||
<div class="object__item object__item--col4">
|
||||
{% with product=form.instance.product %}
|
||||
{{form.id}}
|
||||
<figure class="item__figure">
|
||||
@ -36,7 +36,7 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% 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">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<h1><img src="{% static "images/box.png" %}" alt=""> Orders</h1>
|
||||
</header>
|
||||
<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>Date</span>
|
||||
<span>Customer</span>
|
||||
@ -15,7 +15,7 @@
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% 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.created_at|date:"D, M j Y"}}</span>
|
||||
<span>{{order.customer.get_full_name}}</span>
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>Fulfill Order #{{order.pk}}</h1>
|
||||
<section>
|
||||
<header class="object__header">
|
||||
<h1>Ship Order #{{order.pk}}</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form method="POST" action="">
|
||||
{% csrf_token %}
|
||||
{{ form.management_form }}
|
||||
@ -16,18 +18,15 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity to fulfill</span>
|
||||
<span>Grind</span>
|
||||
</div>
|
||||
{% for formitem in form %}
|
||||
<div class="object__item">
|
||||
{{formitem}}
|
||||
</div>
|
||||
{% 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">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
18
src/dashboard/templates/dashboard/prodphoto_create_form.html
Normal file
18
src/dashboard/templates/dashboard/prodphoto_create_form.html
Normal 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 %}
|
||||
@ -23,5 +23,24 @@
|
||||
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p>
|
||||
</div>
|
||||
</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>
|
||||
{% endblock content %}
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col4">
|
||||
<span></span>
|
||||
<span>Name</span>
|
||||
<span>Visible</span>
|
||||
<span>Price</span>
|
||||
</div>
|
||||
{% 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">
|
||||
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
|
||||
@ -10,6 +10,14 @@ urlpatterns = [
|
||||
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/<int:pk>/', include([
|
||||
path('', views.OrderDetailView.as_view(), name='order-detail'),
|
||||
@ -25,6 +33,11 @@ urlpatterns = [
|
||||
path('', views.ProductDetailView.as_view(), name='product-detail'),
|
||||
path('update/', views.ProductUpdateView.as_view(), name='product-update'),
|
||||
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'),
|
||||
|
||||
@ -23,10 +23,19 @@ from django.db.models.functions import Coalesce
|
||||
from accounts.models import User
|
||||
from accounts.utils import get_or_create_customer
|
||||
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 .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset
|
||||
from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset, CouponForm, ProductPhotoForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -59,6 +68,8 @@ class DashboardConfigView(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
|
||||
|
||||
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = ShippingMethod
|
||||
template_name = 'dashboard/shipmeth_create_form.html'
|
||||
@ -70,6 +81,35 @@ class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
|
||||
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):
|
||||
model = Order
|
||||
template_name = 'dashboard/order_list.html'
|
||||
@ -147,6 +187,7 @@ class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
class ProductListView(LoginRequiredMixin, ListView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_list.html'
|
||||
ordering = 'sorting'
|
||||
|
||||
# def get_queryset(self):
|
||||
# object_list = Product.objects.filter(
|
||||
@ -164,19 +205,52 @@ class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_update_form.html'
|
||||
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):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_confirm_delete.html'
|
||||
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):
|
||||
|
||||
@ -28,6 +28,7 @@ ANYMAIL_CONFIG = {
|
||||
|
||||
SERVER_EMAIL = os.environ.get('SERVER_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_SSL_REDIRECT = os.environ.get('SECURE_SSL_REDIRECT', 'False') == 'True'
|
||||
|
||||
14
src/static/scripts/product_form.js
Normal file
14
src/static/scripts/product_form.js
Normal 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'
|
||||
}
|
||||
|
||||
})
|
||||
@ -72,6 +72,7 @@ label {
|
||||
input[type=text],
|
||||
input[type=email],
|
||||
input[type=number],
|
||||
input[type=date],
|
||||
input[type=password],
|
||||
select[multiple=multiple],
|
||||
textarea {
|
||||
@ -145,7 +146,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.action-button--warning {
|
||||
background-color: var(--red-color);
|
||||
background-color: var(--red-color) !important;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
@ -314,12 +315,33 @@ main article {
|
||||
|
||||
.object__item {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 0.05rem solid var(--gray-color);
|
||||
text-decoration: none;
|
||||
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,
|
||||
@ -328,11 +350,11 @@ main article {
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.object__item:hover {
|
||||
.object__item--link:hover {
|
||||
background-color: var(--bg-alt-color);
|
||||
}
|
||||
|
||||
.object__item--header {
|
||||
.panel__header {
|
||||
font-weight: bold;
|
||||
background-color: var(--bg-alt-color);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
@ -497,3 +519,24 @@ main article {
|
||||
height: 50px;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -63,9 +63,6 @@ small, .text_small {
|
||||
font-size: 0.833rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -82,6 +79,7 @@ textarea {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
@ -91,6 +89,7 @@ input {
|
||||
|
||||
|
||||
label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
}
|
||||
@ -105,6 +104,7 @@ select {
|
||||
input[type=text],
|
||||
input[type=email],
|
||||
input[type=number],
|
||||
input[type=date],
|
||||
input[type=password],
|
||||
select[multiple=multiple],
|
||||
textarea {
|
||||
@ -116,19 +116,27 @@ textarea {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--yellow-color);
|
||||
}
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--yellow-color);
|
||||
}
|
||||
|
||||
select[multiple=multiple] {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
width: 1em;
|
||||
vertical-align: text-top;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input[type=radio] + label,
|
||||
input[type=checkbox] + label {
|
||||
display: inline-block;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@ -296,6 +304,11 @@ nav {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.coupon__form {
|
||||
max-width: 16rem;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.item__figure img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -325,7 +338,17 @@ nav {
|
||||
|
||||
|
||||
.order__shipping {
|
||||
}
|
||||
|
||||
|
||||
.shipping__details {
|
||||
margin-bottom: 3rem;
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.shipping__details input[type=submit] {
|
||||
font-size: 1.25rem;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -334,10 +357,6 @@ nav {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order__details {
|
||||
/*margin: 3rem 0;*/
|
||||
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: 4rem 0 0;
|
||||
@ -386,7 +405,7 @@ footer {
|
||||
background-color: var(--bg-alt-color);
|
||||
}
|
||||
|
||||
.object__item--header {
|
||||
.panel__header {
|
||||
font-weight: bold;
|
||||
background-color: var(--bg-alt-color);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
@ -415,10 +434,11 @@ footer {
|
||||
|
||||
|
||||
|
||||
._form_1 div {
|
||||
text-align: left !important;
|
||||
._form_1 {
|
||||
margin: 0;
|
||||
}
|
||||
._form_1 div form {
|
||||
margin: 1rem 0 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,23 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from core.models import Product, OrderLine
|
||||
from core.models import Product, OrderLine, Coupon
|
||||
from .payments import CreateOrder
|
||||
|
||||
from core import (
|
||||
DiscountValueType,
|
||||
VoucherType,
|
||||
TransactionStatus,
|
||||
OrderStatus,
|
||||
ShippingMethodType
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Cart:
|
||||
def __init__(self, request):
|
||||
self.session = request.session
|
||||
self.coupon_code = self.session.get('coupon_code')
|
||||
cart = self.session.get(settings.CART_SESSION_ID)
|
||||
if not cart:
|
||||
cart = self.session[settings.CART_SESSION_ID] = {}
|
||||
@ -62,14 +71,16 @@ class Cart:
|
||||
|
||||
def clear(self):
|
||||
del self.session[settings.CART_SESSION_ID]
|
||||
del self.coupon_code
|
||||
self.session.modified = True
|
||||
|
||||
def build_order_params(self):
|
||||
return \
|
||||
{
|
||||
'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()}',
|
||||
'discount': f'{self.get_discount()}',
|
||||
'shipping_price': '0',
|
||||
'tax_total': '0',
|
||||
'shipping_method': 'US POSTAL SERVICE',
|
||||
@ -105,16 +116,19 @@ class Cart:
|
||||
'country_code': 'US'
|
||||
}
|
||||
|
||||
# @property
|
||||
# def coupon(self):
|
||||
# if self.coupon_id:
|
||||
# return Coupon.objects.get(id=self.coupon_id)
|
||||
# return None
|
||||
@property
|
||||
def coupon(self):
|
||||
if self.coupon_code:
|
||||
return Coupon.objects.get(code=self.coupon_code)
|
||||
return None
|
||||
|
||||
# def get_discount(self):
|
||||
# if self.coupon:
|
||||
# return (self.coupon.discount / Decimal('100')) * self.get_total_price()
|
||||
# return Decimal('0')
|
||||
def get_discount(self):
|
||||
if self.coupon:
|
||||
if self.coupon.discount_value_type == DiscountValueType.FIXED:
|
||||
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):
|
||||
# return self.get_total_price() - self.get_discount()
|
||||
def get_total_price_after_discount(self):
|
||||
return round(self.get_total_price() - self.get_discount(), 2)
|
||||
|
||||
@ -5,6 +5,8 @@ from django.core.mail import EmailMessage
|
||||
from core.models import Order
|
||||
from accounts import STATE_CHOICES
|
||||
|
||||
from .tasks import contact_form_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AddToCartForm(forms.Form):
|
||||
@ -28,8 +30,27 @@ class AddToCartForm(forms.Form):
|
||||
(PERCOLATOR, 'Percolator'),
|
||||
(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)
|
||||
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)
|
||||
|
||||
|
||||
@ -53,8 +74,43 @@ class OrderCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
'coupon',
|
||||
'total_net_amount',
|
||||
)
|
||||
widgets = {
|
||||
'coupon': 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)
|
||||
|
||||
@ -112,10 +112,10 @@ class CreateOrder(PayPalClient):
|
||||
"currency_code": "USD",
|
||||
"value": params['tax_total']
|
||||
},
|
||||
# "shipping_discount": {
|
||||
# "currency_code": "USD",
|
||||
# "value": "10"
|
||||
# }
|
||||
"discount": {
|
||||
"currency_code": "USD",
|
||||
"value": params['discount']
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": processed_items,
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
# from celery import shared_task
|
||||
# from celery.utils.log import get_task_logger
|
||||
# from django.conf import settings
|
||||
# from django.core.mail import EmailMessage, send_mail
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
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'
|
||||
# ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
|
||||
# ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
|
||||
COTACT_FORM_TEMPLATE = 'storefront/contact_form'
|
||||
|
||||
# @shared_task(name='send_order_confirmation_email')
|
||||
# def send_order_confirmation_email(order):
|
||||
# send_templated_mail(
|
||||
# template_name=CONFIRM_ORDER_TEMPLATE,
|
||||
# from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
# recipient_list=[order['email']],
|
||||
# context=order
|
||||
# )
|
||||
@shared_task(name='contact_form_email')
|
||||
def contact_form_email(formdata):
|
||||
send_templated_mail(
|
||||
template_name=COTACT_FORM_TEMPLATE,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[settings.DEFAULT_CONTACT_EMAIL],
|
||||
context=formdata
|
||||
)
|
||||
|
||||
# logger.info(f"Order confirmation email sent to {order['email']}")
|
||||
logger.info(f"Contact form email sent from {formdata['email_address']}")
|
||||
|
||||
@ -28,7 +28,22 @@
|
||||
{% endfor %}
|
||||
</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">
|
||||
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a> or <a class="action-button action-button--large" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>
|
||||
</p>
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>Checkout</h2>
|
||||
<section class="order__details">
|
||||
<div class="order__shipping">
|
||||
<section class="order__shipping">
|
||||
<div class="shipping__details">
|
||||
<h3>Shipping Address</h3>
|
||||
<form action="" method="POST" class="address__form">
|
||||
{% csrf_token %}
|
||||
|
||||
14
src/storefront/templates/storefront/contact_form.html
Normal file
14
src/storefront/templates/storefront/contact_form.html
Normal 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 %}
|
||||
@ -8,7 +8,7 @@
|
||||
<a href="{% url 'storefront:customer-update' customer.pk %}" class="action-button">Edit profile</a>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Info</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
@ -53,19 +53,15 @@
|
||||
</section>
|
||||
{% with order_list=customer.orders.all %}
|
||||
<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>Date</span>
|
||||
<span>Status</span>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% for order in order_list %}
|
||||
<a class="object__item" href="">
|
||||
<span>#{{order.pk}}</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>
|
||||
</a>
|
||||
{% empty %}
|
||||
|
||||
@ -46,7 +46,13 @@
|
||||
{{form.as_p}}
|
||||
{# <input type="submit" value="Place order"> #}
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script defer src="{% static 'scripts/product_form.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<article class="product__item">
|
||||
<figure class="product__figure">
|
||||
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
@ -20,5 +24,4 @@
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -20,6 +20,3 @@
|
||||
</article>
|
||||
{% 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 %}
|
||||
|
||||
@ -3,6 +3,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
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('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>/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/webhooks/', views.paypal_webhook_endpoint, name='paypal-webhook'),
|
||||
|
||||
path('checkout/address/', views.CheckoutAddressView.as_view(), name='checkout-address'),
|
||||
path('checkout/', views.OrderCreateView.as_view(), name='order-create'),
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.core.mail import EmailMessage
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import JsonResponse
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
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.contrib.auth.decorators import login_required
|
||||
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.core import PayPalHttpClient, SandboxEnvironment
|
||||
|
||||
from accounts.models import User, Address
|
||||
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 .forms import AddToCartForm, OrderCreateForm, AddressForm
|
||||
from .forms import AddToCartForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
|
||||
from .cart import Cart
|
||||
from .payments import CaptureOrder
|
||||
|
||||
@ -41,6 +47,7 @@ class CartView(TemplateView):
|
||||
}
|
||||
)
|
||||
context['cart'] = cart
|
||||
context['coupon_apply_form'] = CouponApplyForm()
|
||||
return context
|
||||
|
||||
class CartAddProductView(SingleObjectMixin, FormView):
|
||||
@ -75,10 +82,32 @@ def cart_remove_product_view(request, pk):
|
||||
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):
|
||||
model = Product
|
||||
template_name = 'storefront/product_list.html'
|
||||
form_class = AddToCartForm
|
||||
ordering = 'sorting'
|
||||
|
||||
queryset = Product.objects.filter(
|
||||
visible_in_listings=True
|
||||
@ -127,6 +156,7 @@ class OrderCreateView(CreateView):
|
||||
def get_initial(self):
|
||||
cart = Cart(self.request)
|
||||
initial = {
|
||||
'coupon': cart.coupon,
|
||||
'total_net_amount': cart.get_total_price()
|
||||
}
|
||||
|
||||
@ -182,6 +212,13 @@ def paypal_order_transaction_capture(request, transaction_id):
|
||||
else:
|
||||
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):
|
||||
template_name = 'storefront/payment_done.html'
|
||||
@ -211,3 +248,12 @@ class CustomerUpdateView(UpdateView):
|
||||
|
||||
class AboutView(TemplateView):
|
||||
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)
|
||||
|
||||
@ -16,10 +16,10 @@
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% 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%}">
|
||||
|
||||
<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 }}
|
||||
{% if emailaddress.verified %}
|
||||
|
||||
@ -42,12 +42,12 @@
|
||||
</div>
|
||||
<nav>
|
||||
<a href="{% url 'storefront:product-list' %}">Shop</a>
|
||||
<a href="">Wholesale</a>
|
||||
{# <a href="">Wholesale</a> #}
|
||||
<a href="">Subscribe</a>
|
||||
<a href="">Cafe</a>
|
||||
<a href="">Fair Trade</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' %}">
|
||||
<span class="cart__length">{{cart|length}}</span>
|
||||
<img class="cart__icon" src="{% static 'images/shopping_cart.svg' %}" alt="Shopping cart">
|
||||
@ -71,8 +71,7 @@
|
||||
<div>
|
||||
<h4>Problem with your order?<br>Have a question?</h4>
|
||||
<p>Please contact us, we’re 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 %}
|
||||
{% endblock footer %}
|
||||
<div class="_form_1"></div><script src="https://bltc999.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site__copyright">
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
<img src="{% static 'images/customer.png' %}" alt="">
|
||||
Customers
|
||||
</a>
|
||||
<a href="">
|
||||
<a href="{% url 'dashboard:coupon-list' %}">
|
||||
<img src="{% static 'images/coupon.png' %}" alt="">
|
||||
Coupons
|
||||
</a>
|
||||
|
||||
16
src/templates/templated_email/storefront/contact_form.email
Normal file
16
src/templates/templated_email/storefront/contact_form.email
Normal 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user