346 lines
12 KiB
Python
346 lines
12 KiB
Python
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)
|