Merge branch 'feature/usps-shipping' into develop
This commit is contained in:
commit
1e49f36f7a
@ -71,3 +71,27 @@ class ShippingMethodType:
|
|||||||
(PRICE_BASED, "Price based shipping"),
|
(PRICE_BASED, "Price based shipping"),
|
||||||
(WEIGHT_BASED, "Weight based shipping"),
|
(WEIGHT_BASED, "Weight based shipping"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class ShippingService:
|
||||||
|
FIRST_CLASS = "FIRST CLASS"
|
||||||
|
PRIORITY = "PRIORITY"
|
||||||
|
PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(FIRST_CLASS, "First Class"),
|
||||||
|
(PRIORITY, "Priority"),
|
||||||
|
(PRIORITY_COMMERCIAL, "Priority Commercial")
|
||||||
|
]
|
||||||
|
|
||||||
|
class ShippingContainer:
|
||||||
|
LG_FLAT_RATE_BOX = "LG FLAT RATE BOX"
|
||||||
|
REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA"
|
||||||
|
REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB"
|
||||||
|
VARIABLE = "VARIABLE"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(LG_FLAT_RATE_BOX, "Flate Rate Box - Large"),
|
||||||
|
(REGIONAL_RATE_BOX_A, "Regional Rate Box A"),
|
||||||
|
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
|
||||||
|
(VARIABLE, "Variable")
|
||||||
|
]
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.0.2 on 2022-04-24 16:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_alter_product_options_product_sorting'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='order',
|
||||||
|
options={'ordering': ('-created_at',)},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='shipping_total',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=5),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -9,6 +9,8 @@ from django.conf import settings
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
from django_measurement.models import MeasurementField
|
from django_measurement.models import MeasurementField
|
||||||
|
|
||||||
@ -25,6 +27,10 @@ from .weight import WeightUnits, zero_weight
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ProductEncoder(DjangoJSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
logger.info(f"\n{obj}\n")
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
class ProductManager(models.Manager):
|
class ProductManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -69,7 +75,7 @@ class Product(models.Model):
|
|||||||
try:
|
try:
|
||||||
return self.productphoto_set.all()[1]
|
return self.productphoto_set.all()[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
return 'No image'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['sorting', 'name']
|
ordering = ['sorting', 'name']
|
||||||
@ -200,6 +206,7 @@ class Order(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
coupon = models.ForeignKey(
|
coupon = models.ForeignKey(
|
||||||
Coupon,
|
Coupon,
|
||||||
related_name='orders',
|
related_name='orders',
|
||||||
@ -207,6 +214,12 @@ class Order(models.Model):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
shipping_total = models.DecimalField(
|
||||||
|
max_digits=5,
|
||||||
|
decimal_places=2,
|
||||||
|
default=0
|
||||||
|
)
|
||||||
|
|
||||||
total_net_amount = models.DecimalField(
|
total_net_amount = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
@ -237,7 +250,7 @@ class Order(models.Model):
|
|||||||
return Decimal('0')
|
return Decimal('0')
|
||||||
|
|
||||||
def get_total_price_after_discount(self):
|
def get_total_price_after_discount(self):
|
||||||
return round(self.total_net_amount - self.get_discount(), 2)
|
return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 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})
|
||||||
|
|||||||
38
src/core/usps.py
Normal file
38
src/core/usps.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from usps import USPSApi
|
||||||
|
|
||||||
|
class USPSApiWithRate(USPSApi):
|
||||||
|
urls = {
|
||||||
|
'tracking': 'TrackV2{test}&XML={xml}',
|
||||||
|
'label': 'eVS{test}&XML={xml}',
|
||||||
|
'validate': 'Verify&XML={xml}',
|
||||||
|
'rate': 'RateV4&XML={xml}',
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_rate(self, *args, **kwargs):
|
||||||
|
return Rate(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Rate:
|
||||||
|
|
||||||
|
def __init__(self, usps, request, **kwargs):
|
||||||
|
xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id})
|
||||||
|
etree.SubElement(xml, 'Revision').text = '2'
|
||||||
|
package = etree.SubElement(xml, 'Package', {'ID': '0'})
|
||||||
|
etree.SubElement(package, 'Service').text = request['service']
|
||||||
|
etree.SubElement(package, 'ZipOrigination').text = request['zip_origination']
|
||||||
|
etree.SubElement(package, 'ZipDestination').text = request['zip_destination']
|
||||||
|
etree.SubElement(package, 'Pounds').text = request['pounds']
|
||||||
|
etree.SubElement(package, 'Ounces').text = request['ounces']
|
||||||
|
etree.SubElement(package, 'Container').text = request['container']
|
||||||
|
etree.SubElement(package, 'Width').text = request['width']
|
||||||
|
etree.SubElement(package, 'Length').text = request['length']
|
||||||
|
etree.SubElement(package, 'Height').text = request['height']
|
||||||
|
etree.SubElement(package, 'Girth').text = request['girth']
|
||||||
|
etree.SubElement(package, 'Machinable').text = request['machinable']
|
||||||
|
|
||||||
|
self.result = usps.send_request('rate', xml)
|
||||||
@ -103,11 +103,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel__item">
|
<div class="panel__item">
|
||||||
<p>
|
<p>
|
||||||
<span>Subtotal: {{order.total_net_amount}}</span><br>
|
<span>Subtotal: ${{order.total_net_amount}}</span><br>
|
||||||
{% if order.coupon %}
|
{% if order.coupon %}
|
||||||
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
|
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>Total: {{order.get_total_price_after_discount}}</span>
|
<span>Shipping: ${{order.shipping_total}}</span><br>
|
||||||
|
<span>Total: ${{order.get_total_price_after_discount}}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
<span class="order__status--display">
|
<span class="order__status--display">
|
||||||
<div class="status__dot order__status--{{order.status}}"></div>
|
<div class="status__dot order__status--{{order.status}}"></div>
|
||||||
{{order.get_status_display}}</span>
|
{{order.get_status_display}}</span>
|
||||||
<span>${{order.total_net_amount}}</span>
|
<span>${{order.get_total_price_after_discount}}</span>
|
||||||
</a>
|
</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="object__item">No orders</span>
|
<span class="object__item">No orders</span>
|
||||||
|
|||||||
@ -20,6 +20,8 @@ CACHE_CONFIG = {
|
|||||||
|
|
||||||
PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '')
|
PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '')
|
||||||
PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '')
|
PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '')
|
||||||
|
USPS_USER_ID = os.environ.get('USPS_USER_ID', '639NATHA3105')
|
||||||
|
DEFAULT_ZIP_ORIGINATION = os.environ.get('DEFAULT_ZIP_ORIGINATION', '98368')
|
||||||
|
|
||||||
ANYMAIL_CONFIG = {
|
ANYMAIL_CONFIG = {
|
||||||
'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''),
|
'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''),
|
||||||
|
|||||||
@ -93,6 +93,7 @@ DATABASES = {
|
|||||||
CACHES = {'default': CACHE_CONFIG}
|
CACHES = {'default': CACHE_CONFIG}
|
||||||
|
|
||||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
||||||
|
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|||||||
BIN
src/static/images/site_banner.jpg
Normal file
BIN
src/static/images/site_banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
@ -1,12 +1,14 @@
|
|||||||
:root {
|
:root {
|
||||||
--fg-color: #34201a;
|
--fg-color: #34201a;
|
||||||
--fg-alt-color: #663a2d;
|
--fg-alt-color: #663a2d;
|
||||||
--bg-color: #f5f5f5;
|
--bg-color: #fffbf8;
|
||||||
--bg-alt-color: #c8a783;
|
--bg-alt-color: #b07952;
|
||||||
--gray-color: #9d9d9d;
|
--gray-color: #9d9d9d;
|
||||||
--yellow-color: #f8a911;
|
--yellow-color: #f8a911;
|
||||||
--yellow-alt-color: #ffce6f;
|
--yellow-alt-color: #ffce6f;
|
||||||
--yellow-dark-color: #b27606;
|
--yellow-dark-color: #b27606;
|
||||||
|
--red-color: #d43131;
|
||||||
|
--green-color: #3ea165;
|
||||||
|
|
||||||
--default-border: 2px solid var(--gray-color);
|
--default-border: 2px solid var(--gray-color);
|
||||||
}
|
}
|
||||||
@ -48,6 +50,7 @@ h1, h2, h3, h4, h5 {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
font-family: 'Vollkorn', serif;
|
||||||
font-size: 2.488rem;
|
font-size: 2.488rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,6 +446,53 @@ section:not(:last-child) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Site Banner
|
||||||
|
========================================================================== */
|
||||||
|
.site__banner {
|
||||||
|
background-color: rgba(0, 0, 0, 0.44);
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
background-image: url("/static/images/site_banner.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8rem 1rem;
|
||||||
|
font-family: 'Vollkorn', serif;
|
||||||
|
}
|
||||||
|
.site__banner h1 {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__banner p {
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages
|
||||||
|
========================================================================== */
|
||||||
|
.messages {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages .success {
|
||||||
|
background-color: var(--green-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages .warning {
|
||||||
|
background-color: var(--yellow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages .error {
|
||||||
|
background-color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Site Cart
|
/* Site Cart
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
.site__cart {
|
.site__cart {
|
||||||
|
|||||||
@ -1,17 +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, Coupon
|
from django.contrib import messages
|
||||||
from .payments import CreateOrder
|
|
||||||
|
|
||||||
|
from core.models import Product, OrderLine, Coupon
|
||||||
|
from core.usps import USPSApiWithRate
|
||||||
from core import (
|
from core import (
|
||||||
DiscountValueType,
|
DiscountValueType,
|
||||||
VoucherType,
|
VoucherType,
|
||||||
TransactionStatus,
|
TransactionStatus,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
ShippingMethodType
|
ShippingMethodType,
|
||||||
|
ShippingService,
|
||||||
|
ShippingContainer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .payments import CreateOrder
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Cart:
|
class Cart:
|
||||||
@ -23,7 +29,7 @@ class Cart:
|
|||||||
cart = self.session[settings.CART_SESSION_ID] = {}
|
cart = self.session[settings.CART_SESSION_ID] = {}
|
||||||
self.cart = cart
|
self.cart = cart
|
||||||
|
|
||||||
def add(self, product, quantity=1, grind='', update_quantity=False):
|
def add(self, request, product, quantity=1, grind='', update_quantity=False):
|
||||||
product_id = str(product.id)
|
product_id = str(product.id)
|
||||||
if product_id not in self.cart:
|
if product_id not in self.cart:
|
||||||
self.cart[product_id] = {
|
self.cart[product_id] = {
|
||||||
@ -36,7 +42,10 @@ class Cart:
|
|||||||
self.cart[product_id]['quantity'] = quantity
|
self.cart[product_id]['quantity'] = quantity
|
||||||
else:
|
else:
|
||||||
self.cart[product_id]['quantity'] += quantity
|
self.cart[product_id]['quantity'] += quantity
|
||||||
self.save()
|
if len(self) <= 20:
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
messages.warning(request, "Cart is full: 20 items or less.")
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.session[settings.CART_SESSION_ID] = self.cart
|
self.session[settings.CART_SESSION_ID] = self.cart
|
||||||
@ -63,6 +72,26 @@ class Cart:
|
|||||||
def __len__(self):
|
def __len__(self):
|
||||||
return sum(item['quantity'] for item in self.cart.values())
|
return sum(item['quantity'] for item in self.cart.values())
|
||||||
|
|
||||||
|
def get_total_weight(self):
|
||||||
|
return sum([item['product'].weight.value * item['quantity'] for item in self])
|
||||||
|
|
||||||
|
def get_shipping_box(self):
|
||||||
|
logger.info(len(self))
|
||||||
|
if len(self) > 6 and len(self) <= 10:
|
||||||
|
return ShippingContainer.LG_FLAT_RATE_BOX
|
||||||
|
elif len(self) > 2 and len(self) <= 6:
|
||||||
|
return ShippingContainer.REGIONAL_RATE_BOX_B
|
||||||
|
elif len(self) <= 2:
|
||||||
|
return ShippingContainer.REGIONAL_RATE_BOX_A
|
||||||
|
else:
|
||||||
|
return ShippingContainer.VARIABLE
|
||||||
|
|
||||||
|
def get_shipping_cost(self):
|
||||||
|
usps_rate_request = self.build_usps_rate_request()
|
||||||
|
usps = USPSApiWithRate(settings.USPS_USER_ID, test=True)
|
||||||
|
validation = usps.get_rate(usps_rate_request)
|
||||||
|
return Decimal(validation.result['RateV4Response']['Package']['Postage']['CommercialRate'])
|
||||||
|
|
||||||
def get_total_price(self):
|
def get_total_price(self):
|
||||||
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
|
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
|
||||||
|
|
||||||
@ -74,6 +103,22 @@ class Cart:
|
|||||||
pass
|
pass
|
||||||
self.session.modified = True
|
self.session.modified = True
|
||||||
|
|
||||||
|
def build_usps_rate_request(self):
|
||||||
|
return \
|
||||||
|
{
|
||||||
|
'service': ShippingService.PRIORITY_COMMERCIAL,
|
||||||
|
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
|
||||||
|
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
|
||||||
|
'pounds': '0',
|
||||||
|
'ounces': f'{self.get_total_weight()}',
|
||||||
|
'container': f'{self.get_shipping_box()}',
|
||||||
|
'width': '',
|
||||||
|
'length': '',
|
||||||
|
'height': '',
|
||||||
|
'girth': '',
|
||||||
|
'machinable': 'TRUE'
|
||||||
|
}
|
||||||
|
|
||||||
def build_order_params(self):
|
def build_order_params(self):
|
||||||
return \
|
return \
|
||||||
{
|
{
|
||||||
@ -81,7 +126,7 @@ class Cart:
|
|||||||
'total_price': f'{self.get_total_price_after_discount()}',
|
'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()}',
|
'discount': f'{self.get_discount()}',
|
||||||
'shipping_price': '0',
|
'shipping_price': f'{self.get_shipping_cost()}',
|
||||||
'tax_total': '0',
|
'tax_total': '0',
|
||||||
'shipping_method': 'US POSTAL SERVICE',
|
'shipping_method': 'US POSTAL SERVICE',
|
||||||
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
|
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
|
||||||
@ -130,5 +175,8 @@ class Cart:
|
|||||||
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
|
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
|
||||||
return Decimal('0')
|
return Decimal('0')
|
||||||
|
|
||||||
def get_total_price_after_discount(self):
|
def get_subtotal_price_after_discount(self):
|
||||||
return round(self.get_total_price() - self.get_discount(), 2)
|
return round(self.get_total_price() - self.get_discount(), 2)
|
||||||
|
|
||||||
|
def get_total_price_after_discount(self):
|
||||||
|
return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2)
|
||||||
|
|||||||
@ -96,9 +96,11 @@ class OrderCreateForm(forms.ModelForm):
|
|||||||
model = Order
|
model = Order
|
||||||
fields = (
|
fields = (
|
||||||
'total_net_amount',
|
'total_net_amount',
|
||||||
|
'shipping_total',
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
'total_net_amount': forms.HiddenInput()
|
'total_net_amount': forms.HiddenInput(),
|
||||||
|
'shipping_total': forms.HiddenInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
class CouponApplyForm(forms.Form):
|
class CouponApplyForm(forms.Form):
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<table class="cart__totals">
|
<table class="cart__totals">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Subtotal</td>
|
<td>Subtotal</td>
|
||||||
<td>${{cart.get_total_price|floatformat:"2"}}</td>
|
<td>${{ cart.get_total_price|floatformat:"2" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if cart.coupon %}
|
{% if cart.coupon %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
|
<td><strong>${{cart.get_subtotal_price_after_discount|floatformat:"2"}}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,13 +5,9 @@
|
|||||||
<header>
|
<header>
|
||||||
<h1>Contact us</h1>
|
<h1>Contact us</h1>
|
||||||
<h4>Problem with your online order or have a question?</h4>
|
<h4>Problem with your online order or have a question?</h4>
|
||||||
<p>
|
<p>Please contact us, we’re happy to help you.</p>
|
||||||
Please contact us, we’re happy to help you over the phone<br>
|
|
||||||
<a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.
|
|
||||||
</p>
|
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
<p>Or send us a message using the form below and we'll email you back as soon as we can.</p>
|
|
||||||
<form action="{% url 'storefront:contact' %}" method="post" class="contact-form">
|
<form action="{% url 'storefront:contact' %}" method="post" class="contact-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{form.as_p}}
|
{{form.as_p}}
|
||||||
|
|||||||
@ -62,6 +62,10 @@
|
|||||||
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
|
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td>Shipping</td>
|
||||||
|
<td>${{ cart.get_shipping_cost }}</small></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
|
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="site__banner">
|
||||||
|
<h1><em>Better</em>, not <em>Bitter</em></h1>
|
||||||
|
<p>ORGANIC COFFEE, SLOW ROASTED, ITALIAN STYLE</p>
|
||||||
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<section class="product__list">
|
<section class="product__list">
|
||||||
{% for product in product_list %}
|
{% for product in product_list %}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h2>Reviews</h2>
|
<h1>Reviews</h1>
|
||||||
</header>
|
</header>
|
||||||
<section>
|
<section>
|
||||||
<figure>
|
<figure>
|
||||||
|
|||||||
@ -44,6 +44,7 @@ class CartTest(TestCase):
|
|||||||
request = response.wsgi_request
|
request = response.wsgi_request
|
||||||
cart = Cart(request)
|
cart = Cart(request)
|
||||||
cart.add(
|
cart.add(
|
||||||
|
request=request,
|
||||||
product=self.product,
|
product=self.product,
|
||||||
quantity=1,
|
quantity=1,
|
||||||
update_quantity=False
|
update_quantity=False
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
|
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
|
||||||
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
|
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
|
||||||
@ -26,6 +27,7 @@ from accounts.utils import get_or_create_customer
|
|||||||
from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
|
from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
|
||||||
from core.models import Product, Order, Transaction, OrderLine, Coupon
|
from core.models import Product, Order, Transaction, OrderLine, Coupon
|
||||||
from core.forms import ShippingMethodForm
|
from core.forms import ShippingMethodForm
|
||||||
|
from core import OrderStatus
|
||||||
|
|
||||||
from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
|
from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
|
||||||
from .cart import Cart
|
from .cart import Cart
|
||||||
@ -61,6 +63,7 @@ class CartAddProductView(SingleObjectMixin, FormView):
|
|||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
cart.add(
|
cart.add(
|
||||||
|
request=request,
|
||||||
product=self.get_object(),
|
product=self.get_object(),
|
||||||
grind=form.cleaned_data['grind'],
|
grind=form.cleaned_data['grind'],
|
||||||
quantity=form.cleaned_data['quantity']
|
quantity=form.cleaned_data['quantity']
|
||||||
@ -85,6 +88,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
|
|||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
cart.add(
|
cart.add(
|
||||||
|
request=request,
|
||||||
product=self.get_object(),
|
product=self.get_object(),
|
||||||
quantity=form.cleaned_data['quantity'],
|
quantity=form.cleaned_data['quantity'],
|
||||||
update_quantity=form.cleaned_data['update']
|
update_quantity=form.cleaned_data['update']
|
||||||
@ -178,7 +182,8 @@ class OrderCreateView(CreateView):
|
|||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
cart = Cart(self.request)
|
cart = Cart(self.request)
|
||||||
initial = {
|
initial = {
|
||||||
'total_net_amount': cart.get_total_price()
|
'total_net_amount': cart.get_total_price(),
|
||||||
|
'shipping_total': cart.get_shipping_cost()
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
@ -210,13 +215,14 @@ class OrderCreateView(CreateView):
|
|||||||
cart = Cart(self.request)
|
cart = Cart(self.request)
|
||||||
shipping_address = self.request.session.get('shipping_address')
|
shipping_address = self.request.session.get('shipping_address')
|
||||||
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
|
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
|
||||||
|
form.instance.status = OrderStatus.DRAFT
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
bulk_list = cart.build_bulk_list(self.object)
|
bulk_list = cart.build_bulk_list(self.object)
|
||||||
objs = OrderLine.objects.bulk_create(bulk_list)
|
objs = OrderLine.objects.bulk_create(bulk_list)
|
||||||
|
|
||||||
response = cart.create_order()
|
response = cart.create_order()
|
||||||
data = response.result.__dict__['_dict']
|
data = response.result.__dict__['_dict']
|
||||||
cart.clear()
|
|
||||||
self.request.session['order_id'] = self.object.pk
|
self.request.session['order_id'] = self.object.pk
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
@ -224,8 +230,12 @@ class OrderCreateView(CreateView):
|
|||||||
def paypal_order_transaction_capture(request, transaction_id):
|
def paypal_order_transaction_capture(request, transaction_id):
|
||||||
if request.method =="POST":
|
if request.method =="POST":
|
||||||
data = CaptureOrder().capture_order(transaction_id)
|
data = CaptureOrder().capture_order(transaction_id)
|
||||||
|
cart = Cart(request)
|
||||||
transaction = Transaction.objects.get(order__pk=request.session.get('order_id'))
|
cart.clear()
|
||||||
|
order = Order.objects.get(pk=request.session.get('order_id'))
|
||||||
|
order.status = OrderStatus.UNFULFILLED
|
||||||
|
order.save()
|
||||||
|
transaction = Transaction.objects.get(order=order)
|
||||||
transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id']
|
transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id']
|
||||||
transaction.status = data['status']
|
transaction.status = data['status']
|
||||||
transaction.save()
|
transaction.save()
|
||||||
|
|||||||
@ -72,7 +72,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
{% if messages %}
|
||||||
|
<section class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<p {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</main>
|
</main>
|
||||||
@ -81,8 +88,7 @@
|
|||||||
<p><button class="show-modal">Subscribe to our newsletter</button></p>
|
<p><button class="show-modal">Subscribe to our newsletter</button></p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Problem with your online order or have a question?</strong><br>
|
<strong>Problem with your online order or have a question?</strong><br>
|
||||||
Please <a href="{% url 'storefront:contact' %}">contact us</a>, we’re happy to help you over the phone<br>
|
Please <a href="{% url 'storefront:contact' %}">contact us</a>, we’re happy to help you.<br>
|
||||||
<a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.<br>
|
|
||||||
<address>854 East Park Ave. Suite 1, Port Townsend, WA 98368</address>
|
<address>854 East Park Ave. Suite 1, Port Townsend, WA 98368</address>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
{% block plain %}
|
{% block plain %}
|
||||||
Great news! Your recent order #{{order_id}} has shipped
|
Great news! Your recent order #{{order_id}} has shipped
|
||||||
|
|
||||||
{{tracking_id}}
|
Your USPS tracking ID: {{tracking_id}}
|
||||||
|
|
||||||
Thanks,
|
Thanks,
|
||||||
Port Townsend Coffee
|
Port Townsend Coffee
|
||||||
@ -11,7 +11,7 @@
|
|||||||
{% block html %}
|
{% block html %}
|
||||||
<p>Great news! Your recent order #{{order_id}} has shipped</p>
|
<p>Great news! Your recent order #{{order_id}} has shipped</p>
|
||||||
|
|
||||||
<p>{{tracking_id}}</p>
|
<p>Your USPS tracking ID: {{tracking_id}}</p>
|
||||||
|
|
||||||
<p>Thanks,<br>
|
<p>Thanks,<br>
|
||||||
Port Townsend Coffee</p>
|
Port Townsend Coffee</p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user