Add basic product variations and categories

This commit is contained in:
Nathan Chapman 2022-10-03 17:12:45 -06:00
parent 8dfce8e92b
commit 34b6eb6bfd
29 changed files with 487 additions and 345 deletions

View File

@ -18,7 +18,12 @@ class Address(models.Model):
postal_code = models.CharField(max_length=20, blank=True) postal_code = models.CharField(max_length=20, blank=True)
def __str__(self): def __str__(self):
return f'{self.street_address_1}{self.city}' return f"""
{first_name} {last_name}
{street_address_1}
{street_address_2}
{city}, {state}, {postal_code}
"""
class User(AbstractUser): class User(AbstractUser):

View File

@ -127,3 +127,20 @@ class CoffeeGrind:
(PERCOLATOR, 'Percolator'), (PERCOLATOR, 'Percolator'),
(CAFE_STYLE, 'BLTC cafe pour over') (CAFE_STYLE, 'BLTC cafe pour over')
] ]
def build_usps_rate_request(weight, container, zip_destination):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': zip_destination,
'pounds': '0',
'ounces': weight,
'container': container,
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}

View File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
SiteSettings,
ProductCategory, ProductCategory,
Product, Product,
ProductPhoto, ProductPhoto,
@ -13,6 +14,7 @@ from .models import (
OrderLine, OrderLine,
) )
admin.site.register(SiteSettings)
admin.site.register(ProductCategory) admin.site.register(ProductCategory)
admin.site.register(Product) admin.site.register(Product)
admin.site.register(ProductPhoto) admin.site.register(ProductPhoto)

View File

@ -10,7 +10,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "subtotal_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:18:59.584Z", "created_at": "2022-03-15T17:18:59.584Z",
"updated_at": "2022-03-15T17:18:59.584Z" "updated_at": "2022-03-15T17:18:59.584Z"
@ -26,7 +26,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "subtotal_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:22:18.440Z", "created_at": "2022-03-15T17:22:18.440Z",
"updated_at": "2022-03-15T17:22:18.440Z" "updated_at": "2022-03-15T17:22:18.440Z"
@ -42,7 +42,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "subtotal_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:26:27.869Z", "created_at": "2022-03-15T17:26:27.869Z",
"updated_at": "2022-03-15T17:26:27.869Z" "updated_at": "2022-03-15T17:26:27.869Z"

View File

@ -0,0 +1,38 @@
# Generated by Django 4.0.2 on 2022-10-01 18:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0012_alter_productvariant_product'),
]
operations = [
migrations.RenameField(
model_name='order',
old_name='total_net_amount',
new_name='subtotal_amount',
),
migrations.RemoveField(
model_name='orderline',
name='product',
),
migrations.AddField(
model_name='order',
name='coupon_amount',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='order',
name='total_amount',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
),
migrations.AddField(
model_name='orderline',
name='variant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.productvariant'),
),
]

View File

@ -24,7 +24,8 @@ from . import (
TransactionStatus, TransactionStatus,
OrderStatus, OrderStatus,
ShippingProvider, ShippingProvider,
ShippingContainer ShippingContainer,
build_usps_rate_request
) )
from .weight import WeightUnits, zero_weight from .weight import WeightUnits, zero_weight
@ -77,13 +78,6 @@ class ProductCategory(models.Model):
verbose_name_plural = 'Product Categories' verbose_name_plural = 'Product Categories'
class ProductManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
num_ordered=models.Sum('order_lines__quantity')
)
class Product(models.Model): class Product(models.Model):
category = models.ForeignKey( category = models.ForeignKey(
ProductCategory, ProductCategory,
@ -105,8 +99,6 @@ class Product(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
objects = ProductManager()
def __str__(self): def __str__(self):
return self.name return self.name
@ -126,6 +118,13 @@ class Product(models.Model):
ordering = ['sorting', 'name'] ordering = ['sorting', 'name']
class ProductVariantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
num_ordered=models.Sum('order_lines__quantity')
)
class ProductVariant(models.Model): class ProductVariant(models.Model):
product = models.ForeignKey( product = models.ForeignKey(
Product, Product,
@ -157,6 +156,8 @@ class ProductVariant(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
objects = ProductVariantManager()
def __str__(self): def __str__(self):
return f'{self.product}: {self.name}' return f'{self.product}: {self.name}'
@ -296,7 +297,7 @@ class OrderManager(models.Manager):
class Order(models.Model): class Order(models.Model):
customer = models.ForeignKey( customer = models.ForeignKey(
User, User,
related_name="orders", related_name='orders',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True null=True
) )
@ -319,7 +320,6 @@ class Order(models.Model):
null=True, null=True,
on_delete=models.SET_NULL on_delete=models.SET_NULL
) )
coupon = models.ForeignKey( coupon = models.ForeignKey(
Coupon, Coupon,
related_name='orders', related_name='orders',
@ -327,19 +327,22 @@ class Order(models.Model):
blank=True, blank=True,
null=True null=True
) )
subtotal_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0
)
coupon_amount = models.CharField(max_length=255, blank=True)
shipping_total = models.DecimalField( shipping_total = models.DecimalField(
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=0 default=0
) )
total_amount = models.DecimalField(
total_net_amount = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
default=0 default=0
) )
weight = MeasurementField( weight = MeasurementField(
measurement=Weight, measurement=Weight,
unit_choices=WeightUnits.CHOICES, unit_choices=WeightUnits.CHOICES,
@ -361,11 +364,11 @@ class Order(models.Model):
if self.coupon.discount_value_type == DiscountValueType.FIXED: if self.coupon.discount_value_type == DiscountValueType.FIXED:
return self.coupon.discount_value return self.coupon.discount_value
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount return (self.coupon.discount_value / Decimal('100')) * self.subtotal_amount
return Decimal('0') return Decimal('0')
def get_total_price_after_discount(self): def get_total_price_after_discount(self):
return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 2) return round((self.subtotal_amount - self.get_discount()) + self.shipping_total, 2)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
@ -395,13 +398,13 @@ class Transaction(models.Model):
class OrderLine(models.Model): class OrderLine(models.Model):
order = models.ForeignKey( order = models.ForeignKey(
Order, Order,
related_name="lines", related_name='lines',
editable=False, editable=False,
on_delete=models.CASCADE on_delete=models.CASCADE
) )
product = models.ForeignKey( variant = models.ForeignKey(
Product, ProductVariant,
related_name="order_lines", related_name='order_lines',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
blank=True, blank=True,
null=True, null=True,
@ -410,20 +413,17 @@ class OrderLine(models.Model):
quantity_fulfilled = models.IntegerField( quantity_fulfilled = models.IntegerField(
validators=[MinValueValidator(0)], default=0 validators=[MinValueValidator(0)], default=0
) )
customer_note = models.TextField(blank=True, default="") customer_note = models.TextField(blank=True, default='')
currency = models.CharField( currency = models.CharField(
max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
default=settings.DEFAULT_CURRENCY, default=settings.DEFAULT_CURRENCY,
) )
unit_price = models.DecimalField( unit_price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS, max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES, decimal_places=settings.DEFAULT_DECIMAL_PLACES,
) )
tax_rate = models.DecimalField( tax_rate = models.DecimalField(
max_digits=5, decimal_places=2, default=Decimal("0.0") max_digits=5, decimal_places=2, default=Decimal('0.0')
) )
def get_total(self): def get_total(self):

View File

@ -71,7 +71,7 @@
<span class="order__status--display"> <span class="order__status--display">
<div class="status__dot order__status--{{order.status}}"></div> <div class="status__dot order__status--{{order.status}}"></div>
{{order.get_status_display}}</span> {{order.get_status_display}}</span>
<span>${{order.total_net_amount}}</span> <span>${{order.total_amount}}</span>
</a> </a>
{% empty %} {% empty %}
<span class="object__item">No orders</span> <span class="object__item">No orders</span>

View File

@ -26,10 +26,10 @@
</div> </div>
{% for item in order.lines.all %} {% for item in order.lines.all %}
<div class="object__item object__item--col5"> <div class="object__item object__item--col5">
{% with product=item.product %} {% with product=item.variant.product %}
<figure class="item__figure"> <figure class="item__figure">
<img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
<figcaption><strong>{{product.name}}</strong><br>Grind: {{item.customer_note}}</figcaption> <figcaption><strong>{{item.variant}}</strong><br>{{item.customer_note}}</figcaption>
</figure> </figure>
<span>{{product.sku}}</span> <span>{{product.sku}}</span>
<span>{{item.quantity}}</span> <span>{{item.quantity}}</span>
@ -103,7 +103,7 @@
</div> </div>
<div class="panel__item"> <div class="panel__item">
<p> <p>
<span>Subtotal: ${{order.total_net_amount}}</span><br> <span>Subtotal: ${{order.subtotal_amount}}</span><br>
{% if order.coupon %} {% if order.coupon %}
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br> <span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %} {% endif %}

View File

@ -15,15 +15,33 @@
<img class="" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure> </figure>
<div> <div>
<p>Category: {{ product.category }}</p>
<h1>{{product.name}}</h1> <h1>{{product.name}}</h1>
<h5>{{ product.subtitle }}</h5>
<p>{{product.description}}</p> <p>{{product.description}}</p>
<p>$<strong>{{product.price}}</strong></p> <p>Checkout limit: <strong>{{ product.checkout_limit }}</strong></p>
<p>{{product.weight.oz}} oz</p>
<p>Visible in listings: <strong>{{product.visible_in_listings|yesno:"Yes,No"}}</strong></p> <p>Visible in listings: <strong>{{product.visible_in_listings|yesno:"Yes,No"}}</strong></p>
<p>Stripe ID: {{ product.stripe_id }}</p> <p>Sorting: {{ product.sorting }}</p>
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p> <p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p>
</div> </div>
</section> </section>
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Variants</h4>
<a href="{% url 'dashboard:variant-create' product.pk %}" class="action-button order__fulfill">+ New variant</a>
</div>
{% for variant in product.variants.all %}
<div class="panel__item">
<p>name: {{ variant.name }}</p>
<p>sku: {{ variant.sku }}</p>
<p>stripe_id: {{ variant.stripe_id }}</p>
<p>price: ${{ variant.price }}</p>
<p>weight: {{ variant.weight }}</p>
<p>track_inventory: {{ variant.track_inventory }}</p>
<p>stock: {{ variant.stock }}</p>
</div>
{% endfor %}
</section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item panel__header panel__header--flex"> <div class="object__item panel__header panel__header--flex">
<h4>Photos</h4> <h4>Photos</h4>

View File

@ -0,0 +1,18 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Create variant</h1>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:variant-create' product.pk %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Create variant"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -121,6 +121,32 @@ urlpatterns = [
name='prodphoto-delete' name='prodphoto-delete'
), ),
])), ])),
# ProductVariants
path('variants/', include([
path(
'new/',
views.ProductVariantCreateView.as_view(),
name='variant-create'
),
path('<int:variant_pk>/', include([
path(
'',
views.ProductVariantDetailView.as_view(),
name='variant-detail'
),
path(
'update/',
views.ProductVariantUpdateView.as_view(),
name='variant-update'
),
path(
'delete/',
views.ProductVariantDeleteView.as_view(),
name='variant-delete'
),
])),
])),
])), ])),
path( path(

View File

@ -28,6 +28,7 @@ from accounts.forms import AddressForm
from core.models import ( from core.models import (
Product, Product,
ProductPhoto, ProductPhoto,
ProductVariant,
Order, Order,
OrderLine, OrderLine,
ShippingRate, ShippingRate,
@ -71,7 +72,7 @@ class DashboardHomeView(LoginRequiredMixin, TemplateView):
status=OrderStatus.DRAFT status=OrderStatus.DRAFT
).filter( ).filter(
created_at__date=today created_at__date=today
).aggregate(total=Sum('total_net_amount'))['total'] ).aggregate(total=Sum('total_amount'))['total']
return context return context
@ -167,10 +168,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
).select_related( ).select_related(
'customer', 'customer',
'billing_address', 'billing_address',
'shipping_address', 'shipping_address'
'shipping_method'
).prefetch_related( ).prefetch_related(
'lines__product__productphoto_set' 'lines__variant__product__productphoto_set'
) )
obj = queryset.get() obj = queryset.get()
return obj return obj
@ -291,6 +291,54 @@ class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductVariantCreateView(SuccessMessageMixin, CreateView):
model = ProductVariant
success_message = 'ProductVariant created.'
template_name = 'dashboard/productvariant_create_form.html'
fields = [
'name',
'sku',
'price',
'weight',
'track_inventory',
'stock',
]
success_message = 'Variant created.'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['product'] = Product.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
form.instance.product = Product.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductVariantDetailView(DetailView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
class ProductVariantUpdateView(SuccessMessageMixin, UpdateView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant saved.'
fields = '__all__'
class ProductVariantDeleteView(SuccessMessageMixin, DeleteView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant deleted.'
def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class CustomerListView(LoginRequiredMixin, ListView): class CustomerListView(LoginRequiredMixin, ListView):
model = User model = User
template_name = 'dashboard/customer_list.html' template_name = 'dashboard/customer_list.html'

View File

@ -1881,7 +1881,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:18:59.584Z", "created_at": "2022-03-15T17:18:59.584Z",
"updated_at": "2022-03-15T17:18:59.584Z" "updated_at": "2022-03-15T17:18:59.584Z"
@ -1897,7 +1897,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:22:18.440Z", "created_at": "2022-03-15T17:22:18.440Z",
"updated_at": "2022-03-15T17:22:18.440Z" "updated_at": "2022-03-15T17:22:18.440Z"
@ -1913,7 +1913,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T17:26:27.869Z", "created_at": "2022-03-15T17:26:27.869Z",
"updated_at": "2022-03-15T17:26:27.869Z" "updated_at": "2022-03-15T17:26:27.869Z"
@ -1929,7 +1929,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T18:14:16.587Z", "created_at": "2022-03-15T18:14:16.587Z",
"updated_at": "2022-03-15T18:14:16.587Z" "updated_at": "2022-03-15T18:14:16.587Z"
@ -1945,7 +1945,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T18:16:59.460Z", "created_at": "2022-03-15T18:16:59.460Z",
"updated_at": "2022-03-15T18:16:59.460Z" "updated_at": "2022-03-15T18:16:59.460Z"
@ -1961,7 +1961,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T18:23:13.283Z", "created_at": "2022-03-15T18:23:13.283Z",
"updated_at": "2022-03-15T18:23:13.283Z" "updated_at": "2022-03-15T18:23:13.283Z"
@ -1977,7 +1977,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T18:29:02.632Z", "created_at": "2022-03-15T18:29:02.632Z",
"updated_at": "2022-03-15T18:29:02.632Z" "updated_at": "2022-03-15T18:29:02.632Z"
@ -1993,7 +1993,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:13:50.050Z", "created_at": "2022-03-15T19:13:50.050Z",
"updated_at": "2022-03-15T19:13:50.050Z" "updated_at": "2022-03-15T19:13:50.050Z"
@ -2009,7 +2009,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:15:18.843Z", "created_at": "2022-03-15T19:15:18.843Z",
"updated_at": "2022-03-15T19:15:18.843Z" "updated_at": "2022-03-15T19:15:18.843Z"
@ -2025,7 +2025,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:17:21.952Z", "created_at": "2022-03-15T19:17:21.952Z",
"updated_at": "2022-03-15T19:17:21.952Z" "updated_at": "2022-03-15T19:17:21.952Z"
@ -2041,7 +2041,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:22:34.503Z", "created_at": "2022-03-15T19:22:34.503Z",
"updated_at": "2022-03-15T19:22:34.503Z" "updated_at": "2022-03-15T19:22:34.503Z"
@ -2057,7 +2057,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:25:35.313Z", "created_at": "2022-03-15T19:25:35.313Z",
"updated_at": "2022-03-15T19:25:35.313Z" "updated_at": "2022-03-15T19:25:35.313Z"
@ -2073,7 +2073,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:26:51.478Z", "created_at": "2022-03-15T19:26:51.478Z",
"updated_at": "2022-03-15T19:26:51.478Z" "updated_at": "2022-03-15T19:26:51.478Z"
@ -2089,7 +2089,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:30:28.497Z", "created_at": "2022-03-15T19:30:28.497Z",
"updated_at": "2022-03-15T19:30:28.497Z" "updated_at": "2022-03-15T19:30:28.497Z"
@ -2105,7 +2105,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:36:30.561Z", "created_at": "2022-03-15T19:36:30.561Z",
"updated_at": "2022-03-15T19:36:30.561Z" "updated_at": "2022-03-15T19:36:30.561Z"
@ -2121,7 +2121,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:54:38.099Z", "created_at": "2022-03-15T19:54:38.099Z",
"updated_at": "2022-03-15T19:54:38.099Z" "updated_at": "2022-03-15T19:54:38.099Z"
@ -2137,7 +2137,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T19:56:49.477Z", "created_at": "2022-03-15T19:56:49.477Z",
"updated_at": "2022-03-15T19:56:49.477Z" "updated_at": "2022-03-15T19:56:49.477Z"
@ -2153,7 +2153,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:01:53.848Z", "created_at": "2022-03-15T20:01:53.848Z",
"updated_at": "2022-03-15T20:01:53.848Z" "updated_at": "2022-03-15T20:01:53.848Z"
@ -2169,7 +2169,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:09:31.510Z", "created_at": "2022-03-15T20:09:31.510Z",
"updated_at": "2022-03-15T20:09:31.510Z" "updated_at": "2022-03-15T20:09:31.510Z"
@ -2185,7 +2185,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:13:16.927Z", "created_at": "2022-03-15T20:13:16.927Z",
"updated_at": "2022-03-15T20:13:16.927Z" "updated_at": "2022-03-15T20:13:16.927Z"
@ -2201,7 +2201,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:14:43.333Z", "created_at": "2022-03-15T20:14:43.333Z",
"updated_at": "2022-03-15T20:14:43.333Z" "updated_at": "2022-03-15T20:14:43.333Z"
@ -2217,7 +2217,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:16:03.299Z", "created_at": "2022-03-15T20:16:03.299Z",
"updated_at": "2022-03-15T20:16:03.299Z" "updated_at": "2022-03-15T20:16:03.299Z"
@ -2233,7 +2233,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:17:32.842Z", "created_at": "2022-03-15T20:17:32.842Z",
"updated_at": "2022-03-15T20:17:32.842Z" "updated_at": "2022-03-15T20:17:32.842Z"
@ -2249,7 +2249,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:21:35.974Z", "created_at": "2022-03-15T20:21:35.974Z",
"updated_at": "2022-03-15T20:21:35.974Z" "updated_at": "2022-03-15T20:21:35.974Z"
@ -2265,7 +2265,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:22:11.717Z", "created_at": "2022-03-15T20:22:11.717Z",
"updated_at": "2022-03-15T20:22:11.717Z" "updated_at": "2022-03-15T20:22:11.717Z"
@ -2281,7 +2281,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:23:49.392Z", "created_at": "2022-03-15T20:23:49.392Z",
"updated_at": "2022-03-15T20:23:49.392Z" "updated_at": "2022-03-15T20:23:49.392Z"
@ -2297,7 +2297,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:25:04.787Z", "created_at": "2022-03-15T20:25:04.787Z",
"updated_at": "2022-03-15T20:25:04.787Z" "updated_at": "2022-03-15T20:25:04.787Z"
@ -2313,7 +2313,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:27:47.933Z", "created_at": "2022-03-15T20:27:47.933Z",
"updated_at": "2022-03-15T20:27:47.933Z" "updated_at": "2022-03-15T20:27:47.933Z"
@ -2329,7 +2329,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:30:40.141Z", "created_at": "2022-03-15T20:30:40.141Z",
"updated_at": "2022-03-15T20:30:40.141Z" "updated_at": "2022-03-15T20:30:40.141Z"
@ -2345,7 +2345,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-15T20:32:09.015Z", "created_at": "2022-03-15T20:32:09.015Z",
"updated_at": "2022-03-23T16:02:59.305Z" "updated_at": "2022-03-23T16:02:59.305Z"
@ -2361,7 +2361,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-23T16:59:10.471Z", "created_at": "2022-03-23T16:59:10.471Z",
"updated_at": "2022-03-23T17:00:17.128Z" "updated_at": "2022-03-23T17:00:17.128Z"
@ -2377,7 +2377,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "25.46", "total_amount": "25.46",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-23T21:22:54.950Z", "created_at": "2022-03-23T21:22:54.950Z",
"updated_at": "2022-03-23T21:22:54.950Z" "updated_at": "2022-03-23T21:22:54.950Z"
@ -2393,7 +2393,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 1, "coupon": 1,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "12.73", "total_amount": "12.73",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-23T21:30:54.290Z", "created_at": "2022-03-23T21:30:54.290Z",
"updated_at": "2022-03-23T21:30:54.290Z" "updated_at": "2022-03-23T21:30:54.290Z"
@ -2409,7 +2409,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 1, "coupon": 1,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-23T21:45:57.399Z", "created_at": "2022-03-23T21:45:57.399Z",
"updated_at": "2022-03-23T21:45:57.399Z" "updated_at": "2022-03-23T21:45:57.399Z"
@ -2425,7 +2425,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 1, "coupon": 1,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-03-23T21:52:22.463Z", "created_at": "2022-03-23T21:52:22.463Z",
"updated_at": "2022-03-25T16:51:04.837Z" "updated_at": "2022-03-25T16:51:04.837Z"
@ -2441,7 +2441,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 1, "coupon": 1,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "67.00", "total_amount": "67.00",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-01T17:09:34.892Z", "created_at": "2022-04-01T17:09:34.892Z",
"updated_at": "2022-04-01T17:09:34.892Z" "updated_at": "2022-04-01T17:09:34.892Z"
@ -2457,7 +2457,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-04T00:02:12.247Z", "created_at": "2022-04-04T00:02:12.247Z",
"updated_at": "2022-04-04T00:02:12.247Z" "updated_at": "2022-04-04T00:02:12.247Z"
@ -2473,7 +2473,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-04T00:03:44.789Z", "created_at": "2022-04-04T00:03:44.789Z",
"updated_at": "2022-04-04T00:03:44.789Z" "updated_at": "2022-04-04T00:03:44.789Z"
@ -2489,7 +2489,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-06T01:18:18.633Z", "created_at": "2022-04-06T01:18:18.633Z",
"updated_at": "2022-04-06T01:18:18.633Z" "updated_at": "2022-04-06T01:18:18.633Z"
@ -2505,7 +2505,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "67.00", "total_amount": "67.00",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-06T17:48:39.005Z", "created_at": "2022-04-06T17:48:39.005Z",
"updated_at": "2022-04-06T18:04:31.040Z" "updated_at": "2022-04-06T18:04:31.040Z"
@ -2521,7 +2521,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-06T18:00:15.976Z", "created_at": "2022-04-06T18:00:15.976Z",
"updated_at": "2022-04-06T18:00:15.976Z" "updated_at": "2022-04-06T18:00:15.976Z"
@ -2537,7 +2537,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": 2, "coupon": 2,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-06T18:01:51.206Z", "created_at": "2022-04-06T18:01:51.206Z",
"updated_at": "2022-04-06T18:01:51.206Z" "updated_at": "2022-04-06T18:01:51.206Z"
@ -2553,7 +2553,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:18:58.958Z", "created_at": "2022-04-15T03:18:58.958Z",
"updated_at": "2022-04-15T03:18:58.958Z" "updated_at": "2022-04-15T03:18:58.958Z"
@ -2569,7 +2569,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:19:14.980Z", "created_at": "2022-04-15T03:19:14.980Z",
"updated_at": "2022-04-15T03:19:14.980Z" "updated_at": "2022-04-15T03:19:14.980Z"
@ -2585,7 +2585,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:21:45.918Z", "created_at": "2022-04-15T03:21:45.918Z",
"updated_at": "2022-04-15T03:21:45.918Z" "updated_at": "2022-04-15T03:21:45.918Z"
@ -2601,7 +2601,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:22:58.009Z", "created_at": "2022-04-15T03:22:58.009Z",
"updated_at": "2022-04-15T03:22:58.009Z" "updated_at": "2022-04-15T03:22:58.009Z"
@ -2617,7 +2617,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:24:22.731Z", "created_at": "2022-04-15T03:24:22.731Z",
"updated_at": "2022-04-15T03:24:22.731Z" "updated_at": "2022-04-15T03:24:22.731Z"
@ -2633,7 +2633,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:24:38.585Z", "created_at": "2022-04-15T03:24:38.585Z",
"updated_at": "2022-04-15T03:24:38.585Z" "updated_at": "2022-04-15T03:24:38.585Z"
@ -2649,7 +2649,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-15T03:26:19.552Z", "created_at": "2022-04-15T03:26:19.552Z",
"updated_at": "2022-04-15T03:26:19.552Z" "updated_at": "2022-04-15T03:26:19.552Z"
@ -2665,7 +2665,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-23T20:51:39.679Z", "created_at": "2022-04-23T20:51:39.679Z",
"updated_at": "2022-04-23T20:51:39.679Z" "updated_at": "2022-04-23T20:51:39.679Z"
@ -2681,7 +2681,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-23T20:55:39.285Z", "created_at": "2022-04-23T20:55:39.285Z",
"updated_at": "2022-04-23T20:55:39.285Z" "updated_at": "2022-04-23T20:55:39.285Z"
@ -2697,7 +2697,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "26.80", "total_amount": "26.80",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-23T21:00:39.249Z", "created_at": "2022-04-23T21:00:39.249Z",
"updated_at": "2022-04-24T03:38:54.039Z" "updated_at": "2022-04-24T03:38:54.039Z"
@ -2713,7 +2713,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:34:28.911Z", "created_at": "2022-04-24T16:34:28.911Z",
"updated_at": "2022-04-24T16:34:28.911Z" "updated_at": "2022-04-24T16:34:28.911Z"
@ -2729,7 +2729,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:37:32.671Z", "created_at": "2022-04-24T16:37:32.671Z",
"updated_at": "2022-04-24T16:37:32.671Z" "updated_at": "2022-04-24T16:37:32.671Z"
@ -2745,7 +2745,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:41:55.368Z", "created_at": "2022-04-24T16:41:55.368Z",
"updated_at": "2022-04-24T16:41:55.368Z" "updated_at": "2022-04-24T16:41:55.368Z"
@ -2761,7 +2761,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:47:43.438Z", "created_at": "2022-04-24T16:47:43.438Z",
"updated_at": "2022-04-24T16:47:43.438Z" "updated_at": "2022-04-24T16:47:43.438Z"
@ -2777,7 +2777,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:49:10.526Z", "created_at": "2022-04-24T16:49:10.526Z",
"updated_at": "2022-04-24T16:49:10.526Z" "updated_at": "2022-04-24T16:49:10.526Z"
@ -2793,7 +2793,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T16:49:18.644Z", "created_at": "2022-04-24T16:49:18.644Z",
"updated_at": "2022-04-24T16:49:18.645Z" "updated_at": "2022-04-24T16:49:18.645Z"
@ -2809,7 +2809,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:01:14.133Z", "created_at": "2022-04-24T17:01:14.133Z",
"updated_at": "2022-04-24T17:01:14.133Z" "updated_at": "2022-04-24T17:01:14.133Z"
@ -2825,7 +2825,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:03:50.880Z", "created_at": "2022-04-24T17:03:50.880Z",
"updated_at": "2022-04-24T17:03:50.880Z" "updated_at": "2022-04-24T17:03:50.880Z"
@ -2841,7 +2841,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:19:22.528Z", "created_at": "2022-04-24T17:19:22.528Z",
"updated_at": "2022-04-24T17:19:22.528Z" "updated_at": "2022-04-24T17:19:22.528Z"
@ -2857,7 +2857,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "53.60", "total_amount": "53.60",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:23:48.946Z", "created_at": "2022-04-24T17:23:48.946Z",
"updated_at": "2022-04-24T17:23:48.946Z" "updated_at": "2022-04-24T17:23:48.946Z"
@ -2873,7 +2873,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:35:04.209Z", "created_at": "2022-04-24T17:35:04.209Z",
"updated_at": "2022-04-24T17:35:04.209Z" "updated_at": "2022-04-24T17:35:04.209Z"
@ -2889,7 +2889,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:35:40.334Z", "created_at": "2022-04-24T17:35:40.334Z",
"updated_at": "2022-04-24T17:35:40.334Z" "updated_at": "2022-04-24T17:35:40.334Z"
@ -2905,7 +2905,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:36:27.559Z", "created_at": "2022-04-24T17:36:27.559Z",
"updated_at": "2022-04-24T17:36:46.155Z" "updated_at": "2022-04-24T17:36:46.155Z"
@ -2921,7 +2921,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:52:07.802Z", "created_at": "2022-04-24T17:52:07.802Z",
"updated_at": "2022-04-24T17:52:07.802Z" "updated_at": "2022-04-24T17:52:07.802Z"
@ -2937,7 +2937,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "12.47", "shipping_total": "12.47",
"total_net_amount": "40.20", "total_amount": "40.20",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:52:59.926Z", "created_at": "2022-04-24T17:52:59.926Z",
"updated_at": "2022-04-24T17:53:38.188Z" "updated_at": "2022-04-24T17:53:38.188Z"
@ -2953,7 +2953,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T17:57:18.399Z", "created_at": "2022-04-24T17:57:18.399Z",
"updated_at": "2022-04-24T17:57:18.399Z" "updated_at": "2022-04-24T17:57:18.399Z"
@ -2969,7 +2969,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T18:36:43.689Z", "created_at": "2022-04-24T18:36:43.689Z",
"updated_at": "2022-04-24T18:37:06.954Z" "updated_at": "2022-04-24T18:37:06.954Z"
@ -2985,7 +2985,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "0.00", "shipping_total": "0.00",
"total_net_amount": "0.00", "total_amount": "0.00",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T20:44:10.464Z", "created_at": "2022-04-24T20:44:10.464Z",
"updated_at": "2022-04-24T20:44:10.464Z" "updated_at": "2022-04-24T20:44:10.464Z"
@ -3001,7 +3001,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T20:44:28.234Z", "created_at": "2022-04-24T20:44:28.234Z",
"updated_at": "2022-04-24T20:44:44.522Z" "updated_at": "2022-04-24T20:44:44.522Z"
@ -3017,7 +3017,7 @@
"shipping_method": null, "shipping_method": null,
"coupon": null, "coupon": null,
"shipping_total": "9.55", "shipping_total": "9.55",
"total_net_amount": "13.40", "total_amount": "13.40",
"weight": "0.0:oz", "weight": "0.0:oz",
"created_at": "2022-04-24T21:06:59.696Z", "created_at": "2022-04-24T21:06:59.696Z",
"updated_at": "2022-04-24T21:07:17.313Z" "updated_at": "2022-04-24T21:07:17.313Z"

View File

@ -773,6 +773,7 @@ article + article {
.item__price { .item__price {
justify-self: end; justify-self: end;
text-align: right;
} }
.item__form, .item__form,
@ -911,3 +912,8 @@ footer > section {
text-align: center; text-align: center;
} }
.show-modal {
white-space: unset;
}

View File

@ -6,8 +6,11 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import OuterRef, Q, Subquery
from core.models import Product, OrderLine, Coupon from core.models import (
Product, ProductVariant, OrderLine, Coupon, ShippingRate
)
from core.usps import USPSApi from core.usps import USPSApi
from core import ( from core import (
DiscountValueType, DiscountValueType,
@ -16,7 +19,8 @@ from core import (
OrderStatus, OrderStatus,
ShippingService, ShippingService,
ShippingContainer, ShippingContainer,
CoffeeGrind CoffeeGrind,
build_usps_rate_request
) )
from .payments import CreateOrder from .payments import CreateOrder
@ -29,32 +33,18 @@ class Cart:
self.request = request self.request = request
self.session = request.session self.session = request.session
self.coupon_code = self.session.get('coupon_code') self.coupon_code = self.session.get('coupon_code')
self.container = self.session.get('shipping_container')
cart = self.session.get(settings.CART_SESSION_ID) cart = self.session.get(settings.CART_SESSION_ID)
if not cart: if not cart:
cart = self.session[settings.CART_SESSION_ID] = {} cart = self.session[settings.CART_SESSION_ID] = []
self.cart = cart self.cart = cart
def add( def add(self, request, item, update_quantity=False):
self, request, product, quantity=1, grind='', update_quantity=False
):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'variations': {},
'price': str(product.price)
}
self.cart[product_id]['variations'][grind] = {'quantity': 0}
if update_quantity: if update_quantity:
self.cart[product_id]['variations'][grind]['quantity'] = quantity self.cart[item['variant']]['quantity'] = item['quantity']
else: else:
if not grind in self.cart[product_id]['variations']: self.cart.append(item)
# create it
self.cart[product_id]['variations'][grind] = {'quantity': quantity} # TODO: abstract this to a function that will check the max amount of item in the cart
else:
# add to it
self.cart[product_id]['variations'][grind]['quantity'] += quantity
if len(self) <= 20: if len(self) <= 20:
self.save() self.save()
else: else:
@ -65,39 +55,32 @@ class Cart:
self.session.modified = True self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n') logger.info(f'\nCart:\n{self.cart}\n')
def remove(self, product, grind): def remove(self, pk):
product_id = str(product.id) self.cart.pop(pk)
if product_id in self.cart:
del self.cart[product_id]['variations'][grind]
if not self.cart[product_id]['variations']:
del self.cart[product_id]
self.save() self.save()
def __iter__(self): def __iter__(self):
product_ids = self.cart.keys() for item in self.cart:
products = Product.objects.filter(id__in=product_ids) pk = item['variant'].pk if isinstance(item['variant'], ProductVariant) else item['variant']
for product in products: item['variant'] = ProductVariant.objects.get(pk=pk)
self.cart[str(product.id)]['product'] = product item['price_total'] = item['variant'].price * item['quantity']
for item in self.cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = Decimal(sum(self.get_item_prices()))
item['quantity'] = self.get_single_item_total_quantity(item)
yield item yield item
def __len__(self): def __len__(self):
return sum(self.get_all_item_quantities()) return sum([item['quantity'] for item in self.cart])
def get_all_item_quantities(self): def get_all_item_quantities(self):
for item in self.cart.values(): for item in self.cart:
yield sum([value['quantity'] for value in item['variations'].values()]) yield item['quantity']
def get_single_item_total_quantity(self, item): def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()]) return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self): def get_item_prices(self):
for item in self.cart.values(): for item in self:
yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()]) yield item['price_total']
# for item in self.cart.values():
# yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])
def get_total_price(self): def get_total_price(self):
return sum(self.get_item_prices()) return sum(self.get_item_prices())
@ -105,30 +88,36 @@ class Cart:
def get_total_weight(self): def get_total_weight(self):
if len(self) > 0: if len(self) > 0:
for item in self: for item in self:
return item['product'].weight.value * sum(self.get_all_item_quantities()) return item['variant'].weight.value * sum(self.get_all_item_quantities())
else: else:
return 0 return 0
def get_shipping_box(self, container=None): def get_shipping_container_choices(self):
if container: min_weight_matched = Q(
return container min_order_weight__lte=self.get_total_weight()) | Q(
min_order_weight__isnull=True
if self.container: )
return self.container max_weight_matched = Q(
max_order_weight__gte=self.get_total_weight()) | Q(
if len(self) > 6 and len(self) <= 10: max_order_weight__isnull=True
return ShippingContainer.LG_FLAT_RATE_BOX )
elif len(self) > 3 and len(self) <= 6: containers = ShippingRate.objects.filter(
return ShippingContainer.REGIONAL_RATE_BOX_B min_weight_matched & max_weight_matched
elif len(self) <= 3: )
return ShippingContainer.REGIONAL_RATE_BOX_A return containers
else:
return ShippingContainer.VARIABLE
def get_shipping_cost(self, container=None): def get_shipping_cost(self, container=None):
if len(self) > 0 and self.session.get("shipping_address"): 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'])
)
try: try:
usps_rate_request = self.build_usps_rate_request(container) logger.info('wafd')
except TypeError as e: except TypeError as e:
return Decimal('0.00') return Decimal('0.00')
usps = USPSApi(settings.USPS_USER_ID, test=True) usps = USPSApi(settings.USPS_USER_ID, test=True)
@ -140,9 +129,10 @@ class Cart:
'Could not connect to USPS, try again.' 'Could not connect to USPS, try again.'
) )
logger.info(validation.result) logger.error(validation.result)
if 'Error' not in validation.result['RateV4Response']['Package']: package = dict(validation.result['RateV4Response']['Package'])
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate'] if 'Error' not in package:
rate = package['Postage']['CommercialRate']
else: else:
logger.error("USPS Rate error") logger.error("USPS Rate error")
rate = '0.00' rate = '0.00'
@ -158,22 +148,6 @@ class Cart:
pass pass
self.session.modified = True self.session.modified = True
def build_usps_rate_request(self, container=None):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
'pounds': '0',
'ounces': f'{self.get_total_weight()}',
'container': f'{self.get_shipping_box(container)}',
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}
def build_order_params(self, container=None): def build_order_params(self, container=None):
return \ return \
{ {
@ -186,7 +160,9 @@ class Cart:
'shipping_method': 'US POSTAL SERVICE ' + ( 'shipping_method': 'US POSTAL SERVICE ' + (
container if container else '' container if container else ''
), ),
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), 'shipping_address': self.build_shipping_address(
self.session.get('shipping_address')
),
} }
def create_order(self, container=None): def create_order(self, container=None):
@ -198,20 +174,22 @@ class Cart:
response = CreateOrder().create_order(params) response = CreateOrder().create_order(params)
return response 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): def build_bulk_list(self, order):
bulk_list = [] bulk_list = []
for item in self: for item in self:
for key, value in item['variations'].items():
bulk_list.append(OrderLine( bulk_list.append(OrderLine(
order=order, order=order,
product=item['product'], variant=item['variant'],
customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None), customer_note=self.get_line_options(item['options']),
unit_price=item['price'], unit_price=item['variant'].price,
quantity=value['quantity'], quantity=item['quantity']
tax_rate=2,
)) ))
return bulk_list return bulk_list
def build_shipping_address(self, address): def build_shipping_address(self, address):
@ -231,12 +209,25 @@ class Cart:
return Coupon.objects.get(code=self.coupon_code) return Coupon.objects.get(code=self.coupon_code)
return None 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): def get_discount(self):
# SHIPPING
# ENTIRE_ORDER
# SPECIFIC_PRODUCT
if self.coupon: 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 round(self.coupon.discount_value, 2)
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: 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')) * 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') return Decimal('0')
def get_subtotal_price_after_discount(self): def get_subtotal_price_after_discount(self):

View File

@ -18,27 +18,29 @@ logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form): class AddToCartForm(forms.Form):
# grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
variants = forms.ChoiceField(widget=forms.RadioSelect())
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
def __init__(self, variants, options, *args, **kwargs): def __init__(self, variants, options, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['variant'] = forms.ChoiceField(
label='',
choices=[(variant.pk, f'{variant.name} | ${variant.price}') for variant in variants]
)
for option in options: for option in options:
self.fields[option.name] = forms.ChoiceField( self.fields[option.name] = forms.ChoiceField(
choices=[(opt, opt) for opt in option.options] choices=[(opt, opt) for opt in option.options]
) )
self.fields['variants'].widget.choices = [(variant.pk, variant.name) for variant in variants]
class UpdateCartItemForm(forms.Form): class UpdateCartItemForm(forms.Form):
item_pk = 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( update = forms.BooleanField(
required=False, required=False,
initial=True, initial=True,
widget=forms.HiddenInput widget=forms.HiddenInput()
) )
@ -117,14 +119,13 @@ class AddressForm(forms.Form):
class CheckoutShippingForm(forms.Form): class CheckoutShippingForm(forms.Form):
SHIPPING_CHOICES = [ def __init__(self, containers, *args, **kwargs):
(ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'), super().__init__(*args, **kwargs)
(ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
]
shipping_method = forms.ChoiceField( self.fields['shipping_method'] = forms.ChoiceField(
label='',
widget=forms.RadioSelect, widget=forms.RadioSelect,
choices=SHIPPING_CHOICES choices=[(container.pk, f'{container.name} ${container.s_cost}') for container in containers]
) )
@ -136,11 +137,11 @@ class OrderCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Order model = Order
fields = ( fields = (
'total_net_amount', 'total_amount',
'shipping_total', 'shipping_total',
) )
widgets = { widgets = {
'total_net_amount': forms.HiddenInput(), 'total_amount': forms.HiddenInput(),
'shipping_total': forms.HiddenInput() 'shipping_total': forms.HiddenInput()
} }

View File

@ -93,21 +93,13 @@ class CreateOrder(PayPalClient):
processed_items = [ processed_items = [
{ {
# Shows within upper-right dropdown during payment approval # Shows within upper-right dropdown during payment approval
"name": f'{item["product"]}: ' + ', '.join([ "name": str(item["variant"]),
next((
f"{value['quantity']} x {v[1]}"
for i, v in enumerate(CoffeeGrind.GRIND_CHOICES)
if v[0] == key
),
None,
) for key, value in item["variations"].items()]
)[:100],
# Item details will also be in the completed paypal.com # Item details will also be in the completed paypal.com
# transaction view # transaction view
"description": item["product"].subtitle, "description": item["variant"].product.subtitle,
"unit_amount": { "unit_amount": {
"currency_code": settings.DEFAULT_CURRENCY, "currency_code": settings.DEFAULT_CURRENCY,
"value": f'{item["price"]}', "value": f'{item["variant"].price}',
}, },
"quantity": f'{item["quantity"]}', "quantity": f'{item["quantity"]}',
} }

View File

@ -11,26 +11,30 @@
<section class="cart__list"> <section class="cart__list">
{% for item in cart %} {% for item in cart %}
<div class="cart__item"> <div class="cart__item">
{% with product=item.product %} {% with product=item.variant.product %}
<figure class="item__figure"> <figure class="item__figure">
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure> </figure>
<div class="item__info"> <div class="item__info">
<h3>{{product.name}}</h3> <h3>{{product.name}}</h3>
<h5>Grind:</h5> <h4>{{ item.variant.name }}</h4>
{% for key, value in item.variations.items %} {% for key, value in item.options.items %}
<p><strong>{{ key|get_grind_display }}</strong><br> <p><strong>{{ key }}</strong>: {{ value }}</p>
<form class="item__form" action="{% url 'storefront:cart-update' product.pk key %}" method="POST">
{% csrf_token %}
{{ value.update_quantity_form }}
<input type="submit" value="Update">
<a href="{% url 'storefront:cart-remove' product.pk key %}">Remove item</a>
</form>
</p>
{% endfor %} {% endfor %}
<form class="item__form" action="{% url 'storefront:cart-update' product.pk %}" method="POST">
{% csrf_token %}
{{ item.update_quantity_form }}
<input type="submit" value="Update">
</form>
<p><a href="{% url 'storefront:cart-remove' forloop.counter0 %}">Remove item</a></p>
</div> </div>
<div class="item__price"> <div class="item__price">
<p><strong>${{item.price}}</strong></p> <p>
<strong>${{ item.variant.price }}</strong>
{% if cart.coupon and cart.coupon.type == 'specific_product' and product in cart.coupon.products.all %}
<br>Coupon: {{ cart.coupon.name }} <span>({{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}})</span>
{% endif %}
</p>
</div> </div>
{% endwith %} {% endwith %}
</div> </div>
@ -56,7 +60,7 @@
<td>Subtotal</td> <td>Subtotal</td>
<td>${{ cart.get_total_price|floatformat:"2" }}</td> <td>${{ cart.get_total_price|floatformat:"2" }}</td>
</tr> </tr>
{% if cart.coupon %} {% if cart.coupon and cart.coupon.type == 'entire_order' %}
<tr> <tr>
<td>Coupon</td> <td>Coupon</td>
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td> <td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>

View File

@ -14,20 +14,7 @@
{% csrf_token %} {% csrf_token %}
{{ form.non_field_errors }} {{ form.non_field_errors }}
<fieldset> <fieldset>
<legend>{{ form.shipping_method.label }}</legend> {{form.as_p}}
{% for radio in form.shipping_method %}
<p>
<label for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
{% if 'Flate Rate Box - Medium' in radio.choice_label %}
<strong>${{ MD_FLAT_RATE_BOX }}</strong>
{% elif 'Regional Rate Box B' in radio.choice_label %}
<strong>${{ REGIONAL_RATE_BOX_B }}</strong>
{% endif %}
</label>
{{ radio.tag }}
</p>
{% endfor %}
</fieldset> </fieldset>
<br> <br>
<p> <p>

View File

@ -21,12 +21,12 @@
<tbody> <tbody>
{% for item in order.lines.all %} {% for item in order.lines.all %}
<tr> <tr>
{% with product=item.product %} {% with product=item.variant.product %}
<td> <td>
<img class="line__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="line__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</td> </td>
<td> <td>
<strong>{{product.name}}</strong><br> <strong>{{ item.variant }}</strong><br>
{{item.customer_note}} {{item.customer_note}}
</td> </td>
<td>{{item.quantity}}</td> <td>{{item.quantity}}</td>
@ -48,7 +48,7 @@
<table> <table>
<tr> <tr>
<td>Subtotal</td> <td>Subtotal</td>
<td>${{order.total_net_amount}}</td> <td>${{order.subtotal}}</td>
</tr> </tr>
{% if order.coupon %} {% if order.coupon %}
<tr> <tr>

View File

@ -32,18 +32,24 @@
<h3>Review items</h3> <h3>Review items</h3>
{% for item in cart %} {% for item in cart %}
<div class="cart__item"> <div class="cart__item">
{% with product=item.product %} {% with product=item.variant.product %}
<figure> <figure>
<img src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure> </figure>
<div> <div>
<h4>{{product.name}}</h4> <h3>{{product.name}}</h3>
{% for key, value in item.variations.items %} <h4>{{ item.variant.name }}</h4>
<p>Grind: <strong>{{ key|get_grind_display }}</strong>, Qty: <strong>{{value.quantity}}</strong></p> {% for key, value in item.options.items %}
<p><strong>{{ key }}</strong>: {{ value }}</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="item__price"> <div class="item__price">
<p><strong>${{item.price}}</strong></p> <p>
<strong>{{ item.quantity }} &times; ${{ item.variant.price }}</strong>
{% if cart.coupon and cart.coupon.type == 'specific_product' and product in cart.coupon.products.all %}
<br>Coupon: {{ cart.coupon.name }} <span>({{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}})</span>
{% endif %}
</p>
</div> </div>
{% endwith %} {% endwith %}
</div> </div>
@ -61,7 +67,7 @@
<td>Subtotal</td> <td>Subtotal</td>
<td>${{cart.get_total_price|floatformat:"2"}}</td> <td>${{cart.get_total_price|floatformat:"2"}}</td>
</tr> </tr>
{% if cart.coupon %} {% if cart.coupon and cart.coupon.type == 'entire_order' %}
<tr> <tr>
<td>Coupon</td> <td>Coupon</td>
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td> <td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>

View File

@ -21,9 +21,6 @@
<h1>{{product.name}}</h1> <h1>{{product.name}}</h1>
<h3>{{product.subtitle}}</h3> <h3>{{product.subtitle}}</h3>
<p>{{product.description}}</p> <p>{{product.description}}</p>
<p class="site__ft-stamp"><img class="fair_trade--small" src="{% static 'images/fair_trade_stamp.png' %}" alt="Fair trade"></p>
<p>$<strong>{{product.price}}</strong></p>
<p>{{product.weight.oz|floatformat}}oz</p>
<form class="product__form" method="post" action="{% url 'storefront:cart-add' product.pk %}"> <form class="product__form" method="post" action="{% url 'storefront:cart-add' product.pk %}">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}

View File

@ -21,7 +21,7 @@
<h3>{{ product.name }}</h3> <h3>{{ product.name }}</h3>
<h5>{{ product.subtitle }}</h5> <h5>{{ product.subtitle }}</h5>
<p>{{product.description|truncatewords:20}}</p> <p>{{product.description|truncatewords:20}}</p>
<p>$<strong>{{product.price}}</strong> | {{product.weight.oz|floatformat}}oz</p> <p>$<strong>{{product.variants.first.price}}</strong></p>
</div> </div>
</a> </a>
{% endfor %} {% endfor %}

View File

@ -35,7 +35,7 @@ class CartTest(TestCase):
) )
cls.order = Order.objects.create( cls.order = Order.objects.create(
customer=cls.customer, customer=cls.customer,
total_net_amount=13.4 total_amount=13.4
) )
def setUp(self): def setUp(self):

View File

@ -78,7 +78,7 @@ class OrderCreateViewTest(TestCase):
) )
cls.order = Order.objects.create( cls.order = Order.objects.create(
customer=cls.customer, customer=cls.customer,
total_net_amount=13.4 total_amount=13.4
) )
def setUp(self): def setUp(self):

View File

@ -20,12 +20,12 @@ urlpatterns = [
name='cart-add' name='cart-add'
), ),
path( path(
'cart/<int:pk>/update/<slug:grind>/', 'cart/<int:pk>/update/',
views.CartUpdateProductView.as_view(), views.CartUpdateProductView.as_view(),
name='cart-update', name='cart-update',
), ),
path( path(
'cart/<int:pk>/remove/<slug:grind>/', 'cart/<int:pk>/remove/',
views.cart_remove_product_view, views.cart_remove_product_view,
name='cart-remove', name='cart-remove',
), ),
@ -39,11 +39,6 @@ urlpatterns = [
views.paypal_order_transaction_capture, views.paypal_order_transaction_capture,
name='paypal-capture', name='paypal-capture',
), ),
path(
'paypal/webhooks/',
views.paypal_webhook_endpoint,
name='paypal-webhook'
),
path( path(
'checkout/address/', 'checkout/address/',
views.CheckoutAddressView.as_view(), views.CheckoutAddressView.as_view(),

View File

@ -32,7 +32,7 @@ from accounts.forms import (
AddressForm as AccountAddressForm, CustomerUpdateForm AddressForm as AccountAddressForm, CustomerUpdateForm
) )
from core.models import ( from core.models import (
Product, ProductOption, Order, Transaction, OrderLine, Coupon Product, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate
) )
from core.forms import ShippingRateForm from core.forms import ShippingRateForm
from core import OrderStatus, ShippingContainer from core import OrderStatus, ShippingContainer
@ -54,11 +54,11 @@ class CartView(TemplateView):
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) cart = Cart(self.request)
for item in cart: for i, item in enumerate(cart):
for variation in item['variations'].values(): item['update_quantity_form'] = UpdateCartItemForm(
variation['update_quantity_form'] = UpdateCartItemForm(
initial={ initial={
'quantity': variation['quantity'] 'item_pk': i,
'quantity': item['quantity']
} }
) )
context['cart'] = cart context['cart'] = cart
@ -74,23 +74,30 @@ class CartAddProductView(SingleObjectMixin, FormView):
def get_success_url(self): def get_success_url(self):
return reverse('storefront:cart-detail') return reverse('storefront:cart-detail')
def get_form(self, form_class=None):
variants = self.get_object().variants.all()
options = ProductOption.objects.filter(product__pk=self.get_object().pk)
if form_class is None:
form_class = self.get_form_class()
return form_class(variants, options, **self.get_form_kwargs())
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
cart = Cart(request) cart = Cart(request)
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cleaned_data = form.cleaned_data
cart.add( cart.add(
request=request, request=request,
product=self.get_object(), item={
grind=form.cleaned_data['grind'], 'variant': cleaned_data.pop('variant'),
quantity=form.cleaned_data['quantity'] 'quantity': cleaned_data.pop('quantity'),
'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)
def form_valid(self, form):
return super().form_valid(form)
class CartUpdateProductView(SingleObjectMixin, FormView): class CartUpdateProductView(SingleObjectMixin, FormView):
model = Product model = Product
@ -106,9 +113,10 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
if form.is_valid(): if form.is_valid():
cart.add( cart.add(
request=request, request=request,
product=self.get_object(), item={
grind=kwargs['grind'], 'variant': form.cleaned_data['item_pk'],
quantity=form.cleaned_data['quantity'], 'quantity': form.cleaned_data['quantity']
},
update_quantity=form.cleaned_data['update'] update_quantity=form.cleaned_data['update']
) )
return self.form_valid(form) return self.form_valid(form)
@ -119,10 +127,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
return super().form_valid(form) return super().form_valid(form)
def cart_remove_product_view(request, pk, grind): def cart_remove_product_view(request, pk):
cart = Cart(request) cart = Cart(request)
product = get_object_or_404(Product, id=pk) cart.remove(pk)
cart.remove(product, grind)
return redirect('storefront:cart-detail') return redirect('storefront:cart-detail')
@ -147,10 +154,10 @@ class CouponApplyView(FormView):
return super().form_valid(form) return super().form_valid(form)
class ProductListView(FormMixin, ListView): class ProductListView(ListView):
model = Product model = Product
template_name = 'storefront/product_list.html' template_name = 'storefront/product_list.html'
form_class = AddToCartForm # form_class = AddToCartForm
ordering = 'sorting' ordering = 'sorting'
queryset = Product.objects.filter( queryset = Product.objects.filter(
@ -229,38 +236,27 @@ class CheckoutShippingView(FormView):
success_url = reverse_lazy('storefront:order-create') success_url = reverse_lazy('storefront:order-create')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cart = Cart(request) if not self.request.session.get('shipping_address'):
if len(cart) != 6:
if 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
return HttpResponseRedirect(
reverse('storefront:order-create')
)
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.') messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_form(self, form_class=None):
cart = Cart(self.request) cart = Cart(self.request)
context = super().get_context_data(**kwargs) containers = cart.get_shipping_container_choices()
context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost( for container in containers:
ShippingContainer.MD_FLAT_RATE_BOX container.s_cost = cart.get_shipping_cost(container.container)
) if form_class is None:
context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost( form_class = self.get_form_class()
ShippingContainer.REGIONAL_RATE_BOX_B return form_class(containers, **self.get_form_kwargs())
)
return context
def form_valid(self, form): def form_valid(self, form):
cleaned_data = form.cleaned_data shipping_container = ShippingRate.objects.get(
self.request.session['shipping_container'] = cleaned_data.get( pk=form.cleaned_data.get('shipping_method')
'shipping_method'
) )
self.request.session['shipping_container'] = shipping_container
return super().form_valid(form) return super().form_valid(form)
@ -271,17 +267,13 @@ class OrderCreateView(CreateView):
success_url = reverse_lazy('storefront:payment-done') success_url = reverse_lazy('storefront:payment-done')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cart = Cart(request) if not self.request.session.get('shipping_address'):
if len(cart) != 6 and 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.') messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
elif self.request.session.get('coupon_code'): elif self.request.session.get('coupon_code'):
address = self.request.session.get("shipping_address") address = self.request.session.get('shipping_address')
coupon = Coupon.objects.get( coupon = Coupon.objects.get(
code=self.request.session.get('coupon_code') code=self.request.session.get('coupon_code')
) )
@ -292,19 +284,22 @@ class OrderCreateView(CreateView):
if user in coupon.users.all(): if user in coupon.users.all():
del self.request.session['coupon_code'] del self.request.session['coupon_code']
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): def get_initial(self):
cart = Cart(self.request) cart = Cart(self.request)
shipping_container = self.request.session.get(
'shipping_container'
).container
try: try:
shipping_cost = cart.get_shipping_cost() shipping_cost = cart.get_shipping_cost(shipping_container)
except Exception as e: except Exception as e:
raise e('Could not get shipping information') logger.error('Could not get shipping information')
raise
shipping_cost = Decimal('0.00') shipping_cost = Decimal('0.00')
initial = { initial = {
'total_net_amount': cart.get_total_price(), 'total_amount': cart.get_total_price(),
'shipping_total': shipping_cost 'shipping_total': shipping_cost
} }
if self.request.session.get('shipping_address'): if self.request.session.get('shipping_address'):
@ -326,8 +321,12 @@ 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.coupon_amount = cart.get_discount()
form.instance.total_amount = cart.get_total_price_after_discount()
form.instance.weight = cart.get_total_weight()
shipping_address = self.request.session.get('shipping_address') shipping_address = self.request.session.get('shipping_address')
shipping_container = self.request.session.get('shipping_container') 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()
@ -373,14 +372,6 @@ def paypal_order_transaction_capture(request, transaction_id):
return JsonResponse({'details': 'invalid request'}) return JsonResponse({'details': 'invalid request'})
@csrf_exempt
@require_POST
def paypal_webhook_endpoint(request):
data = json.loads(request.body)
logger.info(data)
return JsonResponse(data)
class PaymentDoneView(TemplateView): class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html' template_name = 'storefront/payment_done.html'