2022-11-25 17:08:03 -07:00

346 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from decimal import Decimal
from measurement.measures import Weight
from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db.models import OuterRef, Q, Subquery
from django.core.exceptions import ValidationError
from core.models import (
ProductCategory, Product, ProductVariant, OrderLine, Coupon, ShippingRate,
SiteSettings
)
from core.usps import USPSApi
from core.exceptions import USPSPostageError, ShippingAddressError
from core import (
DiscountValueType,
VoucherType,
TransactionStatus,
OrderStatus,
ShippingService,
ShippingProvider,
ShippingContainer,
CoffeeGrind,
build_usps_rate_request
)
from .forms import UpdateCartItemForm
from .payments import CreateOrder
logger = logging.getLogger(__name__)
class CartItem:
update_form = UpdateCartItemForm
def __init__(self, item):
self.variant = item['variant']
self.quantity = item['quantity']
self.options = item['options']
def get_update_form(self, index):
return self.update_form(initial={
'item_pk': index,
'quantity': self.quantity
})
@property
def total_price(self):
return self.variant.price * self.quantity
@property
def total_weight(self):
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:
item_class = CartItem
def __init__(self, request):
self.request = request
self.session = request.session
self.site_settings = SiteSettings.load()
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] = []
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()
def __iter__(self):
for item in self.cart:
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
def __len__(self):
return sum([item['quantity'] for item in self.cart])
def get_item_prices_for_category(self, category):
for item in self:
if item['variant'].product.category == category:
yield item['price_total']
else:
continue
def get_total_price_for_category(self, category):
return sum(self.get_item_prices_for_category(category))
def get_all_item_quantities(self):
for item in self.cart:
yield item['quantity']
def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self):
for item in self:
yield item['price_total']
def get_total_price(self):
return sum(self.get_item_prices())
def get_weight_for_all_items(self):
for item in self:
yield round(item['variant'].weight.value * item['quantity'], 3)
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):
is_selectable = Q(
is_selectable=True
)
min_weight_matched = Q(
min_order_weight__lte=self.get_total_weight()) | Q(
min_order_weight__isnull=True
)
max_weight_matched = Q(
max_order_weight__gte=self.get_total_weight()) | Q(
max_order_weight__isnull=True
)
containers = ShippingRate.objects.filter(
is_selectable & min_weight_matched & max_weight_matched
)
return containers
def get_shipping_cost(self, container=None):
# free_shipping_min = self.site_settings.free_shipping_min
# if free_shipping_min is not None:
# category = ProductCategory.objects.get(name='Coffee')
# if self.get_total_price_for_category(category) >= free_shipping_min:
# return Decimal('0.00')
if container is None:
container = self.session.get('shipping_container').container
if len(self) > 0 and self.session.get('shipping_address'):
usps_rate_request = build_usps_rate_request(
str(self.get_total_weight()),
container,
str(self.session.get('shipping_address')['postal_code'])
)
usps = USPSApi(settings.USPS_USER_ID, test=True)
try:
validation = usps.get_rate(usps_rate_request)
except ConnectionError as e:
raise e(
'Could not connect to USPS, try again.'
)
logger.info(validation.result)
try:
postage = dict(
validation.result['RateV4Response']['Package']['Postage']
)
except KeyError:
raise USPSPostageError(
'Could not retrieve postage.'
)
if usps_rate_request['service'] == ShippingContainer.PRIORITY:
shipping_cost = Decimal(postage['Rate'])
elif usps_rate_request['service'] == ShippingContainer.PRIORITY_COMMERCIAL:
shipping_cost = Decimal(postage['CommercialRate'])
return shipping_cost
else:
raise ShippingAddressError(
'Could not retrieve shipping address.'
)
def clear(self):
del self.session[settings.CART_SESSION_ID]
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')
if settings.DEBUG:
response = CreateOrder().create_order(params, debug=True)
else:
response = CreateOrder().create_order(params)
return response
def get_line_options(self, options_dict):
options = ''
for key, value in options_dict.items():
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 \
{
'address_line_1': f'{address["street_address_1"]}',
'address_line_2': f'{address["street_address_2"]}',
'admin_area_2': f'{address["city"]}',
'admin_area_1': f'{address["state"]}',
'postal_code': f'{address["postal_code"]}',
'country_code': 'US'
}
@property
def coupon(self):
if self.coupon_code:
return Coupon.objects.get(code=self.coupon_code)
return None
def get_coupon_total_for_specific_products(self):
for item in self.cart:
if item['variant'].product in self.coupon.products.all():
yield item['price_total']
def get_discount(self):
# SHIPPING
# ENTIRE_ORDER
# SPECIFIC_PRODUCT
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:
if self.coupon.type == VoucherType.ENTIRE_ORDER:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT:
# Get the product in cart quantity
total = sum(self.get_coupon_total_for_specific_products())
return round((self.coupon.discount_value / Decimal('100')) * total, 2)
return Decimal('0')
def get_subtotal_price_after_discount(self):
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)