Merge branch 'feature/updated-products' into develop

This commit is contained in:
Nathan Chapman 2022-10-15 20:06:29 -06:00
commit 8b4eb80fbb
65 changed files with 1638 additions and 486 deletions

View File

@ -18,7 +18,12 @@ class Address(models.Model):
postal_code = models.CharField(max_length=20, blank=True)
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):

View File

@ -23,7 +23,7 @@ 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'],
email=form.cleaned_data['email'].lower(),
defaults={
'username': form.cleaned_data['email'].lower(),
'is_staff': False,

View File

@ -66,13 +66,15 @@ class TransactionStatus:
]
class ShippingMethodType:
PRICE_BASED = 'price'
WEIGHT_BASED = 'weight'
class ShippingProvider:
USPS = 'USPS'
# UPS = 'UPS'
# FEDEX = 'FEDEX'
CHOICES = [
(PRICE_BASED, 'Price based shipping'),
(WEIGHT_BASED, 'Weight based shipping'),
(USPS, 'USPS'),
# (UPS, 'UPS'),
# (FEDEX, 'FedEx'),
]
@ -125,3 +127,20 @@ class CoffeeGrind:
(PERCOLATOR, 'Percolator'),
(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,19 +1,27 @@
from django.contrib import admin
from .models import (
SiteSettings,
ProductCategory,
Product,
ProductPhoto,
ProductVariant,
ProductOption,
Coupon,
ShippingMethod,
ShippingRate,
Order,
Transaction,
OrderLine,
)
admin.site.register(SiteSettings)
admin.site.register(ProductCategory)
admin.site.register(Product)
admin.site.register(ProductPhoto)
admin.site.register(ProductVariant)
admin.site.register(ProductOption)
admin.site.register(Coupon)
admin.site.register(ShippingMethod)
admin.site.register(ShippingRate)
admin.site.register(Order)
admin.site.register(Transaction)
admin.site.register(OrderLine)

View File

@ -0,0 +1,5 @@
from .models import SiteSettings
def site_settings(request):
return {'site_settings': SiteSettings.load()}

View File

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

View File

@ -2,11 +2,11 @@ import logging
from django import forms
from django.core.mail import EmailMessage
from core.models import Order, OrderLine, ShippingMethod
from core.models import Order, OrderLine, ShippingRate
logger = logging.getLogger(__name__)
class ShippingMethodForm(forms.ModelForm):
class ShippingRateForm(forms.ModelForm):
class Meta:
model = ShippingMethod
model = ShippingRate
fields = '__all__'

View File

@ -0,0 +1,130 @@
# Generated by Django 4.0.2 on 2022-09-07 20:34
from django.conf import settings
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django_measurement.models
import measurement.measures.mass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0010_product_stripe_id'),
]
operations = [
migrations.CreateModel(
name='ProductCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
options={
'verbose_name': 'Product Category',
'verbose_name_plural': 'Product Categories',
},
),
migrations.CreateModel(
name='ProductOption',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), size=None)),
],
),
migrations.CreateModel(
name='ProductVariant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('sku', models.CharField(max_length=255, unique=True)),
('stripe_id', models.CharField(blank=True, max_length=255)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
('weight', django_measurement.models.MeasurementField(blank=True, measurement=measurement.measures.mass.Mass, null=True)),
('track_inventory', models.BooleanField(default=False)),
('stock', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)])),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='ShippingRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('shipping_provider', models.CharField(choices=[('USPS', 'USPS')], default='USPS', max_length=255)),
('name', models.CharField(max_length=255)),
('container', models.CharField(choices=[('LG FLAT RATE BOX', 'Flate Rate Box - Large'), ('MD FLAT RATE BOX', 'Flate Rate Box - Medium'), ('REGIONALRATEBOXA', 'Regional Rate Box A'), ('REGIONALRATEBOXB', 'Regional Rate Box B'), ('VARIABLE', 'Variable')], default='VARIABLE', max_length=255)),
('min_order_weight', models.PositiveIntegerField(blank=True, null=True)),
('max_order_weight', models.PositiveIntegerField(blank=True, null=True)),
],
options={
'ordering': ['min_order_weight'],
},
),
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('usps_user_id', models.CharField(max_length=255)),
],
options={
'verbose_name': 'Site Settings',
'verbose_name_plural': 'Site Settings',
},
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_id', models.CharField(blank=True, max_length=255)),
('customer', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscription', to=settings.AUTH_USER_MODEL)),
],
),
migrations.RemoveField(
model_name='order',
name='shipping_method',
),
migrations.RemoveField(
model_name='product',
name='price',
),
migrations.RemoveField(
model_name='product',
name='sku',
),
migrations.RemoveField(
model_name='product',
name='stripe_id',
),
migrations.RemoveField(
model_name='product',
name='weight',
),
migrations.AddField(
model_name='product',
name='checkout_limit',
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.DeleteModel(
name='ShippingMethod',
),
migrations.AddField(
model_name='productvariant',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product'),
),
migrations.AddField(
model_name='productoption',
name='product',
field=models.ManyToManyField(related_name='options', to='core.Product'),
),
migrations.AddField(
model_name='product',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.productcategory'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.0.2 on 2022-09-07 21:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0011_productcategory_productoption_productvariant_and_more'),
]
operations = [
migrations.AlterField(
model_name='productvariant',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='core.product'),
),
]

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

@ -0,0 +1,22 @@
# Generated by Django 4.0.2 on 2022-10-13 01:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0013_rename_total_net_amount_order_subtotal_amount_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='productvariant',
options={'ordering': ['weight']},
),
migrations.RenameField(
model_name='productoption',
old_name='product',
new_name='products',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-10-15 22:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_alter_productvariant_options_and_more'),
]
operations = [
migrations.AddField(
model_name='productcategory',
name='main_product',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-10-15 22:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0015_productcategory_main_product'),
]
operations = [
migrations.RenameField(
model_name='productcategory',
old_name='main_product',
new_name='main_category',
),
]

View File

@ -8,8 +8,10 @@ from django.db.models.functions import Coalesce
from django.conf import settings
from django.utils import timezone
from django.urls import reverse
from django.core.cache import cache
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.fields import ArrayField, HStoreField
from django.forms.models import model_to_dict
from django_measurement.models import MeasurementField
@ -21,44 +23,78 @@ from . import (
VoucherType,
TransactionStatus,
OrderStatus,
ShippingMethodType
ShippingProvider,
ShippingContainer,
build_usps_rate_request
)
from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__)
class ProductEncoder(DjangoJSONEncoder):
def default(self, obj):
logger.info(f"\n{obj}\n")
return super().default(obj)
class SingletonBase(models.Model):
def set_cache(self):
cache.set(self.__class__.__name__, self)
def save(self, *args, **kwargs):
self.pk = 1
super(SingletonBase, self).save(*args, **kwargs)
self.set_cache()
def delete(self, *args, **kwargs):
pass
@classmethod
def load(cls):
if cache.get(cls.__name__) is None:
obj, created = cls.objects.get_or_create(pk=1)
if not created:
obj.set_cache()
return cache.get(cls.__name__)
class Meta:
abstract = True
class ProductManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
num_ordered=models.Sum('order_lines__quantity')
)
class SiteSettings(SingletonBase):
usps_user_id = models.CharField(max_length=255)
def __str__(self):
return 'Site Settings'
class Meta:
verbose_name = 'Site Settings'
verbose_name_plural = 'Site Settings'
class ProductCategory(models.Model):
name = models.CharField(max_length=255)
main_category = models.BooleanField(default=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('dashboard:category-detail', kwargs={'pk': self.pk})
class Meta:
verbose_name = 'Product Category'
verbose_name_plural = 'Product Categories'
class Product(models.Model):
category = models.ForeignKey(
ProductCategory,
blank=True,
null=True,
on_delete=models.SET_NULL
)
name = models.CharField(max_length=250)
subtitle = models.CharField(max_length=250, blank=True)
description = models.TextField(blank=True)
sku = models.CharField(max_length=255, unique=True)
stripe_id = models.CharField(max_length=255, blank=True)
price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
blank=True,
null=True,
)
stripe_price_id = models.CharField(max_length=255, blank=True)
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
blank=True,
null=True
checkout_limit = models.IntegerField(
default=0,
validators=[MinValueValidator(0)]
)
visible_in_listings = models.BooleanField(default=False)
@ -67,8 +103,6 @@ class Product(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = ProductManager()
def __str__(self):
return self.name
@ -88,6 +122,73 @@ class Product(models.Model):
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):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='variants'
)
name = models.CharField(max_length=255)
sku = models.CharField(max_length=255, unique=True)
stripe_id = models.CharField(max_length=255, blank=True)
price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
blank=True,
null=True,
)
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
blank=True,
null=True
)
track_inventory = models.BooleanField(default=False)
stock = models.IntegerField(
blank=True,
null=True,
validators=[MinValueValidator(0)]
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = ProductVariantManager()
def __str__(self):
return f'{self.product}: {self.name}'
class Meta:
ordering = ['weight']
class ProductOption(models.Model):
"""
Description: Consistent accross all variants
"""
products = models.ManyToManyField(
Product,
related_name='options'
)
name = models.CharField(max_length=255)
options = ArrayField(
models.CharField(max_length=255)
)
def get_absolute_url(self):
return reverse('dashboard:option-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.name}'
class ProductPhoto(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.ImageField(upload_to='products/images')
@ -150,17 +251,32 @@ class Coupon(models.Model):
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
class ShippingMethod(models.Model):
name = models.CharField(max_length=100)
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
default=0,
class ShippingRate(models.Model):
shipping_provider = models.CharField(
max_length=255,
choices=ShippingProvider.CHOICES,
default=ShippingProvider.USPS
)
name = models.CharField(max_length=255)
container = models.CharField(
max_length=255,
choices=ShippingContainer.CHOICES,
default=ShippingContainer.VARIABLE
)
min_order_weight = models.PositiveIntegerField(
blank=True,
null=True
)
max_order_weight = models.PositiveIntegerField(
blank=True,
null=True
)
def get_absolute_url(self):
return reverse('dashboard:shipmeth-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.shipping_provider}: {self.name} ({self.min_order_weight}{self.max_order_weight})'
class Meta:
ordering = ['min_order_weight']
class OrderManager(models.Manager):
@ -188,7 +304,7 @@ class OrderManager(models.Manager):
class Order(models.Model):
customer = models.ForeignKey(
User,
related_name="orders",
related_name='orders',
on_delete=models.SET_NULL,
null=True
)
@ -211,14 +327,6 @@ class Order(models.Model):
null=True,
on_delete=models.SET_NULL
)
shipping_method = models.ForeignKey(
ShippingMethod,
blank=True,
null=True,
related_name="orders",
on_delete=models.SET_NULL
)
coupon = models.ForeignKey(
Coupon,
related_name='orders',
@ -226,19 +334,22 @@ class Order(models.Model):
blank=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(
max_digits=5,
decimal_places=2,
default=0
)
total_net_amount = models.DecimalField(
total_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0
)
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
@ -260,11 +371,11 @@ class Order(models.Model):
if self.coupon.discount_value_type == DiscountValueType.FIXED:
return self.coupon.discount_value
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')
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):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
@ -294,13 +405,13 @@ class Transaction(models.Model):
class OrderLine(models.Model):
order = models.ForeignKey(
Order,
related_name="lines",
related_name='lines',
editable=False,
on_delete=models.CASCADE
)
product = models.ForeignKey(
Product,
related_name="order_lines",
variant = models.ForeignKey(
ProductVariant,
related_name='order_lines',
on_delete=models.SET_NULL,
blank=True,
null=True,
@ -309,20 +420,17 @@ class OrderLine(models.Model):
quantity_fulfilled = models.IntegerField(
validators=[MinValueValidator(0)], default=0
)
customer_note = models.TextField(blank=True, default="")
customer_note = models.TextField(blank=True, default='')
currency = models.CharField(
max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
default=settings.DEFAULT_CURRENCY,
)
unit_price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
)
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):

View File

@ -7,7 +7,7 @@ from core.models import (
Product,
ProductPhoto,
Coupon,
ShippingMethod,
ShippingRate,
Order,
Transaction,
OrderLine,

View File

@ -1,14 +1,15 @@
from measurement.measures import Weight
class WeightUnits:
# KILOGRAM = "kg"
# POUND = "lb"
POUND = "lb"
OUNCE = "oz"
# GRAM = "g"
CHOICES = [
# (KILOGRAM, "kg"),
# (POUND, "lb"),
(POUND, "lb"),
(OUNCE, "oz"),
# (GRAM, "g"),
]

View File

@ -5,7 +5,7 @@ from core import OrderStatus
from core.models import (
Order,
OrderLine,
ShippingMethod,
ShippingRate,
TrackingNumber,
Coupon,
ProductPhoto

View File

@ -0,0 +1,76 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/cubes.png' %}" alt=""> Catalog</h1>
<div>
<a href="{% url 'dashboard:option-create' %}" class="action-button">+ New product option</a>
<a href="{% url 'dashboard:category-create' %}" class="action-button">+ New category</a>
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
</div>
</header>
{% for category in category_list %}
<section class="object__list">
<div class="object__item panel__header object__item--col3">
<span>
Category:
<h4><a href="{% url 'dashboard:category-detail' category.pk %}">{{ category }}</a></h4>
</span>
<span>Name</span>
<span>Visible in listings</span>
</div>
{% for product in category.product_set.all %}
<a class="object__item object__item--link object__item--col3" href="{% url 'dashboard:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<strong>{{product.name}}</strong>
<span>{{product.visible_in_listings|yesno:"Yes,No"}}</span>
</a>
{% endfor %}
</section>
{% endfor %}
</article>
<article>
<header class="object__header">
<h2>Uncategorized Products</h2>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col4">
<span></span>
<span>Name</span>
<span>Visible</span>
<span>Price</span>
</div>
{% for product in uncategorized_products %}
<a class="object__item object__item--link object__item--col4" href="{% url 'dashboard:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<strong>{{product.name}}</strong>
<span>{{product.visible_in_listings|yesno:"Yes,No"}}</span>
<span>${{product.price}}</span>
</a>
{% endfor %}
</section>
</article>
<article>
<header class="object__header">
<h2>Product Options</h2>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col4">
<span></span>
<span>Name</span>
</div>
{% for option in option_list %}
<a class="object__item object__item--link object__item--col4" href="{% url 'dashboard:option-detail' option.pk %}">
<strong>{{option.name}}</strong>
<span>{{ option.options }}</span>
</a>
{% endfor %}
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/cubes.png' %}" alt=""> {{ category }}</h1>
</header>
<section class="category__detail object__panel">
<form method="post" class="panel__item">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:category-detail' category.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock content %}

View File

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

View File

@ -0,0 +1,24 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<div>
<h1><img src="{% static 'images/cubes.png' %}" alt=""> {{ category.name }}</h1>
<p><strong>Is a main category</strong>: {{ category.main_category|yesno:"Yes,No" }}</p>
</div>
<div class="object__menu">
<a href="{% url 'dashboard:category-delete' category.pk %}" class="action-button action-button--warning">Delete</a>
<a href="{% url 'dashboard:category-update' category.pk %}" class="action-button">Edit</a>
</div>
</header>
<section class="product__detail object__panel">
{% for product in category.product_set.all %}
<a href="{% url 'dashboard:product-detail' product.pk %}">{{ product }}</a>
{% empty %}
<p>No products</p>
{% endfor %}
</section>
</article>
{% endblock content %}

View File

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

View File

@ -0,0 +1,25 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/cubes.png' %}" alt=""> Categories</h1>
<div class="object__menu">
<a href="{% url 'dashboard:category-create' %}" class="action-button order__fulfill">+ New category</a>
</div>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col5" href="category-detail">
<span>Name</span>
</div>
{% for category in category_list %}
<a class="object__item object__item--link object__item--col5" href="{% url 'dashboard:category-detail' category.pk %}">
<span>{{ category.name }}</span>
</a>
{% empty %}
<span class="object__item">No categories</span>
{% endfor %}
</section>
</article>
{% endblock content %}

View File

@ -10,27 +10,18 @@
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Shipping methods</h4>
<a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a>
<h4>Shipping rates</h4>
<a href="{% url 'dashboard:rate-create' %}" class="action-button order__fulfill">+ New rate</a>
</div>
<div class="panel__item">
{% for method in shipping_method_list %}
{% for rate in shipping_rate_list %}
<p>
<a href="{% url 'dashboard:shipmeth-detail' method.pk %}">{{method.name}} | {{method.type}} | {{method.price}}</a>
<a href="{% url 'dashboard:rate-detail' rate.pk %}">{{ rate }}</a>
</p>
{% empty %}
<p>No shipping methods yet.</p>
<p>No shipping rates yet.</p>
{% endfor %}
</div>
</section>
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Staff</h4>
<a href="" class="action-button order__fulfill">+ New staff</a>
</div>
<div class="panel__item">
</div>
</section>
</article>
{% endblock %}

View File

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

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1>Option</h1>
</header>
<section class="option__detail object__panel">
<form method="post" class="panel__item">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:option-detail' option.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock content %}

View File

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

View File

@ -0,0 +1,24 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1>{{ option.name }}</h1>
<div class="object__menu">
<a href="{% url 'dashboard:option-delete' option.pk %}" class="action-button action-button--warning">Delete</a>
<a href="{% url 'dashboard:option-update' option.pk %}" class="action-button">Edit</a>
</div>
</header>
<section class="object__panel">
<div class="object__item panel__header">
<h4>Products</h4>
</div>
{% for product in option.products.all %}
<div class="panel__item">
<h3><a href="{% url 'dashboard:product-detail' product.pk %}">{{ product.name }}</a></h3>
</div>
{% endfor %}
</section>
</article>
{% endblock content %}

View File

@ -6,13 +6,7 @@
<header class="object__header">
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1>
<div class="object__menu">
<div class="dropdown">
<span class="dropdown__menu">Options &darr;</span>
<div class="dropdown__child">
<a href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<a href="">Return order</a>
</div>
</div>
<a class="action-button action-button--warning" href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<span class="order__status order__status--{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span>
</div>
</header>
@ -26,14 +20,14 @@
</div>
{% for item in order.lines.all %}
<div class="object__item object__item--col5">
{% with product=item.product %}
{% with product=item.variant.product %}
<figure class="item__figure">
<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>
<span>{{product.sku}}</span>
<span>{{item.quantity}}</span>
<span>${{product.price}}</span>
<span>${{item.variant.price}}</span>
<span>${{item.get_total}}</span>
{% endwith %}
</div>
@ -103,7 +97,7 @@
</div>
<div class="panel__item">
<p>
<span>Subtotal: ${{order.total_net_amount}}</span><br>
<span>Subtotal: ${{order.subtotal_amount}}</span><br>
{% if order.coupon %}
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %}

View File

@ -7,7 +7,6 @@
<form method="POST" action="">
{% csrf_token %}
{{ form.management_form }}
<section class="object__list">
{% for dict in form.errors %}
{% for error in dict.values %}
@ -20,15 +19,15 @@
<span>Product</span>
<span>SKU</span>
<span>Quantity to fulfill</span>
<span>Grind</span>
<span>Options</span>
</div>
{% for form in form %}
<div class="object__item object__item--col4">
{% with product=form.instance.product %}
{% with product=form.instance.variant.product %}
{{form.id}}
<figure class="item__figure">
<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></figcaption>
<figcaption><strong>{{form.instance.variant}}</strong></figcaption>
</figure>
<span>{{product.sku}}</span>
<span>{{form.quantity_fulfilled}} / {{form.instance.quantity}}</span>

View File

@ -15,15 +15,46 @@
<img class="" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<div>
<p>Category: {{ product.category }}</p>
<h1>{{product.name}}</h1>
<h5>{{ product.subtitle }}</h5>
<p>{{product.description}}</p>
<p>$<strong>{{product.price}}</strong></p>
<p>{{product.weight.oz}} oz</p>
<p>Checkout limit: <strong>{{ product.checkout_limit }}</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>
</div>
</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">
<h3>{{ variant.name }}</h3>
<p>SKU: {{ variant.sku }}</p>
<p>Price: ${{ variant.price }}</p>
<p>Weight: {{ variant.weight }}</p>
{% if variant.track_inventory %}
<p>Stock: {{ variant.stock }}</p>
{% endif %}
<p><a href="{% url 'dashboard:variant-update' product.pk variant.pk %}">Edit</a></p>
</div>
{% endfor %}
</section>
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Options</h4>
<p><em>To create more product options go to the <a href="{% url 'dashboard:catalog' %}">catalog</a></em></p>
</div>
{% for option in product.options.all %}
<div class="panel__item">
<h3>{{ option.name }}</h3>
<p>{{ option.options }}</p>
</div>
{% endfor %}
</section>
<section class="object__panel">
<div class="object__item panel__header panel__header--flex">
<h4>Photos</h4>

View File

@ -4,24 +4,23 @@
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static "images/cubes.png" %}" alt=""> Catalog</h1>
<h1><img src="{% static 'images/cubes.png' %}" alt=""> Catalog</h1>
<a href="{% url 'dashboard:category-create' %}" class="action-button">+ New category</a>
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col4">
<div class="object__item panel__header object__item--col3">
<span></span>
<span>Name</span>
<span>Visible</span>
<span>Price</span>
</div>
{% for product in product_list %}
<a class="object__item object__item--link object__item--col4" href="{% url 'dashboard:product-detail' product.pk %}">
<a class="object__item object__item--link object__item--col3" href="{% url 'dashboard:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<strong>{{product.name}}</strong>
<span>{{product.visible_in_listings|yesno:"Yes,No"}}</span>
<span>${{product.price}}</span>
</a>
{% endfor %}
</section>

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/gear.png' %}" alt=""> {{ rate }}</h1>
</header>
<section class="rate__detail object__panel">
<form method="post" class="panel__item">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:rate-detail' rate.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock content %}

View File

@ -2,13 +2,13 @@
{% block content %}
<article>
<h1>Create Shipping Method</h1>
<h1>Create Shipping Rate</h1>
<section>
<form method="POST" action="{% url 'dashboard:shipmeth-create' %}">
<form method="POST" action="{% url 'dashboard:rate-create' %}">
{% csrf_token %}
{{form.as_p}}
<p class="form__submit">
<input class="action-button" type="submit" value="Create method"> or <a href="{% url 'dashboard:config' %}">cancel</a>
<input class="action-button" type="submit" value="Create rate"> or <a href="{% url 'dashboard:config' %}">cancel</a>
</p>
</form>
</section>

View File

@ -0,0 +1,22 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/gear.png' %}" alt=""> Shipping Rate</h1>
<div class="object__menu">
<a href="{% url 'dashboard:rate-delete' rate.pk %}" class="action-button action-button--warning">Delete</a>
<a href="{% url 'dashboard:rate-update' rate.pk %}" class="action-button">Edit</a>
</div>
</header>
<section class="product__detail object__panel">
<div>
<h1>{{rate.name}}</h1>
<p><strong>Shipping Provider</strong>: {{ rate.shipping_provider }}</p>
<p><strong>Container</strong>: {{ rate.get_container_display }}</p>
<p><strong>Weight range</strong>: {{ rate.min_order_weight }} &ndash; {{ rate.max_order_weight }}</p>
</div>
</section>
</article>
{% endblock content %}

View File

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

View File

@ -1,21 +0,0 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/gear.png' %}" alt=""> Shipping Method</h1>
<div class="object__menu">
<a href="" class="action-button action-button--warning">Delete</a>
<a href="" class="action-button">Edit</a>
</div>
</header>
<section class="product__detail object__panel">
<div>
<h1>{{shippingmethod.name}}</h1>
<p>{{shippingmethod.get_type_display}}</p>
<p>$<strong>{{shippingmethod.price}}</strong></p>
</div>
</section>
</article>
{% endblock content %}

View File

@ -0,0 +1,20 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1>Delete Variant</h1>
</header>
<section class="variant__detail object__panel">
<form method="post" class="panel__item">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
{{ form.as_p }}
<p>
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock content %}

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

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Update variant</h1>
<a href="{% url 'dashboard:variant-delete' product.pk variant.pk %}" class="action-button action-button--warning">Delete</a>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:variant-update' product.pk variant.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

@ -20,8 +20,8 @@ from dashboard.forms import (
from dashboard.views import (
DashboardHomeView,
DashboardConfigView,
ShippingMethodCreateView,
ShippingMethodDetailView,
ShippingRateCreateView,
ShippingRateDetailView,
CouponListView,
CouponCreateView,
CouponDetailView,

View File

@ -12,17 +12,32 @@ urlpatterns = [
views.DashboardConfigView.as_view(),
name='config'
),
path(
'catalog/',
views.CatalogView.as_view(),
name='catalog'
),
path(
'shipping-methods/new/',
views.ShippingMethodCreateView.as_view(),
name='shipmeth-create'
'shipping-rates/new/',
views.ShippingRateCreateView.as_view(),
name='rate-create'
),
path('shipping-methods/<int:pk>/', include([
path('shipping-rates/<int:pk>/', include([
path(
'',
views.ShippingMethodDetailView.as_view(),
name='shipmeth-detail'
views.ShippingRateDetailView.as_view(),
name='rate-detail'
),
path(
'update/',
views.ShippingRateUpdateView.as_view(),
name='rate-update'
),
path(
'delete/',
views.ShippingRateDeleteView.as_view(),
name='rate-delete'
),
])),
@ -82,6 +97,37 @@ urlpatterns = [
),
])),
# Categories
path('categories/', include([
path(
'',
views.CategoryListView.as_view(),
name='category-list'
),
path(
'new/',
views.CategoryCreateView.as_view(),
name='category-create'
),
path('<int:pk>/', include([
path(
'',
views.CategoryDetailView.as_view(),
name='category-detail'
),
path(
'update/',
views.CategoryUpdateView.as_view(),
name='category-update'
),
path(
'delete/',
views.CategoryDeleteView.as_view(),
name='category-delete'
),
])),
])),
path(
'products/',
views.ProductListView.as_view(),
@ -121,6 +167,53 @@ urlpatterns = [
name='prodphoto-delete'
),
])),
# ProductVariants
path('variants/', include([
path(
'new/',
views.ProductVariantCreateView.as_view(),
name='variant-create'
),
path('<int:variant_pk>/', include([
path(
'update/',
views.ProductVariantUpdateView.as_view(),
name='variant-update'
),
path(
'delete/',
views.ProductVariantDeleteView.as_view(),
name='variant-delete'
),
])),
])),
])),
# ProductOptions
path('options/', include([
path(
'new/',
views.ProductOptionCreateView.as_view(),
name='option-create'
),
path('<int:pk>/', include([
path(
'',
views.ProductOptionDetailView.as_view(),
name='option-detail'
),
path(
'update/',
views.ProductOptionUpdateView.as_view(),
name='option-update'
),
path(
'delete/',
views.ProductOptionDeleteView.as_view(),
name='option-delete'
),
])),
])),
path(

View File

@ -26,11 +26,14 @@ from accounts.models import User
from accounts.utils import get_or_create_customer
from accounts.forms import AddressForm
from core.models import (
ProductCategory,
Product,
ProductPhoto,
ProductVariant,
ProductOption,
Order,
OrderLine,
ShippingMethod,
ShippingRate,
Transaction,
TrackingNumber,
Coupon
@ -39,8 +42,7 @@ from core.models import (
from core import (
DiscountValueType,
VoucherType,
OrderStatus,
ShippingMethodType
OrderStatus
)
from .forms import (
OrderLineFulfillForm,
@ -72,7 +74,7 @@ class DashboardHomeView(LoginRequiredMixin, TemplateView):
status=OrderStatus.DRAFT
).filter(
created_at__date=today
).aggregate(total=Sum('total_net_amount'))['total']
).aggregate(total=Sum('total_amount'))['total']
return context
@ -81,23 +83,52 @@ class DashboardConfigView(TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
today = timezone.localtime(timezone.now()).date()
context['shipping_method_list'] = ShippingMethod.objects.all()
context['shipping_rate_list'] = ShippingRate.objects.all()
return context
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ShippingMethod
template_name = 'dashboard/shipmeth_create_form.html'
class CatalogView(ListView):
model = ProductCategory
context_object_name = 'category_list'
template_name = 'dashboard/catalog.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['uncategorized_products'] = Product.objects.filter(
category=None
)
context['option_list'] = ProductOption.objects.all()
return context
class ShippingRateDetailView(LoginRequiredMixin, DetailView):
model = ShippingRate
context_object_name = 'rate'
template_name = 'dashboard/rate_detail.html'
class ShippingRateCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ShippingRate
context_object_name = 'rate'
template_name = 'dashboard/rate_create_form.html'
fields = '__all__'
success_message = '%(name)s created.'
class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
model = ShippingMethod
template_name = 'dashboard/shipmeth_detail.html'
class ShippingRateUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = ShippingRate
context_object_name = 'rate'
template_name = 'dashboard/rate_form.html'
success_message = 'ShippingRate saved.'
fields = '__all__'
class ShippingRateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = ShippingRate
context_object_name = 'rate'
template_name = 'dashboard/rate_confirm_delete.html'
success_message = 'ShippingRate deleted.'
success_url = reverse_lazy('dashboard:config')
class CouponListView(LoginRequiredMixin, ListView):
@ -168,10 +199,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
).select_related(
'customer',
'billing_address',
'shipping_address',
'shipping_method'
'shipping_address'
).prefetch_related(
'lines__product__productphoto_set'
'lines__variant__product__productphoto_set'
)
obj = queryset.get()
return obj
@ -183,9 +213,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Order
template_name = "dashboard/order_fulfill.html"
template_name = 'dashboard/order_fulfill.html'
form_class = OrderLineFormset
success_message = "Order saved."
success_message = 'Order saved.'
def form_valid(self, form):
form.save()
@ -222,6 +252,42 @@ class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
class CategoryListView(ListView):
model = ProductCategory
context_object_name = 'category_list'
template_name = 'dashboard/category_list.html'
class CategoryCreateView(SuccessMessageMixin, CreateView):
model = ProductCategory
context_object_name = 'category'
success_message = 'Category created.'
template_name = 'dashboard/category_create_form.html'
fields = '__all__'
class CategoryDetailView(DetailView):
model = ProductCategory
context_object_name = 'category'
template_name = 'dashboard/category_detail.html'
class CategoryUpdateView(SuccessMessageMixin, UpdateView):
model = ProductCategory
context_object_name = 'category'
success_message = 'Category saved.'
template_name = 'dashboard/category_form.html'
fields = '__all__'
class CategoryDeleteView(SuccessMessageMixin, DeleteView):
model = ProductCategory
context_object_name = 'category'
success_message = 'Category deleted.'
template_name = 'dashboard/category_confirm_delete.html'
success_url = reverse_lazy('dashboard:catalog')
class ProductListView(LoginRequiredMixin, ListView):
model = Product
template_name = 'dashboard/product_list.html'
@ -240,6 +306,20 @@ class ProductDetailView(LoginRequiredMixin, DetailView):
model = Product
template_name = 'dashboard/product_detail.html'
def get_object(self):
pk = self.kwargs.get(self.pk_url_kwarg)
queryset = Product.objects.filter(
pk=pk
).select_related(
'category',
).prefetch_related(
'variants',
'options',
'productphoto_set'
)
obj = queryset.get()
return obj
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Product
@ -292,6 +372,113 @@ class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductVariantCreateView(SuccessMessageMixin, CreateView):
model = ProductVariant
success_message = 'Variant created.'
template_name = 'dashboard/variant_create_form.html'
fields = [
'name',
'sku',
'price',
'weight',
'track_inventory',
'stock',
]
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 ProductVariantUpdateView(SuccessMessageMixin, UpdateView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant saved.'
template_name = 'dashboard/variant_form.html'
fields = [
'name',
'sku',
'price',
'weight',
'track_inventory',
'stock',
]
context_object_name = 'variant'
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 ProductVariantDeleteView(SuccessMessageMixin, DeleteView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant deleted.'
template_name = 'dashboard/variant_confirm_delete.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['product'] = Product.objects.get(pk=self.kwargs['pk'])
return context
def get_success_url(self):
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductOptionDetailView(LoginRequiredMixin, DetailView):
model = ProductOption
template_name = 'dashboard/option_detail.html'
context_object_name = 'option'
class ProductOptionCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = ProductOption
template_name = 'dashboard/option_create_form.html'
fields = [
'name',
'options',
'products',
]
success_message = '%(name)s created.'
class ProductOptionUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = ProductOption
success_message = 'Option saved.'
template_name = 'dashboard/option_form.html'
fields = [
'name',
'options',
'products',
]
context_object_name = 'option'
success_url = reverse_lazy('dashboard:catalog')
class ProductOptionDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = ProductOption
success_message = 'ProductOption deleted.'
template_name = 'dashboard/option_confirm_delete.html'
context_object_name = 'option'
success_url = reverse_lazy('dashboard:catalog')
class CustomerListView(LoginRequiredMixin, ListView):
model = User
template_name = 'dashboard/customer_list.html'

View File

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

View File

@ -85,7 +85,9 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'core.context_processors.site_settings',
'storefront.context_processors.cart',
'storefront.context_processors.product_categories',
],
},
},
@ -256,17 +258,19 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_TIMEZONE = 'US/Mountain'
# Sentry
sentry_sdk.init(
dsn=SENTRY_DSN,
environment=SENTRY_ENV,
integrations=[DjangoIntegration()],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
if not DEBUG:
sentry_sdk.init(
dsn=SENTRY_DSN,
environment=SENTRY_ENV,
integrations=[DjangoIntegration()],
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True
)
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# If you wish to associate users to errors (assuming you are using
# django.contrib.auth) you may enable sending PII data.
send_default_pii=True
)

View File

@ -367,7 +367,7 @@ main article {
}
.product__figure img {
max-height: 400px;
max-height: 200px;
}

View File

@ -526,6 +526,22 @@ section:not(:last-child) {
}
/* Breadcrumbs
========================================================================== */
.breadcrumbs {
margin-bottom: 1.5rem;
}
.breadcrumbs menu {
margin: 0;
padding: 0 1rem;
line-height: 1.75;
list-style: none;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ==========================================================================
Articles
========================================================================== */
@ -773,6 +789,7 @@ article + article {
.item__price {
justify-self: end;
text-align: right;
}
.item__form,
@ -911,3 +928,8 @@ footer > section {
text-align: center;
}
.show-modal {
white-space: unset;
}

View File

@ -6,56 +6,75 @@ from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, reverse
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 import (
DiscountValueType,
VoucherType,
TransactionStatus,
OrderStatus,
ShippingMethodType,
ShippingService,
ShippingContainer,
CoffeeGrind
CoffeeGrind,
build_usps_rate_request
)
from .forms import UpdateCartItemForm
from .payments import CreateOrder
logger = logging.getLogger(__name__)
class CartItem:
update_form = UpdateCartItemForm
def __init__(self, item):
self.variant = item['variant']
self.quantity = item['quantity']
self.options = item['options']
def get_update_form(self, index):
return self.update_form(initial={
'item_pk': index,
'quantity': self.quantity
})
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 str(self.variant)
class Cart:
item_class = CartItem
def __init__(self, request):
self.request = request
self.session = request.session
self.coupon_code = self.session.get('coupon_code')
self.container = self.session.get('shipping_container')
cart = self.session.get(settings.CART_SESSION_ID)
if not cart:
cart = self.session[settings.CART_SESSION_ID] = {}
cart = self.session[settings.CART_SESSION_ID] = []
self.cart = cart
def add(
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}
def add(self, request, item, update_quantity=False):
if update_quantity:
self.cart[product_id]['variations'][grind]['quantity'] = quantity
self.cart[item['variant']]['quantity'] = item['quantity']
else:
if not grind in self.cart[product_id]['variations']:
# create it
self.cart[product_id]['variations'][grind] = {'quantity': quantity}
else:
# add to it
self.cart[product_id]['variations'][grind]['quantity'] += quantity
self.cart.append(item)
# TODO: abstract this to a function that will check the max amount of item in the cart
if len(self) <= 20:
self.save()
else:
@ -66,39 +85,32 @@ class Cart:
self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n')
def remove(self, product, grind):
product_id = str(product.id)
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()
def remove(self, pk):
self.cart.pop(pk)
self.save()
def __iter__(self):
product_ids = self.cart.keys()
products = Product.objects.filter(id__in=product_ids)
for product in products:
self.cart[str(product.id)]['product'] = product
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)
for item in self.cart:
pk = item['variant'].pk if isinstance(item['variant'], ProductVariant) else item['variant']
item['variant'] = ProductVariant.objects.get(pk=pk)
item['price_total'] = item['variant'].price * item['quantity']
yield item
def __len__(self):
return sum(self.get_all_item_quantities())
return sum([item['quantity'] for item in self.cart])
def get_all_item_quantities(self):
for item in self.cart.values():
yield sum([value['quantity'] for value in item['variations'].values()])
for item in self.cart:
yield item['quantity']
def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self):
for item in self.cart.values():
yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])
for item in self:
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):
return sum(self.get_item_prices())
@ -106,30 +118,36 @@ class Cart:
def get_total_weight(self):
if len(self) > 0:
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:
return 0
def get_shipping_box(self, container=None):
if container:
return container
if self.container:
return self.container
if len(self) > 6 and len(self) <= 10:
return ShippingContainer.LG_FLAT_RATE_BOX
elif len(self) > 3 and len(self) <= 6:
return ShippingContainer.REGIONAL_RATE_BOX_B
elif len(self) <= 3:
return ShippingContainer.REGIONAL_RATE_BOX_A
else:
return ShippingContainer.VARIABLE
def get_shipping_container_choices(self):
min_weight_matched = Q(
min_order_weight__lte=self.get_total_weight()) | Q(
min_order_weight__isnull=True
)
max_weight_matched = Q(
max_order_weight__gte=self.get_total_weight()) | Q(
max_order_weight__isnull=True
)
containers = ShippingRate.objects.filter(
min_weight_matched & max_weight_matched
)
return containers
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:
usps_rate_request = self.build_usps_rate_request(container)
logger.info('wafd')
except TypeError as e:
return Decimal('0.00')
usps = USPSApi(settings.USPS_USER_ID, test=True)
@ -141,9 +159,10 @@ class Cart:
'Could not connect to USPS, try again.'
)
logger.info(validation.result)
if 'Error' not in validation.result['RateV4Response']['Package']:
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
logger.error(validation.result)
package = dict(validation.result['RateV4Response']['Package'])
if 'Error' not in package:
rate = package['Postage']['CommercialRate']
else:
logger.error("USPS Rate error")
rate = '0.00'
@ -159,22 +178,6 @@ class Cart:
pass
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):
return \
{
@ -187,7 +190,9 @@ class Cart:
'shipping_method': 'US POSTAL SERVICE ' + (
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):
@ -199,20 +204,22 @@ class Cart:
response = CreateOrder().create_order(params)
return response
def get_line_options(self, options_dict):
options = ''
for key, value in options_dict.items():
options += f'{key}: {value}; '
return options
def build_bulk_list(self, order):
bulk_list = []
for item in self:
for key, value in item['variations'].items():
bulk_list.append(OrderLine(
order=order,
product=item['product'],
customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None),
unit_price=item['price'],
quantity=value['quantity'],
tax_rate=2,
))
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):
@ -232,12 +239,25 @@ class Cart:
return Coupon.objects.get(code=self.coupon_code)
return None
def get_coupon_total_for_specific_products(self):
for item in self.cart:
if item['variant'].product in self.coupon.products.all():
yield item['price_total']
def get_discount(self):
# SHIPPING
# ENTIRE_ORDER
# SPECIFIC_PRODUCT
if self.coupon:
if self.coupon.discount_value_type == DiscountValueType.FIXED:
return round(self.coupon.discount_value, 2)
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
if self.coupon.type == VoucherType.ENTIRE_ORDER:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
elif self.coupon.type == VoucherType.SPECIFIC_PRODUCT:
# Get the product in cart quantity
total = sum(self.get_coupon_total_for_specific_products())
return round((self.coupon.discount_value / Decimal('100')) * total, 2)
return Decimal('0')
def get_subtotal_price_after_discount(self):

View File

@ -1,6 +1,14 @@
from core.models import ProductCategory
from .cart import Cart
def cart(request):
return {
'cart': Cart(request)
}
def product_categories(self):
return {
'category_list': ProductCategory.objects.all()
}

View File

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

View File

@ -93,21 +93,13 @@ class CreateOrder(PayPalClient):
processed_items = [
{
# Shows within upper-right dropdown during payment approval
"name": f'{item["product"]}: ' + ', '.join([
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],
"name": str(item["variant"]),
# Item details will also be in the completed paypal.com
# transaction view
"description": item["product"].subtitle,
"description": item["variant"].product.subtitle,
"unit_amount": {
"currency_code": settings.DEFAULT_CURRENCY,
"value": f'{item["price"]}',
"value": f'{item["variant"].price}',
},
"quantity": f'{item["quantity"]}',
}

View File

@ -11,26 +11,30 @@
<section class="cart__list">
{% for item in cart %}
<div class="cart__item">
{% with product=item.product %}
{% with product=item.variant.product %}
<figure class="item__figure">
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<div class="item__info">
<h3>{{product.name}}</h3>
<h5>Grind:</h5>
{% for key, value in item.variations.items %}
<p><strong>{{ key|get_grind_display }}</strong><br>
<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>
<h4>{{ item.variant.name }}</h4>
{% for key, value in item.options.items %}
<p><strong>{{ key }}</strong>: {{ value }}</p>
{% 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 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>
{% endwith %}
</div>
@ -56,7 +60,7 @@
<td>Subtotal</td>
<td>${{ cart.get_total_price|floatformat:"2" }}</td>
</tr>
{% if cart.coupon %}
{% if cart.coupon and cart.coupon.type == 'entire_order' %}
<tr>
<td>Coupon</td>
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>

View File

@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% load static %}
{% block head %}
<script defer src="{% static 'scripts/product_list.js' %}"></script>
{% endblock %}
{% block content %}
<div class="site__banner site__banner--site">
<h1>Welcome to our new website!</h1>
<h4>NEW COOL LOOK, SAME GREAT COFFEE</h4>
</div>
{# Home > Category > "Coffee/Merchandise" #}
<article>
<div class="breadcrumbs">
<menu>
<li><strong><a href="{% url 'storefront:product-list' %}">Shop</a></strong></li>
<span></span>
<li><strong>{{ category }}</strong></a></li>
</menu>
</div>
<section class="product__list">
{% for product in category.product_set.all %}
<a class="product__item" href="{% url 'storefront:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image product__with-img-swap" data-altimg-src="{{product.get_second_img.image.url}}" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<div>
<h3>{{ product.name }}</h3>
<h5>{{ product.subtitle }}</h5>
<p>{{product.description|truncatewords:20}}</p>
<p>$<strong>{{product.variants.first.price}}</strong></p>
</div>
</a>
{% endfor %}
</section>
</article>
{% endblock %}

View File

@ -14,20 +14,7 @@
{% csrf_token %}
{{ form.non_field_errors }}
<fieldset>
<legend>{{ form.shipping_method.label }}</legend>
{% 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 %}
{{form.as_p}}
</fieldset>
<br>
<p>

View File

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

View File

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

View File

@ -21,9 +21,6 @@
<h1>{{product.name}}</h1>
<h3>{{product.subtitle}}</h3>
<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 %}">
{% csrf_token %}
{{ form.as_p }}

View File

@ -5,6 +5,14 @@
<script defer src="{% static 'scripts/product_list.js' %}"></script>
{% endblock %}
{% block product_categories %}
<ul class="nav__dropdown">
{% for category in category_list %}
<li><a class="nav__link" href="">{{ category }}</a></li>
{% endfor %}
</ul>
{% endblock product_categories %}
{% block content %}
<div class="site__banner site__banner--site">
<h1>Welcome to our new website!</h1>
@ -21,7 +29,7 @@
<h3>{{ product.name }}</h3>
<h5>{{ product.subtitle }}</h5>
<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>
</a>
{% endfor %}

View File

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

View File

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

View File

@ -6,8 +6,17 @@ urlpatterns = [
path('fair-trade/', views.FairTradeView.as_view(), name='fair-trade'),
path('reviews/', views.ReviewListView.as_view(), name='reviews'),
path('contact/', views.ContactFormView.as_view(), name='contact'),
path('subscriptions/', views.SubscriptionCreateView.as_view(), name='subscriptions'),
path(
'subscriptions/',
views.SubscriptionCreateView.as_view(),
name='subscriptions'
),
path(
'categories/<int:pk>/',
views.ProductCategoryDetailView.as_view(),
name='category-detail'
),
path('', views.ProductListView.as_view(), name='product-list'),
path('products/<int:pk>/', include([
path('', views.ProductDetailView.as_view(), name='product-detail'),
@ -20,12 +29,12 @@ urlpatterns = [
name='cart-add'
),
path(
'cart/<int:pk>/update/<slug:grind>/',
'cart/<int:pk>/update/',
views.CartUpdateProductView.as_view(),
name='cart-update',
),
path(
'cart/<int:pk>/remove/<slug:grind>/',
'cart/<int:pk>/remove/',
views.cart_remove_product_view,
name='cart-remove',
),
@ -39,11 +48,6 @@ urlpatterns = [
views.paypal_order_transaction_capture,
name='paypal-capture',
),
path(
'paypal/webhooks/',
views.paypal_webhook_endpoint,
name='paypal-webhook'
),
path(
'checkout/address/',
views.CheckoutAddressView.as_view(),

View File

@ -22,6 +22,9 @@ from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.forms.models import model_to_dict
from django.db.models import (
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value
)
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
@ -31,8 +34,11 @@ from accounts.utils import get_or_create_customer
from accounts.forms import (
AddressForm as AccountAddressForm, CustomerUpdateForm
)
from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm
from core.models import (
ProductCategory, Product, ProductOption,
Order, Transaction, OrderLine, Coupon, ShippingRate
)
from core.forms import ShippingRateForm
from core import OrderStatus, ShippingContainer
from .forms import (
@ -52,13 +58,13 @@ class CartView(TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
cart = Cart(self.request)
for item in cart:
for variation in item['variations'].values():
variation['update_quantity_form'] = UpdateCartItemForm(
initial={
'quantity': variation['quantity']
}
)
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
@ -72,23 +78,30 @@ class CartAddProductView(SingleObjectMixin, FormView):
def get_success_url(self):
return reverse('storefront:cart-detail')
def get_form(self, form_class=None):
variants = self.get_object().variants.all()
options = ProductOption.objects.filter(products__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):
cart = Cart(request)
form = self.get_form()
if form.is_valid():
cleaned_data = form.cleaned_data
cart.add(
request=request,
product=self.get_object(),
grind=form.cleaned_data['grind'],
quantity=form.cleaned_data['quantity']
item={
'variant': cleaned_data.pop('variant'),
'quantity': cleaned_data.pop('quantity'),
'options': cleaned_data
}
)
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
return super().form_valid(form)
class CartUpdateProductView(SingleObjectMixin, FormView):
model = Product
@ -104,9 +117,10 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
if form.is_valid():
cart.add(
request=request,
product=self.get_object(),
grind=kwargs['grind'],
quantity=form.cleaned_data['quantity'],
item={
'variant': form.cleaned_data['item_pk'],
'quantity': form.cleaned_data['quantity']
},
update_quantity=form.cleaned_data['update']
)
return self.form_valid(form)
@ -117,10 +131,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
return super().form_valid(form)
def cart_remove_product_view(request, pk, grind):
def cart_remove_product_view(request, pk):
cart = Cart(request)
product = get_object_or_404(Product, id=pk)
cart.remove(product, grind)
cart.remove(pk)
return redirect('storefront:cart-detail')
@ -145,14 +158,31 @@ class CouponApplyView(FormView):
return super().form_valid(form)
class ProductListView(FormMixin, ListView):
class ProductCategoryDetailView(DetailView):
model = ProductCategory
template_name = 'storefront/category_detail.html'
context_object_name = 'category'
def get_queryset(self):
object_list = ProductCategory.objects.prefetch_related(
Prefetch(
'product_set',
queryset=Product.objects.filter(
visible_in_listings=True
)
)
)
return object_list
class ProductListView(ListView):
model = Product
template_name = 'storefront/product_list.html'
form_class = AddToCartForm
ordering = 'sorting'
queryset = Product.objects.filter(
visible_in_listings=True
visible_in_listings=True,
category__main_category=True
)
@ -161,6 +191,13 @@ class ProductDetailView(FormMixin, DetailView):
template_name = 'storefront/product_detail.html'
form_class = AddToCartForm
def get_form(self, form_class=None):
variants = self.object.variants.all()
options = ProductOption.objects.filter(products__pk=self.object.pk)
if form_class is None:
form_class = self.get_form_class()
return form_class(variants, options, **self.get_form_kwargs())
class CheckoutAddressView(FormView):
template_name = 'storefront/checkout_address.html'
@ -220,38 +257,27 @@ class CheckoutShippingView(FormView):
success_url = reverse_lazy('storefront:order-create')
def get(self, request, *args, **kwargs):
cart = Cart(request)
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"):
if not self.request.session.get('shipping_address'):
messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect(
reverse('storefront:checkout-address')
)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
def get_form(self, form_class=None):
cart = Cart(self.request)
context = super().get_context_data(**kwargs)
context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost(
ShippingContainer.MD_FLAT_RATE_BOX
)
context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost(
ShippingContainer.REGIONAL_RATE_BOX_B
)
return context
containers = cart.get_shipping_container_choices()
for container in containers:
container.s_cost = cart.get_shipping_cost(container.container)
if form_class is None:
form_class = self.get_form_class()
return form_class(containers, **self.get_form_kwargs())
def form_valid(self, form):
cleaned_data = form.cleaned_data
self.request.session['shipping_container'] = cleaned_data.get(
'shipping_method'
shipping_container = ShippingRate.objects.get(
pk=form.cleaned_data.get('shipping_method')
)
self.request.session['shipping_container'] = shipping_container
return super().form_valid(form)
@ -262,17 +288,13 @@ class OrderCreateView(CreateView):
success_url = reverse_lazy('storefront:payment-done')
def get(self, request, *args, **kwargs):
cart = Cart(request)
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"):
if not self.request.session.get('shipping_address'):
messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect(
reverse('storefront:checkout-address')
)
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(
code=self.request.session.get('coupon_code')
)
@ -283,19 +305,22 @@ class OrderCreateView(CreateView):
if user in coupon.users.all():
del self.request.session['coupon_code']
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
try:
shipping_cost = cart.get_shipping_cost()
shipping_cost = cart.get_shipping_cost(shipping_container)
except Exception as e:
raise e('Could not get shipping information')
logger.error('Could not get shipping information')
raise
shipping_cost = Decimal('0.00')
initial = {
'total_net_amount': cart.get_total_price(),
'total_amount': cart.get_total_price(),
'shipping_total': shipping_cost
}
if self.request.session.get('shipping_address'):
@ -317,8 +342,12 @@ 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()
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.status = OrderStatus.DRAFT
self.object = form.save()
@ -364,14 +393,6 @@ def paypal_order_transaction_capture(request, transaction_id):
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):
template_name = 'storefront/payment_done.html'

View File

@ -46,7 +46,14 @@
<nav class="site__nav">
<a class="site__logo" href="{% url 'storefront:product-list' %}"><img src="{% static 'images/site_logo.svg' %}" alt="Port Townsend Roasting Co."></a>
<ul class="nav__list nav__main">
<li><a class="nav__link" href="{% url 'storefront:product-list' %}">Shop</a></li>
<li class="nav__menu">
<a class="nav__link" href="{% url 'storefront:product-list' %}">Shop ▼</a>
<ul class="nav__dropdown">
{% for category in category_list %}
<li><a class="nav__link" href="{% url 'storefront:category-detail' category.pk %}">{{ category }}</a></li>
{% endfor %}
</ul>
</li>
<li><a class="nav__link" href="{% url 'storefront:subscriptions' %}">Subscriptions</a></li>
<li><a class="nav__link" href="{% url 'storefront:fair-trade' %}">Fair trade</a></li>
<li><a class="nav__link" href="{% url 'storefront:reviews' %}">Reviews</a></li>

View File

@ -29,7 +29,7 @@
<img src="{% static 'images/store.png' %}" alt="">
Home
</a>
<a href="{% url 'dashboard:product-list' %}">
<a href="{% url 'dashboard:catalog' %}">
<img src="{% static 'images/cubes.png' %}" alt="">
Catalog
</a>