Merge branch 'feature/updated-products' into develop
This commit is contained in:
commit
8b4eb80fbb
@ -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):
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
5
src/core/context_processors.py
Normal file
5
src/core/context_processors.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .models import SiteSettings
|
||||
|
||||
|
||||
def site_settings(request):
|
||||
return {'site_settings': SiteSettings.load()}
|
||||
@ -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"
|
||||
|
||||
@ -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__'
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
19
src/core/migrations/0012_alter_productvariant_product.py
Normal file
19
src/core/migrations/0012_alter_productvariant_product.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
18
src/core/migrations/0015_productcategory_main_product.py
Normal file
18
src/core/migrations/0015_productcategory_main_product.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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):
|
||||
|
||||
@ -7,7 +7,7 @@ from core.models import (
|
||||
Product,
|
||||
ProductPhoto,
|
||||
Coupon,
|
||||
ShippingMethod,
|
||||
ShippingRate,
|
||||
Order,
|
||||
Transaction,
|
||||
OrderLine,
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
|
||||
@ -5,7 +5,7 @@ from core import OrderStatus
|
||||
from core.models import (
|
||||
Order,
|
||||
OrderLine,
|
||||
ShippingMethod,
|
||||
ShippingRate,
|
||||
TrackingNumber,
|
||||
Coupon,
|
||||
ProductPhoto
|
||||
|
||||
76
src/dashboard/templates/dashboard/catalog.html
Normal file
76
src/dashboard/templates/dashboard/catalog.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
18
src/dashboard/templates/dashboard/category_create_form.html
Normal file
18
src/dashboard/templates/dashboard/category_create_form.html
Normal 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 %}
|
||||
24
src/dashboard/templates/dashboard/category_detail.html
Normal file
24
src/dashboard/templates/dashboard/category_detail.html
Normal 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 %}
|
||||
18
src/dashboard/templates/dashboard/category_form.html
Normal file
18
src/dashboard/templates/dashboard/category_form.html
Normal 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 %}
|
||||
25
src/dashboard/templates/dashboard/category_list.html
Normal file
25
src/dashboard/templates/dashboard/category_list.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
19
src/dashboard/templates/dashboard/option_confirm_delete.html
Normal file
19
src/dashboard/templates/dashboard/option_confirm_delete.html
Normal 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 %}
|
||||
18
src/dashboard/templates/dashboard/option_create_form.html
Normal file
18
src/dashboard/templates/dashboard/option_create_form.html
Normal 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 %}
|
||||
24
src/dashboard/templates/dashboard/option_detail.html
Normal file
24
src/dashboard/templates/dashboard/option_detail.html
Normal 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 %}
|
||||
0
src/dashboard/templates/dashboard/option_form.html
Normal file
0
src/dashboard/templates/dashboard/option_form.html
Normal 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 ↓</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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
19
src/dashboard/templates/dashboard/rate_confirm_delete.html
Normal file
19
src/dashboard/templates/dashboard/rate_confirm_delete.html
Normal 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 %}
|
||||
@ -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>
|
||||
22
src/dashboard/templates/dashboard/rate_detail.html
Normal file
22
src/dashboard/templates/dashboard/rate_detail.html
Normal 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 }} – {{ rate.max_order_weight }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
18
src/dashboard/templates/dashboard/rate_form.html
Normal file
18
src/dashboard/templates/dashboard/rate_form.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
18
src/dashboard/templates/dashboard/variant_create_form.html
Normal file
18
src/dashboard/templates/dashboard/variant_create_form.html
Normal 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 %}
|
||||
19
src/dashboard/templates/dashboard/variant_form.html
Normal file
19
src/dashboard/templates/dashboard/variant_form.html
Normal 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 %}
|
||||
@ -20,8 +20,8 @@ from dashboard.forms import (
|
||||
from dashboard.views import (
|
||||
DashboardHomeView,
|
||||
DashboardConfigView,
|
||||
ShippingMethodCreateView,
|
||||
ShippingMethodDetailView,
|
||||
ShippingRateCreateView,
|
||||
ShippingRateDetailView,
|
||||
CouponListView,
|
||||
CouponCreateView,
|
||||
CouponDetailView,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -367,7 +367,7 @@ main article {
|
||||
}
|
||||
|
||||
.product__figure img {
|
||||
max-height: 400px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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"]}',
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
39
src/storefront/templates/storefront/category_detail.html
Normal file
39
src/storefront/templates/storefront/category_detail.html
Normal 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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }} × ${{ 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>
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user