Merge branch 'feature/cart-redesign' into develop

This commit is contained in:
Nathan Chapman 2022-11-27 12:18:02 -07:00
commit b07806a0d7
25 changed files with 521 additions and 470 deletions

View File

@ -25,6 +25,14 @@ class Address(models.Model):
{self.city}, {self.state}, {self.postal_code} {self.city}, {self.state}, {self.postal_code}
""" """
def __iter__(self):
yield ('address_line_1', self.street_address_1),
yield ('address_line_2', self.street_address_2),
yield ('admin_area_2', self.city),
yield ('admin_area_1', self.state),
yield ('postal_code', self.postal_code),
yield ('country_code', 'US')
class User(AbstractUser): class User(AbstractUser):
addresses = models.ManyToManyField( addresses = models.ManyToManyField(

View File

@ -23,14 +23,14 @@ def get_or_create_customer(request, form, shipping_address):
user.save() user.save()
else: else:
user, u_created = User.objects.get_or_create( user, u_created = User.objects.get_or_create(
email=form.cleaned_data['email'].lower(), email=shipping_address['email'].lower(),
defaults={ defaults={
'username': form.cleaned_data['email'].lower(), 'username': shipping_address['email'].lower(),
'is_staff': False, 'is_staff': False,
'is_active': True, 'is_active': True,
'is_superuser': False, 'is_superuser': False,
'first_name': form.cleaned_data['first_name'], 'first_name': address.first_name,
'last_name': form.cleaned_data['last_name'], 'last_name': address.last_name,
'default_shipping_address': address, 'default_shipping_address': address,
} }
) )

View File

@ -15,6 +15,8 @@
"fields": { "fields": {
"usps_user_id": "012BETTE1249", "usps_user_id": "012BETTE1249",
"default_shipping_rate": 1, "default_shipping_rate": 1,
"free_shipping_min": "100.00" "free_shipping_min": "100.00",
"max_cart_quantity": 20,
"max_cart_weight": "20:lb"
} }
}] }]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.0.2 on 2022-11-27 04:28
import core.weight
from django.db import migrations
import django_measurement.models
import measurement.measures.mass
class Migration(migrations.Migration):
dependencies = [
('core', '0012_sitesettings_max_cart_quantity_and_more'),
]
operations = [
migrations.AlterField(
model_name='shippingrate',
name='max_order_weight',
field=django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True),
),
migrations.AlterField(
model_name='shippingrate',
name='min_order_weight',
field=django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 4.0.2 on 2022-11-27 16:26
import core.weight
from django.db import migrations, models
import django_measurement.models
import measurement.measures.mass
class Migration(migrations.Migration):
dependencies = [
('core', '0013_alter_shippingrate_max_order_weight_and_more'),
]
operations = [
migrations.AddField(
model_name='sitesettings',
name='max_cart_weight',
field=django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, help_text='Maximum weight allowed for cart.', measurement=measurement.measures.mass.Mass, null=True),
),
migrations.AlterField(
model_name='sitesettings',
name='free_shipping_min',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping.', max_digits=12, null=True),
),
migrations.AlterField(
model_name='sitesettings',
name='max_cart_quantity',
field=models.PositiveIntegerField(blank=True, default=20, help_text='Maximum amount of items allowed in cart.', null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-11-27 18:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_sitesettings_max_cart_weight_and_more'),
]
operations = [
migrations.AlterField(
model_name='order',
name='coupon_amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
]

View File

@ -270,11 +270,17 @@ class ShippingRate(models.Model):
choices=ShippingContainer.CHOICES, choices=ShippingContainer.CHOICES,
default=ShippingContainer.VARIABLE default=ShippingContainer.VARIABLE
) )
min_order_weight = models.PositiveIntegerField( min_order_weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
default=zero_weight,
blank=True, blank=True,
null=True null=True
) )
max_order_weight = models.PositiveIntegerField( max_order_weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
default=zero_weight,
blank=True, blank=True,
null=True null=True
) )
@ -350,7 +356,11 @@ class Order(models.Model):
decimal_places=2, decimal_places=2,
default=0 default=0
) )
coupon_amount = models.CharField(max_length=255, blank=True) coupon_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0
)
shipping_total = models.DecimalField( shipping_total = models.DecimalField(
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
@ -514,10 +524,21 @@ class SiteSettings(SingletonBase):
decimal_places=settings.DEFAULT_DECIMAL_PLACES, decimal_places=settings.DEFAULT_DECIMAL_PLACES,
blank=True, blank=True,
null=True, null=True,
help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping' help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping.'
) )
max_cart_quantity = models.PositiveIntegerField( max_cart_quantity = models.PositiveIntegerField(
default=20 default=20,
blank=True,
null=True,
help_text='Maximum amount of items allowed in cart.'
)
max_cart_weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
default=zero_weight,
blank=True,
null=True,
help_text='Maximum weight allowed for cart.'
) )
def __str__(self): def __str__(self):

View File

@ -17,7 +17,7 @@ class WeightUnits:
def zero_weight(): def zero_weight():
"""Represent the zero weight value.""" """Represent the zero weight value."""
return Weight(kg=0) return Weight(lb=0)
def convert_weight(weight: Weight, unit: str) -> Weight: def convert_weight(weight: Weight, unit: str) -> Weight:

View File

@ -33,6 +33,8 @@
<p>USPS User ID: {{ site_settings.usps_user_id }}</p> <p>USPS User ID: {{ site_settings.usps_user_id }}</p>
<p>Default shipping rate: {{ site_settings.default_shipping_rate }}</p> <p>Default shipping rate: {{ site_settings.default_shipping_rate }}</p>
<p>Free shipping min: ${{ site_settings.free_shipping_min }}</p> <p>Free shipping min: ${{ site_settings.free_shipping_min }}</p>
<p>Max cart quantity: {{ site_settings.max_cart_quantity }} items</p>
<p>Max cart weight: {{ site_settings.max_cart_weight }}</p>
</div> </div>
</section> </section>
</article> </article>

View File

@ -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.get_total_price_after_discount}}</span> <span>${{order.total_amount}}</span>
</a> </a>
{% empty %} {% empty %}
<span class="object__item">No orders</span> <span class="object__item">No orders</span>

View File

@ -66,7 +66,7 @@ logger = logging.getLogger(__name__)
class ProductCreateViewTests(TestCase): class ProductCreateViewTests(TestCase):
fixtures = [ fixtures = [
'shipping_rates.json', 'site_settings_and_shipping_rates',
'accounts.json', 'accounts.json',
'coupons.json', 'coupons.json',
'products.json', 'products.json',
@ -101,7 +101,7 @@ class ProductCreateViewTests(TestCase):
class OrderCancelViewTests(TestCase): class OrderCancelViewTests(TestCase):
fixtures = [ fixtures = [
'shipping_rates.json', 'site_settings_and_shipping_rates',
'accounts.json', 'accounts.json',
'coupons.json', 'coupons.json',
'products.json', 'products.json',

View File

@ -12,7 +12,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
class AddressTests(StaticLiveServerTestCase): class AddressTests(StaticLiveServerTestCase):
fixtures = ['shipping_rates.json', 'products.json'] fixtures = ['site_settings_and_shipping_rates', 'products.json']
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
class CouponTests(StaticLiveServerTestCase): class CouponTests(StaticLiveServerTestCase):
fixtures = [ fixtures = [
'shipping_rates.json', 'products.json', 'accounts.json', 'coupons.json' 'site_settings_and_shipping_rates', 'products.json', 'accounts.json', 'coupons.json'
] ]
@classmethod @classmethod

View File

@ -106,7 +106,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' # 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
@ -179,6 +179,8 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
) )
FILE_UPLOAD_MAX_MEMORY_SIZE = 62914560
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
@ -238,7 +240,6 @@ LOGGING = {
}, },
} }
CART_SESSION_ID = 'cart'
DEFAULT_COUNTRY = 'US' DEFAULT_COUNTRY = 'US'
DEFAULT_CURRENCY = 'USD' DEFAULT_CURRENCY = 'USD'

View File

@ -8,7 +8,7 @@ from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.core.cache import cache from django.core.cache import cache
from django.db.models import OuterRef, Q, Subquery from django.db.models import OuterRef, Q, Subquery
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, ObjectDoesNotExist
from core.models import ( from core.models import (
ProductCategory, Product, ProductVariant, OrderLine, Coupon, ShippingRate, ProductCategory, Product, ProductVariant, OrderLine, Coupon, ShippingRate,
@ -28,23 +28,70 @@ from core import (
build_usps_rate_request build_usps_rate_request
) )
from .forms import UpdateCartItemForm from .forms import CartItemUpdateForm
from .payments import CreateOrder from .payments import CreateOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CartItem: class CartItem:
update_form = UpdateCartItemForm update_form = CartItemUpdateForm
order_line_class = OrderLine
variant = None
quantity = None
options = None
def __init__(self, item): def __init__(self, item):
self.variant = item['variant'] try:
self.quantity = item['quantity'] self.variant = ProductVariant.objects.get(pk=item['variant_pk'])
self.options = item['options'] except ProductVariant.DoesNotExist:
self.quantity = None
else:
self.quantity = item['quantity']
self.options = item['options']
def __iter__(self):
yield ('name', f'{self.variant} {self.options_as_str()}')
yield ('description', self.variant.product.subtitle)
yield ('unit_amount', {
'currency_code': settings.DEFAULT_CURRENCY,
'value': str(self.variant.price),
})
yield ('quantity', str(self.quantity))
def __str__(self):
return f'{self.variant} [{self.quantity} × ${self.variant.price}]'
def serialize(self):
if self.variant is not None:
return \
{
'variant_pk': self.variant.pk,
'quantity': self.quantity,
'options': self.options
}
def deserialize(self, data):
self.variant = ProductVariant.objects.get(data['variant_pk'])
self.quantity = data['quantity']
self.options = data['options']
def options_as_str(self):
options = [f'{key}: {value}' for key, value in self.options.items()]
return '; '.join(options)
def as_order_line(self, order):
return self.order_line_class(
order=order,
variant=self.variant,
customer_note=self.options_as_str(),
unit_price=self.variant.price,
quantity=self.quantity
)
def get_update_form(self, index): def get_update_form(self, index):
return self.update_form(initial={ return self.update_form(initial={
'item_pk': index, 'item_index': index,
'quantity': self.quantity 'quantity': self.quantity
}) })
@ -56,144 +103,133 @@ class CartItem:
def total_weight(self): def total_weight(self):
return Weight(lb=self.variant.weight.lb * self.quantity) return Weight(lb=self.variant.weight.lb * self.quantity)
def __iter__(self):
yield ('name', str(self.variant))
yield ('description', self.variant.product.subtitle)
yield ('unit_amount', {
'currency_code': settings.DEFAULT_CURRENCY,
'value': f'{self.variant.price}',
})
yield ('quantity', f'{item["quantity"]}')
def __str__(self):
return f'{self.variant} [{self.quantity} × ${self.price}]'
class Cart: class Cart:
item_class = CartItem item_class = CartItem
items = []
coupon = None
request = None
site_settings = None
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
self.session = request.session self.session = request.session
self.site_settings = SiteSettings.load() self.site_settings = SiteSettings.load()
self.coupon_code = self.session.get('coupon_code') if self.session.get('cart'):
cart = self.session.get(settings.CART_SESSION_ID) self.deserialize(self.session.get('cart'))
if not cart:
cart = self.session[settings.CART_SESSION_ID] = []
self.cart = cart
def add(self, request, item, update_quantity=False):
if update_quantity:
self.cart[item['variant']]['quantity'] = item['quantity']
else:
self.add_or_update_item(item)
# TODO: abstract this to a function that will check the max amount of item in the cart
if self.check_max_cart_quantity(request) and self.check_max_shipping_weight(request):
self.check_item_stock_quantities(request)
self.save()
def add_or_update_item(self, new_item):
new_item_pk = int(new_item['variant'])
for item in self:
if new_item_pk == item['variant'].pk:
if new_item['options'] == item['options']:
item['quantity'] += new_item['quantity']
return
else:
continue
self.cart.append(new_item)
def save(self):
self.session[settings.CART_SESSION_ID] = self.cart
self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n')
def check_item_stock_quantities(self, request):
for item in self:
if item['variant'].track_inventory:
if item['quantity'] > item['variant'].stock:
if item['quantity'] > item['variant'].product.checkout_limit:
messages.warning(request, 'Quantity exceeds checkout limit.')
item['quantity'] = item['variant'].product.checkout_limit
continue
messages.warning(request, 'Quantity exceeds available stock.')
item['quantity'] = item['variant'].stock
elif item['quantity'] > item['variant'].product.checkout_limit:
messages.warning(request, 'Quantity exceeds checkout limit.')
item['quantity'] = item['variant'].product.checkout_limit
self.save()
def check_max_cart_quantity(self, request):
if len(self) > self.site_settings.max_cart_quantity:
messages.warning(request, 'Cart is full: 20 items or less.')
return False
return True
def check_max_shipping_weight(self, request):
if self.get_total_weight() > ShippingProvider.USPS_MAX_SHIPPING_WEIGHT.lb:
messages.warning(request, 'Weight exceeds shipping limit')
return False
return True
def remove(self, pk):
self.cart.pop(pk)
self.save() self.save()
def __iter__(self): def __iter__(self):
for item in self.cart: for item in self.items:
pk = item['variant'].pk if isinstance(item['variant'], ProductVariant) else item['variant']
item['variant'] = ProductVariant.objects.get(pk=pk)
item['price_total'] = item['variant'].price * item['quantity']
yield item yield item
def __len__(self): def __len__(self):
return sum([item['quantity'] for item in self.cart]) return sum([item.quantity for item in self])
def get_item_prices_for_category(self, category): def serialize(self):
return \
{
'coupon_code': self.coupon.code if self.coupon is not None else None,
'items': [item.serialize() for item in self]
}
def deserialize(self, data):
# Transform old cart
if type(data) is list:
return
try:
self.coupon = Coupon.objects.get(code=data.get('coupon_code'))
except Coupon.DoesNotExist:
self.coupon = None
self.items = [self.item_class(item) for item in data['items'] if item is not None]
def save(self):
if self.validate():
self.session['cart'] = self.serialize()
self.session.modified = True
def validate(self):
self.check_item_checkout_limit_and_stock()
if self.check_max_cart_quantity() and self.check_max_shipping_weight():
return True
return False
def check_item_checkout_limit_and_stock(self):
for item in self: for item in self:
if item['variant'].product.category == category: if item.variant.track_inventory:
yield item['price_total'] if item.quantity > item.variant.stock:
else: if item.quantity > item.variant.product.checkout_limit:
continue messages.warning(self.request, 'Quantity exceeds checkout limit.')
item.quantity = item.variant.product.checkout_limit
continue
messages.warning(self.request, 'Quantity exceeds available stock.')
item.quantity = item.variant.stock
elif item.quantity > item.variant.product.checkout_limit:
messages.warning(self.request, 'Quantity exceeds checkout limit.')
item.quantity = item.variant.product.checkout_limit
def get_total_price_for_category(self, category): def check_max_cart_quantity(self):
return sum(self.get_item_prices_for_category(category)) if len(self) > self.site_settings.max_cart_quantity:
messages.warning(self.request, 'Cart is full: 20 items or less.')
return False
return True
def get_all_item_quantities(self): def check_max_shipping_weight(self):
for item in self.cart: if self.total_weight > self.site_settings.max_cart_weight:
yield item['quantity'] messages.warning(self.request, 'Weight exceeds shipping limit')
return False
return True
def get_single_item_total_quantity(self, item): def clear(self):
return sum([value['quantity'] for value in item['variations'].values()]) del self.session['cart']
self.session.modified = True
def get_item_prices(self): def add_item(self, new_item):
for item in self: for item in self:
yield item['price_total'] if new_item.variant == item.variant:
if new_item.options == item.options:
item.quantity += new_item.quantity
self.save()
return
else:
continue
self.items.append(new_item)
self.save()
def get_total_price(self): def update_item_quantity(self, item_index, quantity):
return sum(self.get_item_prices()) self.items[item_index].quantity = quantity
self.save()
def get_weight_for_all_items(self): def remove_item(self, index):
self.items.pop(index)
self.save()
def add_coupon(self, coupon):
# TODO: Apply coupon validity checks
self.coupon = coupon
self.save()
def remove_coupon(self):
if self.coupon is not None:
del self.coupon
self.save()
def get_total_price_for_coupon_items(self):
for item in self: for item in self:
yield round(item['variant'].weight.value * item['quantity'], 3) if item.variant.product in self.coupon.products.all():
yield item.total_price
def get_total_weight(self):
if len(self) > 0:
return sum(self.get_weight_for_all_items())
else:
return 0
def get_shipping_container_choices(self): def get_shipping_container_choices(self):
is_selectable = Q( is_selectable = Q(
is_selectable=True is_selectable=True
) )
min_weight_matched = Q( min_weight_matched = Q(
min_order_weight__lte=self.get_total_weight()) | Q( min_order_weight__lte=self.total_weight) | Q(
min_order_weight__isnull=True min_order_weight__isnull=True
) )
max_weight_matched = Q( max_weight_matched = Q(
max_order_weight__gte=self.get_total_weight()) | Q( max_order_weight__gte=self.total_weight) | Q(
max_order_weight__isnull=True max_order_weight__isnull=True
) )
containers = ShippingRate.objects.filter( containers = ShippingRate.objects.filter(
@ -201,24 +237,27 @@ class Cart:
) )
return containers return containers
def get_shipping_cost(self, container=None): def get_shipping_container(self):
# free_shipping_min = self.site_settings.free_shipping_min possible_containers = self.get_shipping_container_choices()
# if free_shipping_min is not None: if len(possible_containers) == 0:
# category = ProductCategory.objects.get(name='Coffee') return self.site_settings.default_shipping_rate.container
# if self.get_total_price_for_category(category) >= free_shipping_min: return possible_containers[0].container
# return Decimal('0.00')
def get_shipping_price(self, container=None):
if container is None: if container is None:
container = self.session.get('shipping_container').container container = self.get_shipping_container()
if not self.total_weight > Weight(lb=0):
return Decimal('0.00')
if len(self) > 0 and self.session.get('shipping_address'): if len(self) > 0 and self.session.get('shipping_address'):
usps_rate_request = build_usps_rate_request( usps_rate_request = build_usps_rate_request(
str(self.get_total_weight()), str(self.total_weight.lb),
container, container,
str(self.session.get('shipping_address')['postal_code']) str(self.session.get('shipping_address')['postal_code'])
) )
usps = USPSApi(settings.USPS_USER_ID, test=True) usps = USPSApi(settings.USPS_USER_ID, test=settings.DEBUG)
try: try:
validation = usps.get_rate(usps_rate_request) validation = usps.get_rate(usps_rate_request)
@ -248,98 +287,72 @@ class Cart:
'Could not retrieve shipping address.' 'Could not retrieve shipping address.'
) )
def clear(self): def create_order(self):
del self.session[settings.CART_SESSION_ID] params = self.build_order_params()
try:
del self.session['coupon_code']
except KeyError:
pass
self.session.modified = True
def build_order_params(self, container=None):
return \
{
'items': self,
'total_price': f'{self.get_total_price_after_discount()}',
'item_total': f'{self.get_total_price()}',
'discount': f'{self.get_discount()}',
'shipping_price': f'{self.get_shipping_cost()}',
'tax_total': '0',
'shipping_method': 'US POSTAL SERVICE ' + (
container if container else ''
),
'shipping_address': self.build_shipping_address(
self.session.get('shipping_address')
),
}
def create_order(self, container=None):
params = self.build_order_params(container)
logger.info(f'\nParams: {params}\n') logger.info(f'\nParams: {params}\n')
if settings.DEBUG: response = CreateOrder().create_order(params, debug=settings.DEBUG)
response = CreateOrder().create_order(params, debug=True)
else:
response = CreateOrder().create_order(params)
return response return response
def get_line_options(self, options_dict): def get_address_as_dict(self, address=None):
options = '' if address is None:
for key, value in options_dict.items(): address = self.session.get('shipping_address')
options += f'{key}: {value}; '
return options
def build_bulk_list(self, order):
bulk_list = []
for item in self:
bulk_list.append(OrderLine(
order=order,
variant=item['variant'],
customer_note=self.get_line_options(item['options']),
unit_price=item['variant'].price,
quantity=item['quantity']
))
return bulk_list
def build_shipping_address(self, address):
return \ return \
{ {
'address_line_1': f'{address["street_address_1"]}', 'address_line_1': address.get('street_address_1', ''),
'address_line_2': f'{address["street_address_2"]}', 'address_line_2': address.get('street_address_2', ''),
'admin_area_2': f'{address["city"]}', 'admin_area_2': address.get('city', ''),
'admin_area_1': f'{address["state"]}', 'admin_area_1': address.get('state', ''),
'postal_code': f'{address["postal_code"]}', 'postal_code': address.get('postal_code', ''),
'country_code': 'US' 'country_code': 'US'
} }
def build_order_params(self):
return \
{
'items': self.items,
'total_price': self.total_price,
'item_total': self.subtotal_price,
'discount': self.discount_amount,
'shipping_price': self.get_shipping_price(),
'tax_total': '0.00',
'shipping_method': 'US POSTAL SERVICE',
'shipping_address': self.get_address_as_dict(),
}
def build_bulk_list(self, order):
return [item.as_order_line(order) for item in self]
@property @property
def coupon(self): def item_variant_pks(self):
if self.coupon_code: return [item.variant.pk for item in self]
return Coupon.objects.get(code=self.coupon_code)
return None
def get_coupon_total_for_specific_products(self): @property
for item in self.cart: def subtotal_price(self):
if item['variant'].product in self.coupon.products.all(): return sum([item.total_price for item in self])
yield item['price_total']
def get_discount(self): @property
# SHIPPING def discount_amount(self):
# ENTIRE_ORDER if self.coupon is not None:
# SPECIFIC_PRODUCT
if self.coupon:
if self.coupon.discount_value_type == DiscountValueType.FIXED: if self.coupon.discount_value_type == DiscountValueType.FIXED:
return round(self.coupon.discount_value, 2) return self.coupon.discount_value
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
if self.coupon.type == VoucherType.ENTIRE_ORDER: if self.coupon.type == VoucherType.ENTIRE_ORDER:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2) return round((self.coupon.discount_value / Decimal('100.00')) * self.subtotal_price, 2)
elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT: elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT:
# Get the product in cart quantity # Get the product in cart quantity
total = sum(self.get_coupon_total_for_specific_products()) total_price = sum(self.get_total_price_for_coupon_items())
return round((self.coupon.discount_value / Decimal('100')) * total, 2) return round((self.coupon.discount_value / Decimal('100.00')) * total_price, 2)
return Decimal('0') return Decimal('0.00')
def get_subtotal_price_after_discount(self): @property
return round(self.get_total_price() - self.get_discount(), 2) def subtotal_price_after_discount(self):
return self.subtotal_price - self.discount_amount
def get_total_price_after_discount(self): @property
return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2) def total_price(self):
return self.subtotal_price - self.discount_amount + self.get_shipping_price()
@property
def total_weight(self):
return Weight(lb=sum([item.total_weight.lb for item in self]))

View File

@ -34,14 +34,9 @@ class AddToCartForm(forms.Form):
) )
class UpdateCartItemForm(forms.Form): class CartItemUpdateForm(forms.Form):
item_pk = forms.IntegerField(widget=forms.HiddenInput()) item_index = forms.IntegerField(widget=forms.HiddenInput())
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
update = forms.BooleanField(
required=False,
initial=True,
widget=forms.HiddenInput()
)
class AddToSubscriptionForm(forms.Form): class AddToSubscriptionForm(forms.Form):
@ -130,20 +125,9 @@ class CheckoutShippingForm(forms.Form):
class OrderCreateForm(forms.ModelForm): class OrderCreateForm(forms.ModelForm):
email = forms.CharField(widget=forms.HiddenInput())
first_name = forms.CharField(widget=forms.HiddenInput())
last_name = forms.CharField(widget=forms.HiddenInput())
class Meta: class Meta:
model = Order model = Order
fields = ( fields = []
'total_amount',
'shipping_total',
)
widgets = {
'total_amount': forms.HiddenInput(),
'shipping_total': forms.HiddenInput()
}
class CouponApplyForm(forms.Form): class CouponApplyForm(forms.Form):

View File

@ -90,24 +90,6 @@ class CreateOrder(PayPalClient):
def build_request_body(self, params): def build_request_body(self, params):
"""Method to create body with CAPTURE intent""" """Method to create body with CAPTURE intent"""
processed_items = [
{
# Shows within upper-right dropdown during payment approval
"name": f"{item['variant']} " + "; ".join(
f"{key}: {value}" for key, value in item["options"].items()
),
# Item details will also be in the completed paypal.com
# transaction view
"description": item["variant"].product.subtitle,
"unit_amount": {
"currency_code": settings.DEFAULT_CURRENCY,
"value": f"{item['variant'].price}",
},
"quantity": f"{item['quantity']}",
}
for item in params["items"]
]
request_body = { request_body = {
"intent": "CAPTURE", "intent": "CAPTURE",
"application_context": { "application_context": {
@ -126,15 +108,15 @@ class CreateOrder(PayPalClient):
# "soft_descriptor": "HighFashions", # "soft_descriptor": "HighFashions",
"amount": { "amount": {
"currency_code": "USD", "currency_code": "USD",
"value": params["total_price"], "value": str(params["total_price"]),
"breakdown": { "breakdown": {
"item_total": { "item_total": {
"currency_code": "USD", "currency_code": "USD",
"value": params["item_total"], "value": str(params["item_total"]),
}, },
"shipping": { "shipping": {
"currency_code": "USD", "currency_code": "USD",
"value": params["shipping_price"], "value": str(params["shipping_price"]),
}, },
"tax_total": { "tax_total": {
"currency_code": "USD", "currency_code": "USD",
@ -142,11 +124,11 @@ class CreateOrder(PayPalClient):
}, },
"discount": { "discount": {
"currency_code": "USD", "currency_code": "USD",
"value": params["discount"], "value": str(params["discount"]),
}, },
}, },
}, },
"items": processed_items, "items": [dict(item) for item in params['items']],
"shipping": { "shipping": {
"method": params["shipping_method"], "method": params["shipping_method"],
"address": params["shipping_address"], "address": params["shipping_address"],

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load grind_filter %} {% load initialize_update_form %}
{% block head_title %}Cart | {% endblock %} {% block head_title %}Cart | {% endblock %}
@ -25,9 +25,9 @@
{% for key, value in item.options.items %} {% for key, value in item.options.items %}
<p><strong>{{ key }}</strong>: {{ value }}</p> <p><strong>{{ key }}</strong>: {{ value }}</p>
{% endfor %} {% endfor %}
<form class="item__form" action="{% url 'storefront:cart-update' product.pk %}" method="POST"> <form class="item__form" action="{% url 'storefront:cart-detail' %}" method="POST">
{% csrf_token %} {% csrf_token %}
{{ item.update_quantity_form }} {{ item|initialize_update_form:forloop.counter0 }}
<input type="submit" value="Update"> <input type="submit" value="Update">
</form> </form>
<p><a href="{% url 'storefront:cart-remove' forloop.counter0 %}">Remove item</a></p> <p><a href="{% url 'storefront:cart-remove' forloop.counter0 %}">Remove item</a></p>
@ -62,7 +62,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.subtotal_price }}</td>
</tr> </tr>
{% if cart.coupon and cart.coupon.type == 'entire_order' %} {% if cart.coupon and cart.coupon.type == 'entire_order' %}
<tr> <tr>
@ -72,7 +72,7 @@
{% endif %} {% endif %}
<tr> <tr>
<th>Total</th> <th>Total</th>
<td><strong>${{cart.get_subtotal_price_after_discount|floatformat:"2"}}</strong></td> <td><strong>${{cart.subtotal_price_after_discount}}</strong></td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -1,6 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load grind_filter %}
{% block head_title %}Checkout | {% endblock %} {% block head_title %}Checkout | {% endblock %}
@ -69,7 +68,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.subtotal_price }}</td>
</tr> </tr>
{% if cart.coupon and cart.coupon.type == 'entire_order' %} {% if cart.coupon and cart.coupon.type == 'entire_order' %}
<tr> <tr>
@ -79,11 +78,11 @@
{% endif %} {% endif %}
<tr> <tr>
<td>Shipping</td> <td>Shipping</td>
<td>${{ cart.get_shipping_cost }}</td> <td>${{ cart.get_shipping_price }}</td>
</tr> </tr>
<tr> <tr>
<th>Total</th> <th>Total</th>
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td> <td><strong>${{ cart.total_price }}</strong></td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -0,0 +1,9 @@
from django import template
from storefront.cart import CartItem
register = template.Library()
@register.filter
def initialize_update_form(item, index):
return item.get_update_form(index)

View File

@ -76,9 +76,9 @@ class CartItemTest(TestCase):
def test_calculates_total_weight(self): def test_calculates_total_weight(self):
cart_item = CartItem({ cart_item = CartItem({
'options': {'Grind': 'Whole Beans'}, 'variant_pk': self.variant_1.pk,
'quantity': 14, 'quantity': 14,
'variant': self.variant_1 'options': {'Grind': 'Whole Beans'}
}) })
self.assertEqual( self.assertEqual(
cart_item.total_price, cart_item.total_price,
@ -87,9 +87,9 @@ class CartItemTest(TestCase):
def test_calculates_total_price(self): def test_calculates_total_price(self):
cart_item = CartItem({ cart_item = CartItem({
'options': {'Grind': 'Whole Beans'}, 'variant_pk': self.variant_1.pk,
'quantity': 14, 'quantity': 14,
'variant': self.variant_1 'options': {'Grind': 'Whole Beans'}
}) })
self.assertEqual( self.assertEqual(
cart_item.total_weight, cart_item.total_weight,
@ -98,6 +98,8 @@ class CartItemTest(TestCase):
class CartTest(TestCase): class CartTest(TestCase):
fixtures = ['site_settings_and_shipping_rates.json']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.customer = User.objects.create_user( cls.customer = User.objects.create_user(
@ -116,20 +118,48 @@ class CartTest(TestCase):
product=cls.product, product=cls.product,
name='16 oz', name='16 oz',
sku='234987', sku='234987',
price=13.4, price=Decimal('13.40'),
weight=Weight(oz=16), weight=Weight(oz=16),
) )
cls.order = Order.objects.create( cls.order = Order.objects.create(
customer=cls.customer, customer=cls.customer,
total_amount=13.4 total_amount=Decimal('13.40')
) )
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
self.client.force_login(self.customer) self.client.force_login(self.customer)
self.client.session['shipping_address'] = {
def test_cart_item_variations(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url, follow=True)
logger.debug(response.context['messages'])
request = response.wsgi_request
cart = Cart(request)
cart.add_item(
CartItem({
'variant_pk': self.variant.pk,
'quantity': 1,
'options': {'Grind': 'Whole Beans'}
})
)
cart.add_item(
CartItem({
'variant_pk': self.variant.pk,
'quantity': 1,
'options': {'Grind': 'Espresso'}
})
)
for item in cart:
self.assertTrue(hasattr(item, 'variant'))
def test_add_item_to_cart(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
request.session['shipping_address'] = {
'first_name': 'Nathan', 'first_name': 'Nathan',
'last_name': 'Chapman', 'last_name': 'Chapman',
'email': 'contact@nathanjchapman.com', 'email': 'contact@nathanjchapman.com',
@ -139,89 +169,53 @@ class CartTest(TestCase):
'state': 'UT', 'state': 'UT',
'postal_code': '84341' 'postal_code': '84341'
} }
def test_cart_item_variations(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url, follow=True)
logger.debug(response.context['messages'])
request = response.wsgi_request
cart = Cart(request)
cart.add(
request,
item={
'options': {'Grind': 'Whole Beans'},
'price_total': 13.4,
'quantity': 1,
'variant': self.variant.pk
}
)
cart.add(
request,
item={
'options': {'Grind': 'Espresso'},
'price_total': 13.4,
'quantity': 1,
'variant': self.variant.pk
}
)
for item in cart.cart:
self.assertTrue('variant' in item, item)
def test_add_item_to_cart(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart = Cart(request) cart = Cart(request)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Whole Beans'},
'price_total': 13.4,
'quantity': 1, 'quantity': 1,
'variant': self.variant.pk 'options': {'Grind': 'Whole Beans'}
} })
) )
self.assertEqual( self.assertEqual(
cart.cart[0]['quantity'], cart.items[0].quantity,
1 1
) )
self.assertEqual(len(cart), 1) self.assertEqual(len(cart), 1)
self.assertEqual(sum(cart.get_item_prices()), Decimal('13.4')) self.assertEqual(cart.total_weight, Weight(lb=1))
self.assertEqual(cart.get_total_price(), Decimal('13.4')) self.assertEqual(cart.subtotal_price, Decimal('13.40'))
cart.add( self.assertEqual(cart.total_price, cart.get_shipping_price() + Decimal('13.40'))
request, cart.add_item(
item={ CartItem({
'options': {'Grind': 'Whole Beans'}, 'variant_pk': self.variant.pk,
'price_total': 13.4,
'quantity': 1, 'quantity': 1,
'variant': self.variant.pk 'options': {'Grind': 'Whole Beans'}
} })
) )
self.assertEqual( self.assertEqual(
cart.cart[0]['quantity'], cart.items[0].quantity,
2 2
) )
self.assertEqual(len(cart), 2) self.assertEqual(len(cart), 2)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Espresso'},
'price_total': 40.2,
'quantity': 3, 'quantity': 3,
'variant': self.variant.pk 'options': {'Grind': 'Espresso'}
} })
) )
self.assertEqual( self.assertEqual(
cart.cart[1]['quantity'], cart.items[1].quantity,
3 3
) )
self.assertEqual(len(cart), 5) self.assertEqual(len(cart), 5)
self.assertEqual(cart.get_total_price(), Decimal('67')) self.assertEqual(cart.total_weight, Weight(lb=5))
self.assertEqual(cart.subtotal_price, Decimal('67.00'))
self.assertEqual(cart.total_price, cart.get_shipping_price() + Decimal('67.00'))
def test_update_cart_item_quantity(self): def test_update_cart_item_quantity(self):
cart_detail_url = reverse('storefront:cart-detail') cart_detail_url = reverse('storefront:cart-detail')
@ -230,32 +224,21 @@ class CartTest(TestCase):
cart = Cart(request) cart = Cart(request)
cart = Cart(request) cart = Cart(request)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Whole Beans'},
'price_total': 40.2,
'quantity': 3, 'quantity': 3,
'variant': self.variant.pk 'options': {'Grind': 'Whole Beans'}
} })
) )
self.assertEqual( self.assertEqual(
cart.cart[0]['quantity'], cart.items[0].quantity,
3 3
) )
cart.add( cart.update_item_quantity(0, 1)
request,
item={
'options': {'Grind': 'Whole Beans'},
'price_total': 13.4,
'quantity': 1,
'variant': 0
},
update_quantity=True
)
self.assertEqual( self.assertEqual(
cart.cart[0]['quantity'], cart.items[0].quantity,
1 1
) )
@ -266,33 +249,29 @@ class CartTest(TestCase):
cart = Cart(request) cart = Cart(request)
cart = Cart(request) cart = Cart(request)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Whole Beans'},
'price_total': 40.2,
'quantity': 3, 'quantity': 3,
'variant': self.variant.pk 'options': {'Grind': 'Whole Beans'}
} })
) )
self.assertEqual(len(cart), 3) self.assertEqual(len(cart), 3)
cart.remove(0) cart.remove_item(0)
self.assertEqual(len(cart), 0) self.assertEqual(len(cart), 0)
def test_cart_get_total_weight(self): def test_cart_total_weight(self):
cart_detail_url = reverse('storefront:cart-detail') cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url) response = self.client.get(cart_detail_url)
request = response.wsgi_request request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart = Cart(request) cart = Cart(request)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Whole Beans'},
'price_total': 40.2,
'quantity': 3, 'quantity': 3,
'variant': self.variant.pk 'options': {'Grind': 'Whole Beans'}
} })
) )
self.assertEqual(cart.get_total_weight(), 3) self.assertEqual(cart.total_weight, Weight(lb=3))

View File

@ -13,7 +13,7 @@ from accounts.models import User, Address
from core.models import Product, ProductVariant, Order from core.models import Product, ProductVariant, Order
from core import CoffeeGrind from core import CoffeeGrind
from storefront.views import OrderCreateView from storefront.views import OrderCreateView
from storefront.cart import Cart from storefront.cart import CartItem, Cart
from storefront.payments import CreateOrder from storefront.payments import CreateOrder
from . import RequestFaker from . import RequestFaker
@ -48,23 +48,19 @@ class CreateOrderTest(TestCase):
request = response.wsgi_request request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Whole Beans'}, 'options': {'Grind': 'Whole Beans'},
'price_total': '13.40', 'quantity': 1
'quantity': 1, })
'variant': self.variant.pk
}
) )
cart.add( cart.add_item(
request, CartItem({
item={ 'variant_pk': self.variant.pk,
'options': {'Grind': 'Percolator'}, 'options': {'Grind': 'Percolator'},
'price_total': '26.80', 'quantity': 2
'quantity': 2, })
'variant': self.variant.pk
}
) )
params = { params = {
'items': cart, 'items': cart,

View File

@ -13,7 +13,7 @@ from core.models import Product, ProductVariant, Order, Coupon
from core import CoffeeGrind from core import CoffeeGrind
from storefront.forms import AddressForm, OrderCreateForm from storefront.forms import AddressForm, OrderCreateForm
from storefront.views import ( from storefront.views import (
CartView, CartAddProductView, CartUpdateProductView, CouponApplyView, CartView, CartAddProductView, CartItemUpdateView, CouponApplyView,
ProductListView, ProductDetailView, ProductListView, ProductDetailView,
CheckoutAddressView, OrderCreateView, CheckoutAddressView, OrderCreateView,
paypal_order_transaction_capture, paypal_order_transaction_capture,

View File

@ -29,7 +29,7 @@ urlpatterns = [
), ),
path( path(
'cart/<int:pk>/update/', 'cart/<int:pk>/update/',
views.CartUpdateProductView.as_view(), views.CartItemUpdateView.as_view(),
name='cart-update', name='cart-update',
), ),
path( path(

View File

@ -44,30 +44,40 @@ from core.forms import ShippingRateForm
from core import OrderStatus, ShippingContainer from core import OrderStatus, ShippingContainer
from .forms import ( from .forms import (
AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddToCartForm, CartItemUpdateForm, OrderCreateForm,
AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm, AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm,
SubscriptionCreateForm SubscriptionCreateForm
) )
from .cart import Cart from .cart import CartItem, Cart
from .payments import CaptureOrder from .payments import CaptureOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CartView(TemplateView): class CartView(FormView):
template_name = 'storefront/cart_detail.html' template_name = 'storefront/cart_detail.html'
form_class = CartItemUpdateForm
def get_success_url(self):
return reverse('storefront:cart-detail')
def post(self, request, *args, **kwargs):
cart = Cart(request)
form = self.get_form()
if form.is_valid():
cart.update_item_quantity(
form.cleaned_data['item_index'],
form.cleaned_data['quantity']
)
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
cart = Cart(self.request)
for i, item in enumerate(cart):
item['update_quantity_form'] = UpdateCartItemForm(
initial={
'item_pk': i,
'quantity': item['quantity']
}
)
context['cart'] = cart
context['coupon_apply_form'] = CouponApplyForm() context['coupon_apply_form'] = CouponApplyForm()
return context return context
@ -97,22 +107,20 @@ class CartAddProductView(SingleObjectMixin, FormView):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cleaned_data = form.cleaned_data cleaned_data = form.cleaned_data
cart.add( cart.add_item(
request=request, CartItem({
item={ 'variant_pk': cleaned_data.pop('variant'),
'variant': cleaned_data.pop('variant'),
'quantity': cleaned_data.pop('quantity'), 'quantity': cleaned_data.pop('quantity'),
'options': cleaned_data 'options': cleaned_data
} })
) )
return self.form_valid(form) return self.form_valid(form)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
class CartUpdateProductView(SingleObjectMixin, FormView): class CartItemUpdateView(FormView):
model = Product form_class = CartItemUpdateForm
form_class = UpdateCartItemForm
http_method_names = ['post'] http_method_names = ['post']
def get_success_url(self): def get_success_url(self):
@ -122,13 +130,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
cart = Cart(request) cart = Cart(request)
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cart.add( cart.update_item_quantity(
request=request, form.cleaned_data['item_index'],
item={ form.cleaned_data['quantity']
'variant': form.cleaned_data['item_pk'],
'quantity': form.cleaned_data['quantity']
},
update_quantity=form.cleaned_data['update']
) )
return self.form_valid(form) return self.form_valid(form)
else: else:
@ -140,7 +144,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
def cart_remove_product_view(request, pk): def cart_remove_product_view(request, pk):
cart = Cart(request) cart = Cart(request)
cart.remove(pk) cart.remove_item(pk)
return redirect('storefront:cart-detail') return redirect('storefront:cart-detail')
@ -151,17 +155,16 @@ class CouponApplyView(FormView):
def form_valid(self, form): def form_valid(self, form):
today = timezone.localtime(timezone.now()).date() today = timezone.localtime(timezone.now()).date()
code = form.cleaned_data['code'].upper()
try: try:
coupon = Coupon.objects.get( coupon = Coupon.objects.get(code__iexact=form.cleaned_data['code'])
code__iexact=code, except Coupon.DoesNotExist:
valid_from__date__lte=today, messages(self.request, 'Coupon does not exist.')
valid_to__date__gte=today else:
)
if coupon.is_valid: if coupon.is_valid:
self.request.session['coupon_code'] = coupon.code cart = Cart(self.request)
except ObjectDoesNotExist: cart.add_coupon(coupon)
self.request.session['coupon_code'] = None else:
messages.warning(self.request, 'Coupon is invalid.')
return super().form_valid(form) return super().form_valid(form)
@ -226,7 +229,7 @@ class ProductDetailView(FormMixin, DetailView):
class CheckoutAddressView(FormView): class CheckoutAddressView(FormView):
template_name = 'storefront/checkout_address.html' template_name = 'storefront/checkout_address.html'
form_class = AddressForm form_class = AddressForm
success_url = reverse_lazy('storefront:checkout-shipping') success_url = reverse_lazy('storefront:order-create')
def get_initial(self): def get_initial(self):
user = self.request.user user = self.request.user
@ -284,7 +287,7 @@ class CheckoutShippingView(FormView):
def get_containers(self, request): def get_containers(self, request):
if self.containers is None: if self.containers is None:
cart = Cart(request) cart = Cart(request)
self.containers = cart.get_shipping_container_choices() self.containers = cart.get_shipping_container()
return self.containers return self.containers
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -297,20 +300,16 @@ class CheckoutShippingView(FormView):
cart = Cart(self.request) cart = Cart(self.request)
if len(self.get_containers(request)) == 0: if len(self.get_containers(request)) == 0:
self.request.session['shipping_container'] = site_settings.default_shipping_rate self.request.session['shipping_container'] = site_settings.default_shipping_rate
return HttpResponseRedirect( return HttpResponseRedirect(self.success_url)
reverse('storefront:order-create')
)
elif len(self.get_containers(request)) == 1: elif len(self.get_containers(request)) == 1:
self.request.session['shipping_container'] = self.get_containers(request)[0] self.request.session['shipping_container'] = self.get_containers(request)[0]
return HttpResponseRedirect( return HttpResponseRedirect(self.success_url)
reverse('storefront:order-create')
)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_form(self, form_class=None): def get_form(self, form_class=None):
cart = Cart(self.request) cart = Cart(self.request)
for container in self.get_containers(self.request): for container in self.get_containers(self.request):
container.s_cost = cart.get_shipping_cost(container.container) container.s_cost = cart.get_shipping_price(container.container)
if form_class is None: if form_class is None:
form_class = self.get_form_class() form_class = self.get_form_class()
return form_class(self.get_containers(self.request), **self.get_form_kwargs()) return form_class(self.get_containers(self.request), **self.get_form_kwargs())
@ -335,42 +334,21 @@ class OrderCreateView(CreateView):
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
elif self.request.session.get('coupon_code'):
address = self.request.session.get('shipping_address') cart = Cart(request)
coupon = Coupon.objects.get(
code=self.request.session.get('coupon_code') if cart.coupon is not None:
)
try: try:
user = User.objects.get(email=address['email']) user = User.objects.get(
except ObjectDoesNotExist: email=request.session.get('shipping_address').get('email')
)
except User.DoesNotExist:
user = None user = None
if user in coupon.users.all(): if user in cart.coupon.users.all():
del self.request.session['coupon_code'] cart.remove_coupon()
messages.warning(request, 'Coupon already used.') messages.warning(request, 'Coupon already used.')
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_initial(self):
cart = Cart(self.request)
shipping_container = self.request.session.get(
'shipping_container'
).container
shipping_cost = cart.get_shipping_cost(shipping_container)
initial = {
'total_amount': cart.get_total_price(),
'shipping_total': shipping_cost
}
if self.request.session.get('shipping_address'):
a = self.request.session.get('shipping_address')
user_info = {
'email': a['email'],
'first_name': a['first_name'],
'last_name': a['last_name']
}
initial |= user_info
return initial
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['shipping_address'] = self.request.session.get('shipping_address') context['shipping_address'] = self.request.session.get('shipping_address')
@ -379,19 +357,21 @@ class OrderCreateView(CreateView):
def form_valid(self, form): def form_valid(self, form):
cart = Cart(self.request) cart = Cart(self.request)
form.instance.subtotal_amount = cart.get_subtotal_price_after_discount() form.instance.subtotal_amount = cart.subtotal_price_after_discount
form.instance.coupon_amount = cart.get_discount() form.instance.coupon = cart.coupon
form.instance.total_amount = cart.get_total_price_after_discount() form.instance.coupon_amount = cart.discount_amount
form.instance.weight = cart.get_total_weight() form.instance.total_amount = cart.total_price
form.instance.weight = cart.total_weight
shipping_container = cart.get_shipping_container()
form.instance.shipping_total = cart.get_shipping_price(shipping_container)
shipping_address = self.request.session.get('shipping_address') shipping_address = self.request.session.get('shipping_address')
shipping_container = self.request.session.get('shipping_container').container
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 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(shipping_container) response = cart.create_order()
data = response.result.__dict__['_dict'] data = response.result.__dict__['_dict']
self.request.session['order_id'] = self.object.pk self.request.session['order_id'] = self.object.pk