diff --git a/src/accounts/models.py b/src/accounts/models.py index bdda95d..583ef01 100644 --- a/src/accounts/models.py +++ b/src/accounts/models.py @@ -25,6 +25,14 @@ class Address(models.Model): {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): addresses = models.ManyToManyField( diff --git a/src/accounts/utils.py b/src/accounts/utils.py index f1b24a9..de0c7e7 100644 --- a/src/accounts/utils.py +++ b/src/accounts/utils.py @@ -23,14 +23,14 @@ def get_or_create_customer(request, form, shipping_address): user.save() else: user, u_created = User.objects.get_or_create( - email=form.cleaned_data['email'].lower(), + email=shipping_address['email'].lower(), defaults={ - 'username': form.cleaned_data['email'].lower(), + 'username': shipping_address['email'].lower(), 'is_staff': False, 'is_active': True, 'is_superuser': False, - 'first_name': form.cleaned_data['first_name'], - 'last_name': form.cleaned_data['last_name'], + 'first_name': address.first_name, + 'last_name': address.last_name, 'default_shipping_address': address, } ) diff --git a/src/core/fixtures/shipping_rates.json b/src/core/fixtures/site_settings_and_shipping_rates.json similarity index 79% rename from src/core/fixtures/shipping_rates.json rename to src/core/fixtures/site_settings_and_shipping_rates.json index 21c2fc5..b4ec741 100644 --- a/src/core/fixtures/shipping_rates.json +++ b/src/core/fixtures/site_settings_and_shipping_rates.json @@ -15,6 +15,8 @@ "fields": { "usps_user_id": "012BETTE1249", "default_shipping_rate": 1, - "free_shipping_min": "100.00" + "free_shipping_min": "100.00", + "max_cart_quantity": 20, + "max_cart_weight": "20:lb" } }] diff --git a/src/core/migrations/0013_alter_shippingrate_max_order_weight_and_more.py b/src/core/migrations/0013_alter_shippingrate_max_order_weight_and_more.py new file mode 100644 index 0000000..2dad092 --- /dev/null +++ b/src/core/migrations/0013_alter_shippingrate_max_order_weight_and_more.py @@ -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), + ), + ] diff --git a/src/core/migrations/0014_sitesettings_max_cart_weight_and_more.py b/src/core/migrations/0014_sitesettings_max_cart_weight_and_more.py new file mode 100644 index 0000000..07ad37f --- /dev/null +++ b/src/core/migrations/0014_sitesettings_max_cart_weight_and_more.py @@ -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), + ), + ] diff --git a/src/core/migrations/0015_alter_order_coupon_amount.py b/src/core/migrations/0015_alter_order_coupon_amount.py new file mode 100644 index 0000000..65618d1 --- /dev/null +++ b/src/core/migrations/0015_alter_order_coupon_amount.py @@ -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), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index a5afb80..f6816f1 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -270,11 +270,17 @@ class ShippingRate(models.Model): choices=ShippingContainer.CHOICES, default=ShippingContainer.VARIABLE ) - min_order_weight = models.PositiveIntegerField( + min_order_weight = MeasurementField( + measurement=Weight, + unit_choices=WeightUnits.CHOICES, + default=zero_weight, blank=True, null=True ) - max_order_weight = models.PositiveIntegerField( + max_order_weight = MeasurementField( + measurement=Weight, + unit_choices=WeightUnits.CHOICES, + default=zero_weight, blank=True, null=True ) @@ -350,7 +356,11 @@ class Order(models.Model): decimal_places=2, 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( max_digits=5, decimal_places=2, @@ -514,10 +524,21 @@ class SiteSettings(SingletonBase): decimal_places=settings.DEFAULT_DECIMAL_PLACES, blank=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( - 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): diff --git a/src/core/weight.py b/src/core/weight.py index a3a6dc3..44dd55b 100644 --- a/src/core/weight.py +++ b/src/core/weight.py @@ -17,7 +17,7 @@ class WeightUnits: def zero_weight(): """Represent the zero weight value.""" - return Weight(kg=0) + return Weight(lb=0) def convert_weight(weight: Weight, unit: str) -> Weight: diff --git a/src/dashboard/templates/dashboard/config.html b/src/dashboard/templates/dashboard/config.html index 70bf292..1657b2d 100644 --- a/src/dashboard/templates/dashboard/config.html +++ b/src/dashboard/templates/dashboard/config.html @@ -33,6 +33,8 @@

USPS User ID: {{ site_settings.usps_user_id }}

Default shipping rate: {{ site_settings.default_shipping_rate }}

Free shipping min: ${{ site_settings.free_shipping_min }}

+

Max cart quantity: {{ site_settings.max_cart_quantity }} items

+

Max cart weight: {{ site_settings.max_cart_weight }}

diff --git a/src/dashboard/templates/dashboard/order_list.html b/src/dashboard/templates/dashboard/order_list.html index 7bd3bd1..05a2c2d 100644 --- a/src/dashboard/templates/dashboard/order_list.html +++ b/src/dashboard/templates/dashboard/order_list.html @@ -22,7 +22,7 @@
{{order.get_status_display}}
- ${{order.get_total_price_after_discount}} + ${{order.total_amount}} {% empty %} No orders diff --git a/src/dashboard/tests/test_views.py b/src/dashboard/tests/test_views.py index a0b06d1..feccc8f 100644 --- a/src/dashboard/tests/test_views.py +++ b/src/dashboard/tests/test_views.py @@ -66,7 +66,7 @@ logger = logging.getLogger(__name__) class ProductCreateViewTests(TestCase): fixtures = [ - 'shipping_rates.json', + 'site_settings_and_shipping_rates', 'accounts.json', 'coupons.json', 'products.json', @@ -101,7 +101,7 @@ class ProductCreateViewTests(TestCase): class OrderCancelViewTests(TestCase): fixtures = [ - 'shipping_rates.json', + 'site_settings_and_shipping_rates', 'accounts.json', 'coupons.json', 'products.json', diff --git a/src/functional_tests/test_address.py b/src/functional_tests/test_address.py index aa990c4..ba8c8cc 100644 --- a/src/functional_tests/test_address.py +++ b/src/functional_tests/test_address.py @@ -12,7 +12,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase class AddressTests(StaticLiveServerTestCase): - fixtures = ['shipping_rates.json', 'products.json'] + fixtures = ['site_settings_and_shipping_rates', 'products.json'] @classmethod def setUpClass(cls): diff --git a/src/functional_tests/test_coupon.py b/src/functional_tests/test_coupon.py index 4029d09..fdfdb88 100644 --- a/src/functional_tests/test_coupon.py +++ b/src/functional_tests/test_coupon.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) class CouponTests(StaticLiveServerTestCase): fixtures = [ - 'shipping_rates.json', 'products.json', 'accounts.json', 'coupons.json' + 'site_settings_and_shipping_rates', 'products.json', 'accounts.json', 'coupons.json' ] @classmethod diff --git a/src/ptcoffee/settings.py b/src/ptcoffee/settings.py index 39b2ebf..c1b1887 100644 --- a/src/ptcoffee/settings.py +++ b/src/ptcoffee/settings.py @@ -106,7 +106,7 @@ DATABASES = { CACHES = {'default': CACHE_CONFIG} SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' +# SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators @@ -179,6 +179,8 @@ STATICFILES_FINDERS = ( 'compressor.finders.CompressorFinder', ) +FILE_UPLOAD_MAX_MEMORY_SIZE = 62914560 + # Default primary key field type # 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_CURRENCY = 'USD' diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 6e9cc8f..d0e87c1 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -8,7 +8,7 @@ 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 django.core.exceptions import ValidationError, ObjectDoesNotExist from core.models import ( ProductCategory, Product, ProductVariant, OrderLine, Coupon, ShippingRate, @@ -28,23 +28,70 @@ from core import ( build_usps_rate_request ) -from .forms import UpdateCartItemForm +from .forms import CartItemUpdateForm from .payments import CreateOrder logger = logging.getLogger(__name__) class CartItem: - update_form = UpdateCartItemForm + update_form = CartItemUpdateForm + order_line_class = OrderLine + variant = None + quantity = None + options = None def __init__(self, item): - self.variant = item['variant'] - self.quantity = item['quantity'] - self.options = item['options'] + try: + self.variant = ProductVariant.objects.get(pk=item['variant_pk']) + 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): return self.update_form(initial={ - 'item_pk': index, + 'item_index': index, 'quantity': self.quantity }) @@ -56,144 +103,129 @@ class CartItem: 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 + items = [] + coupon = None + request = None + site_settings = None 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) + if self.session.get('cart'): + self.deserialize(self.session.get('cart')) 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'] + for item in self.items: yield item 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): + 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: - if item['variant'].product.category == category: - yield item['price_total'] - else: - continue + if item.variant.track_inventory: + if item.quantity > item.variant.stock: + if item.quantity > item.variant.product.checkout_limit: + 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): - return sum(self.get_item_prices_for_category(category)) + def check_max_cart_quantity(self): + 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): - for item in self.cart: - yield item['quantity'] + def check_max_shipping_weight(self): + if self.total_weight > self.site_settings.max_cart_weight: + messages.warning(self.request, 'Weight exceeds shipping limit') + return False + return True - def get_single_item_total_quantity(self, item): - return sum([value['quantity'] for value in item['variations'].values()]) + def clear(self): + del self.session['cart'] + self.session.modified = True - def get_item_prices(self): + def add_item(self, new_item): 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): - return sum(self.get_item_prices()) + def update_item_quantity(self, item_index, quantity): + 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: - 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 + if item.variant.product in self.coupon.products.all(): + yield item.total_price 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__lte=self.total_weight) | Q( min_order_weight__isnull=True ) 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 ) containers = ShippingRate.objects.filter( @@ -201,24 +233,27 @@ class Cart: ) 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') + def get_shipping_container(self): + possible_containers = self.get_shipping_container_choices() + if len(possible_containers) == 0: + return self.site_settings.default_shipping_rate.container + return possible_containers[0].container + def get_shipping_price(self, container=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'): usps_rate_request = build_usps_rate_request( - str(self.get_total_weight()), + str(self.total_weight.lb), container, 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: validation = usps.get_rate(usps_rate_request) @@ -248,98 +283,72 @@ class Cart: '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) + def create_order(self): + params = self.build_order_params() logger.info(f'\nParams: {params}\n') - if settings.DEBUG: - response = CreateOrder().create_order(params, debug=True) - else: - response = CreateOrder().create_order(params) + response = CreateOrder().create_order(params, debug=settings.DEBUG) return response - def get_line_options(self, options_dict): - options = '' - for key, value in options_dict.items(): - options += f'{key}: {value}; ' - return options + def get_address_as_dict(self, address=None): + if address is None: + address = self.session.get('shipping_address') - 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"]}', + 'address_line_1': address.get('street_address_1', ''), + 'address_line_2': address.get('street_address_2', ''), + 'admin_area_2': address.get('city', ''), + 'admin_area_1': address.get('state', ''), + 'postal_code': address.get('postal_code', ''), '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 - def coupon(self): - if self.coupon_code: - return Coupon.objects.get(code=self.coupon_code) - return None + def item_variant_pks(self): + return [item.variant.pk for item in self] - 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'] + @property + def subtotal_price(self): + return sum([item.total_price for item in self]) - def get_discount(self): - # SHIPPING - # ENTIRE_ORDER - # SPECIFIC_PRODUCT - if self.coupon: + @property + def discount_amount(self): + if self.coupon is not None: 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: 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: # 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') + total_price = sum(self.get_total_price_for_coupon_items()) + return round((self.coupon.discount_value / Decimal('100.00')) * total_price, 2) + return Decimal('0.00') - def get_subtotal_price_after_discount(self): - return round(self.get_total_price() - self.get_discount(), 2) + @property + def subtotal_price_after_discount(self): + return self.subtotal_price - self.discount_amount - def get_total_price_after_discount(self): - return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2) + @property + 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])) diff --git a/src/storefront/forms.py b/src/storefront/forms.py index fbe7a1c..94886b9 100644 --- a/src/storefront/forms.py +++ b/src/storefront/forms.py @@ -34,14 +34,9 @@ class AddToCartForm(forms.Form): ) -class UpdateCartItemForm(forms.Form): - item_pk = forms.IntegerField(widget=forms.HiddenInput()) +class CartItemUpdateForm(forms.Form): + item_index = forms.IntegerField(widget=forms.HiddenInput()) 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): @@ -130,20 +125,9 @@ class CheckoutShippingForm(forms.Form): 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: model = Order - fields = ( - 'total_amount', - 'shipping_total', - ) - widgets = { - 'total_amount': forms.HiddenInput(), - 'shipping_total': forms.HiddenInput() - } + fields = [] class CouponApplyForm(forms.Form): diff --git a/src/storefront/payments.py b/src/storefront/payments.py index 38e2e62..3dda13c 100644 --- a/src/storefront/payments.py +++ b/src/storefront/payments.py @@ -90,24 +90,6 @@ class CreateOrder(PayPalClient): def build_request_body(self, params): """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 = { "intent": "CAPTURE", "application_context": { @@ -126,15 +108,15 @@ class CreateOrder(PayPalClient): # "soft_descriptor": "HighFashions", "amount": { "currency_code": "USD", - "value": params["total_price"], + "value": str(params["total_price"]), "breakdown": { "item_total": { "currency_code": "USD", - "value": params["item_total"], + "value": str(params["item_total"]), }, "shipping": { "currency_code": "USD", - "value": params["shipping_price"], + "value": str(params["shipping_price"]), }, "tax_total": { "currency_code": "USD", @@ -142,11 +124,11 @@ class CreateOrder(PayPalClient): }, "discount": { "currency_code": "USD", - "value": params["discount"], + "value": str(params["discount"]), }, }, }, - "items": processed_items, + "items": [dict(item) for item in params['items']], "shipping": { "method": params["shipping_method"], "address": params["shipping_address"], diff --git a/src/storefront/templates/storefront/cart_detail.html b/src/storefront/templates/storefront/cart_detail.html index e2be281..c161130 100644 --- a/src/storefront/templates/storefront/cart_detail.html +++ b/src/storefront/templates/storefront/cart_detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load grind_filter %} +{% load initialize_update_form %} {% block head_title %}Cart | {% endblock %} @@ -25,9 +25,9 @@ {% for key, value in item.options.items %}

{{ key }}: {{ value }}

{% endfor %} -
+ {% csrf_token %} - {{ item.update_quantity_form }} + {{ item|initialize_update_form:forloop.counter0 }}

Remove item

@@ -62,7 +62,7 @@ - + {% if cart.coupon and cart.coupon.type == 'entire_order' %} @@ -72,7 +72,7 @@ {% endif %} - +
Subtotal${{ cart.get_total_price|floatformat:"2" }}${{ cart.subtotal_price }}
Total${{cart.get_subtotal_price_after_discount|floatformat:"2"}}${{cart.subtotal_price_after_discount}}
diff --git a/src/storefront/templates/storefront/order_form.html b/src/storefront/templates/storefront/order_form.html index 03d8c0e..7ea7b4f 100644 --- a/src/storefront/templates/storefront/order_form.html +++ b/src/storefront/templates/storefront/order_form.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static %} -{% load grind_filter %} {% block head_title %}Checkout | {% endblock %} @@ -69,7 +68,7 @@ - + {% if cart.coupon and cart.coupon.type == 'entire_order' %} @@ -79,11 +78,11 @@ {% endif %} - + - +
Subtotal${{cart.get_total_price|floatformat:"2"}}${{ cart.subtotal_price }}
Shipping${{ cart.get_shipping_cost }}${{ cart.get_shipping_price }}
Total${{cart.get_total_price_after_discount|floatformat:"2"}}${{ cart.total_price }}
diff --git a/src/storefront/templatetags/initialize_update_form.py b/src/storefront/templatetags/initialize_update_form.py new file mode 100644 index 0000000..cc19d3d --- /dev/null +++ b/src/storefront/templatetags/initialize_update_form.py @@ -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) diff --git a/src/storefront/tests/test_cart.py b/src/storefront/tests/test_cart.py index 5b8c94d..75979f2 100644 --- a/src/storefront/tests/test_cart.py +++ b/src/storefront/tests/test_cart.py @@ -76,9 +76,9 @@ class CartItemTest(TestCase): def test_calculates_total_weight(self): cart_item = CartItem({ - 'options': {'Grind': 'Whole Beans'}, + 'variant_pk': self.variant_1.pk, 'quantity': 14, - 'variant': self.variant_1 + 'options': {'Grind': 'Whole Beans'} }) self.assertEqual( cart_item.total_price, @@ -87,9 +87,9 @@ class CartItemTest(TestCase): def test_calculates_total_price(self): cart_item = CartItem({ - 'options': {'Grind': 'Whole Beans'}, + 'variant_pk': self.variant_1.pk, 'quantity': 14, - 'variant': self.variant_1 + 'options': {'Grind': 'Whole Beans'} }) self.assertEqual( cart_item.total_weight, @@ -98,6 +98,8 @@ class CartItemTest(TestCase): class CartTest(TestCase): + fixtures = ['site_settings_and_shipping_rates.json'] + @classmethod def setUpTestData(cls): cls.customer = User.objects.create_user( @@ -116,20 +118,48 @@ class CartTest(TestCase): product=cls.product, name='16 oz', sku='234987', - price=13.4, + price=Decimal('13.40'), weight=Weight(oz=16), ) cls.order = Order.objects.create( customer=cls.customer, - total_amount=13.4 + total_amount=Decimal('13.40') ) def setUp(self): self.client = Client() self.factory = RequestFactory() - 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', 'last_name': 'Chapman', 'email': 'contact@nathanjchapman.com', @@ -139,89 +169,53 @@ class CartTest(TestCase): 'state': 'UT', '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.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 13.4, + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'quantity': 1, - 'variant': self.variant.pk - } + 'options': {'Grind': 'Whole Beans'} + }) ) self.assertEqual( - cart.cart[0]['quantity'], + cart.items[0].quantity, 1 ) self.assertEqual(len(cart), 1) - self.assertEqual(sum(cart.get_item_prices()), Decimal('13.4')) - self.assertEqual(cart.get_total_price(), Decimal('13.4')) - cart.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 13.4, + self.assertEqual(cart.total_weight, Weight(lb=1)) + self.assertEqual(cart.subtotal_price, Decimal('13.40')) + self.assertEqual(cart.total_price, cart.get_shipping_price() + Decimal('13.40')) + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'quantity': 1, - 'variant': self.variant.pk - } + 'options': {'Grind': 'Whole Beans'} + }) ) self.assertEqual( - cart.cart[0]['quantity'], + cart.items[0].quantity, 2 ) self.assertEqual(len(cart), 2) - cart.add( - request, - item={ - 'options': {'Grind': 'Espresso'}, - 'price_total': 40.2, + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'quantity': 3, - 'variant': self.variant.pk - } + 'options': {'Grind': 'Espresso'} + }) ) self.assertEqual( - cart.cart[1]['quantity'], + cart.items[1].quantity, 3 ) 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): cart_detail_url = reverse('storefront:cart-detail') @@ -230,32 +224,21 @@ class CartTest(TestCase): cart = Cart(request) cart = Cart(request) - cart.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 40.2, + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'quantity': 3, - 'variant': self.variant.pk - } + 'options': {'Grind': 'Whole Beans'} + }) ) self.assertEqual( - cart.cart[0]['quantity'], + cart.items[0].quantity, 3 ) - cart.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 13.4, - 'quantity': 1, - 'variant': 0 - }, - update_quantity=True - ) + cart.update_item_quantity(0, 1) self.assertEqual( - cart.cart[0]['quantity'], + cart.items[0].quantity, 1 ) @@ -266,33 +249,29 @@ class CartTest(TestCase): cart = Cart(request) cart = Cart(request) - cart.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 40.2, + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'quantity': 3, - 'variant': self.variant.pk - } + 'options': {'Grind': 'Whole Beans'} + }) ) self.assertEqual(len(cart), 3) - cart.remove(0) + cart.remove_item(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') response = self.client.get(cart_detail_url) request = response.wsgi_request cart = Cart(request) cart = Cart(request) - cart.add( - request, - item={ - 'options': {'Grind': 'Whole Beans'}, - 'price_total': 40.2, + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, '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)) diff --git a/src/storefront/tests/test_payments.py b/src/storefront/tests/test_payments.py index 146c9fc..bfe95e8 100644 --- a/src/storefront/tests/test_payments.py +++ b/src/storefront/tests/test_payments.py @@ -13,7 +13,7 @@ from accounts.models import User, Address from core.models import Product, ProductVariant, Order from core import CoffeeGrind from storefront.views import OrderCreateView -from storefront.cart import Cart +from storefront.cart import CartItem, Cart from storefront.payments import CreateOrder from . import RequestFaker @@ -48,23 +48,19 @@ class CreateOrderTest(TestCase): request = response.wsgi_request cart = Cart(request) - cart.add( - request, - item={ + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'options': {'Grind': 'Whole Beans'}, - 'price_total': '13.40', - 'quantity': 1, - 'variant': self.variant.pk - } + 'quantity': 1 + }) ) - cart.add( - request, - item={ + cart.add_item( + CartItem({ + 'variant_pk': self.variant.pk, 'options': {'Grind': 'Percolator'}, - 'price_total': '26.80', - 'quantity': 2, - 'variant': self.variant.pk - } + 'quantity': 2 + }) ) params = { 'items': cart, diff --git a/src/storefront/tests/test_views.py b/src/storefront/tests/test_views.py index b1bd487..36a0b29 100644 --- a/src/storefront/tests/test_views.py +++ b/src/storefront/tests/test_views.py @@ -13,7 +13,7 @@ from core.models import Product, ProductVariant, Order, Coupon from core import CoffeeGrind from storefront.forms import AddressForm, OrderCreateForm from storefront.views import ( - CartView, CartAddProductView, CartUpdateProductView, CouponApplyView, + CartView, CartAddProductView, CartItemUpdateView, CouponApplyView, ProductListView, ProductDetailView, CheckoutAddressView, OrderCreateView, paypal_order_transaction_capture, diff --git a/src/storefront/urls.py b/src/storefront/urls.py index 845250b..5f626f1 100644 --- a/src/storefront/urls.py +++ b/src/storefront/urls.py @@ -29,7 +29,7 @@ urlpatterns = [ ), path( 'cart//update/', - views.CartUpdateProductView.as_view(), + views.CartItemUpdateView.as_view(), name='cart-update', ), path( diff --git a/src/storefront/views.py b/src/storefront/views.py index de544d8..8cc2ef4 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -44,30 +44,40 @@ from core.forms import ShippingRateForm from core import OrderStatus, ShippingContainer from .forms import ( - AddToCartForm, UpdateCartItemForm, OrderCreateForm, + AddToCartForm, CartItemUpdateForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm, SubscriptionCreateForm ) -from .cart import Cart +from .cart import CartItem, Cart from .payments import CaptureOrder logger = logging.getLogger(__name__) -class CartView(TemplateView): +class CartView(FormView): 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): 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() return context @@ -97,22 +107,20 @@ class CartAddProductView(SingleObjectMixin, FormView): form = self.get_form() if form.is_valid(): cleaned_data = form.cleaned_data - cart.add( - request=request, - item={ - 'variant': cleaned_data.pop('variant'), + cart.add_item( + CartItem({ + 'variant_pk': cleaned_data.pop('variant'), 'quantity': cleaned_data.pop('quantity'), 'options': cleaned_data - } + }) ) return self.form_valid(form) else: return self.form_invalid(form) -class CartUpdateProductView(SingleObjectMixin, FormView): - model = Product - form_class = UpdateCartItemForm +class CartItemUpdateView(FormView): + form_class = CartItemUpdateForm http_method_names = ['post'] def get_success_url(self): @@ -122,13 +130,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView): cart = Cart(request) form = self.get_form() if form.is_valid(): - cart.add( - request=request, - item={ - 'variant': form.cleaned_data['item_pk'], - 'quantity': form.cleaned_data['quantity'] - }, - update_quantity=form.cleaned_data['update'] + cart.update_item_quantity( + form.cleaned_data['item_index'], + form.cleaned_data['quantity'] ) return self.form_valid(form) else: @@ -140,7 +144,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView): def cart_remove_product_view(request, pk): cart = Cart(request) - cart.remove(pk) + cart.remove_item(pk) return redirect('storefront:cart-detail') @@ -151,17 +155,16 @@ class CouponApplyView(FormView): def form_valid(self, form): today = timezone.localtime(timezone.now()).date() - code = form.cleaned_data['code'].upper() try: - coupon = Coupon.objects.get( - code__iexact=code, - valid_from__date__lte=today, - valid_to__date__gte=today - ) + coupon = Coupon.objects.get(code__iexact=form.cleaned_data['code']) + except Coupon.DoesNotExist: + messages(self.request, 'Coupon does not exist.') + else: if coupon.is_valid: - self.request.session['coupon_code'] = coupon.code - except ObjectDoesNotExist: - self.request.session['coupon_code'] = None + cart = Cart(self.request) + cart.add_coupon(coupon) + else: + messages.warning(self.request, 'Coupon is invalid.') return super().form_valid(form) @@ -226,7 +229,7 @@ class ProductDetailView(FormMixin, DetailView): class CheckoutAddressView(FormView): template_name = 'storefront/checkout_address.html' form_class = AddressForm - success_url = reverse_lazy('storefront:checkout-shipping') + success_url = reverse_lazy('storefront:order-create') def get_initial(self): user = self.request.user @@ -284,7 +287,7 @@ class CheckoutShippingView(FormView): def get_containers(self, request): if self.containers is None: cart = Cart(request) - self.containers = cart.get_shipping_container_choices() + self.containers = cart.get_shipping_container() return self.containers def get(self, request, *args, **kwargs): @@ -297,20 +300,16 @@ class CheckoutShippingView(FormView): cart = Cart(self.request) if len(self.get_containers(request)) == 0: self.request.session['shipping_container'] = site_settings.default_shipping_rate - return HttpResponseRedirect( - reverse('storefront:order-create') - ) + return HttpResponseRedirect(self.success_url) elif len(self.get_containers(request)) == 1: self.request.session['shipping_container'] = self.get_containers(request)[0] - return HttpResponseRedirect( - reverse('storefront:order-create') - ) + return HttpResponseRedirect(self.success_url) return super().get(request, *args, **kwargs) def get_form(self, form_class=None): cart = Cart(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: form_class = self.get_form_class() return form_class(self.get_containers(self.request), **self.get_form_kwargs()) @@ -335,42 +334,21 @@ class OrderCreateView(CreateView): return HttpResponseRedirect( reverse('storefront:checkout-address') ) - elif self.request.session.get('coupon_code'): - address = self.request.session.get('shipping_address') - coupon = Coupon.objects.get( - code=self.request.session.get('coupon_code') - ) + + cart = Cart(request) + + if cart.coupon is not None: try: - user = User.objects.get(email=address['email']) - except ObjectDoesNotExist: + user = User.objects.get( + email=request.session.get('shipping_address').get('email') + ) + except User.DoesNotExist: user = None - if user in coupon.users.all(): - del self.request.session['coupon_code'] + if user in cart.coupon.users.all(): + cart.remove_coupon() messages.warning(request, 'Coupon already used.') 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): context = super().get_context_data(**kwargs) context['shipping_address'] = self.request.session.get('shipping_address') @@ -379,19 +357,21 @@ class OrderCreateView(CreateView): def form_valid(self, form): cart = Cart(self.request) - form.instance.subtotal_amount = cart.get_subtotal_price_after_discount() - form.instance.coupon_amount = cart.get_discount() - form.instance.total_amount = cart.get_total_price_after_discount() - form.instance.weight = cart.get_total_weight() + form.instance.subtotal_amount = cart.subtotal_price_after_discount + form.instance.coupon = cart.coupon + form.instance.coupon_amount = cart.discount_amount + 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_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.status = OrderStatus.DRAFT self.object = form.save() bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) - response = cart.create_order(shipping_container) + response = cart.create_order() data = response.result.__dict__['_dict'] self.request.session['order_id'] = self.object.pk