diff --git a/src/core/fixtures/merchandise.json b/src/core/fixtures/merchandise.json
new file mode 100644
index 0000000..95338a8
--- /dev/null
+++ b/src/core/fixtures/merchandise.json
@@ -0,0 +1,369 @@
+[{
+ "model": "core.product",
+ "pk": 10,
+ "fields": {
+ "category": 2,
+ "name": "BLTC Mug",
+ "subtitle": "Camper Vacuum Mug",
+ "description": "Camp mug is made of double-wall stainless steel, designed to keep drinks hot or cold for hours.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 10,
+ "name": "Blue - 12 oz.",
+ "sku": "REPLACE_4001",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 10,
+ "image": "products/images/bltc_mug_blue_12_oz.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 11,
+ "fields": {
+ "category": 2,
+ "name": "BLTC Pull-over Hoodie",
+ "subtitle": "Classic BLTC Logo front left and Steam/Waves on back",
+ "description": "ComfortBlend Hoodie - Screen. EcoSmart® 7.8-oz poly-cotton blend hoodie features a front pouch pocket and two-ply hood with matching drawcord. Rib-knit cuffs and waistband ensure all-day comfort and warmth.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 11,
+ "name": "Blue",
+ "sku": "REPLACE_4002",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 11,
+ "image": "products/images/bltc_pullover_hoodie_blue_front.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 11,
+ "image": "products/images/bltc_pullover_hoodie_blue_back.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 12,
+ "fields": {
+ "category": 2,
+ "name": "BLTC T-Shirt",
+ "subtitle": "Logo T-Shirt with Steam/Waves on back",
+ "description": "5.5 oz. DryBlend 50/50 T-Shirt - Screen. The 5.5-oz 50/50 cotton/polyester blend fabric is moisture-wicking.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "name": "Heather Red",
+ "sku": "REPLACE_4003",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "name": "Heather Blue",
+ "sku": "REPLACE_4004",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "image": "products/images/bltc_tshirt_blue_front.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "image": "products/images/bltc_tshirt_blue_back.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "image": "products/images/bltc_tshirt_red_front.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 12,
+ "image": "products/images/bltc_tshirt_red_back.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 13,
+ "fields": {
+ "category": 2,
+ "name": "BLTC Trucker Cap",
+ "subtitle": "Washed Cotton Mesh Back Cap with Logo",
+ "description": "Soft washed cotton gives this trucker hat an appealing vintage look. Designed with ventilated mesh back panels and a front twill sweatband for comfort.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 13,
+ "name": "Black and White",
+ "sku": "REPLACE_4005",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 13,
+ "image": "products/images/bltc_trucker_cap_black_and_white.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 14,
+ "fields": {
+ "category": 2,
+ "name": "BLTC Zip-up Hoodie",
+ "subtitle": "Classic BLTC Cafe Logo without the extra branding",
+ "description": "Made from a 7.1-oz, 52/48 cotton/polyester blend. Includes a spacious hood with drawcords and metal grommets. Interior phone pocket with headphone cord port.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "name": "Forest Green/Silver Embroidered",
+ "sku": "REPLACE_4006",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "name": "Navy Blue/Silver Embroidered",
+ "sku": "REPLACE_4007",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "name": "Black/Gold Embroidered",
+ "sku": "REPLACE_4008",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "image": "products/images/bltc_zipup_hoodie_black_gold.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "image": "products/images/bltc_zipup_hoodie_green_silver.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 14,
+ "image": "products/images/bltc_zipup_hoodie_navy_silver.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 15,
+ "fields": {
+ "category": 2,
+ "name": "PT Coffee Cap",
+ "subtitle": "Embroidered Buttonless Cap",
+ "description": "These caps feature a buttonless design that won't interfere with headsets/headphones. The low profile fit and adjustable fabric strap with hook-and-loop closure helps create a more comfortable fit.",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 15,
+ "name": "Navy Blue with Red Embroidered Print",
+ "sku": "REPLACE_4009",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 15,
+ "image": "products/images/pt_coffee_cap_navy.jpg"
+ }
+},
+
+
+{
+ "model": "core.product",
+ "pk": 16,
+ "fields": {
+ "category": 2,
+ "name": "PT Coffee Cup",
+ "subtitle": "Bean/Leaf Logo Neo Vacuum Insulated Cup",
+ "description": "Designed to keep drinks hot or cold for hours. A great cup for enjoying wine, tea, coffee, hot chocolate, and more!",
+ "checkout_limit": 20,
+ "visible_in_listings": false,
+ "sorting": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 16,
+ "name": "Black and Gold 10 oz.",
+ "sku": "REPLACE_4010",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": null,
+ "fields": {
+ "product": 16,
+ "name": "White and Black 10 oz.",
+ "sku": "REPLACE_4011",
+ "price": null,
+ "weight": "00:oz",
+ "track_inventory": true,
+ "stock": 0,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 16,
+ "image": "products/images/pt_coffee_cup_black_gold_10_oz.jpg"
+ }
+}, {
+ "model": "core.productphoto",
+ "pk": null,
+ "fields": {
+ "product": 16,
+ "image": "products/images/pt_coffee_cup_white_black_10_oz.jpg"
+ }
+}]
diff --git a/src/core/fixtures/new_coffee_variants.json b/src/core/fixtures/new_coffee_variants.json
new file mode 100644
index 0000000..1a38570
--- /dev/null
+++ b/src/core/fixtures/new_coffee_variants.json
@@ -0,0 +1,289 @@
+[{
+ "model": "core.productvariant",
+ "pk": 10,
+ "fields": {
+ "product": 1,
+ "name": "12 oz",
+ "sku": "REPLACE_87431",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 11,
+ "fields": {
+ "product": 1,
+ "name": "5 lb",
+ "sku": "REPLACE_87430",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 12,
+ "fields": {
+ "product": 2,
+ "name": "12 oz",
+ "sku": "REPLACE_87429",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 13,
+ "fields": {
+ "product": 2,
+ "name": "5 lb",
+ "sku": "REPLACE_87428",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 14,
+ "fields": {
+ "product": 3,
+ "name": "12 oz",
+ "sku": "REPLACE_87427",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 15,
+ "fields": {
+ "product": 3,
+ "name": "5 lb",
+ "sku": "REPLACE_87426",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 16,
+ "fields": {
+ "product": 4,
+ "name": "12 oz",
+ "sku": "REPLACE_87425",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 17,
+ "fields": {
+ "product": 4,
+ "name": "5 lb",
+ "sku": "REPLACE_87424",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 18,
+ "fields": {
+ "product": 5,
+ "name": "12 oz",
+ "sku": "REPLACE_87423",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 19,
+ "fields": {
+ "product": 5,
+ "name": "5 lb",
+ "sku": "REPLACE_87422",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 20,
+ "fields": {
+ "product": 6,
+ "name": "12 oz",
+ "sku": "REPLACE_87421",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 21,
+ "fields": {
+ "product": 6,
+ "name": "5 lb",
+ "sku": "REPLACE_87420",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 22,
+ "fields": {
+ "product": 7,
+ "name": "12 oz",
+ "sku": "REPLACE_87419",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 23,
+ "fields": {
+ "product": 7,
+ "name": "5 lb",
+ "sku": "REPLACE_87418",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 24,
+ "fields": {
+ "product": 8,
+ "name": "12 oz",
+ "sku": "REPLACE_87417",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 25,
+ "fields": {
+ "product": 8,
+ "name": "5 lb",
+ "sku": "REPLACE_87416",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 26,
+ "fields": {
+ "product": 9,
+ "name": "12 oz",
+ "sku": "REPLACE_87467",
+ "price": "12.00",
+ "weight": "12.0:oz",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 1,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}, {
+ "model": "core.productvariant",
+ "pk": 27,
+ "fields": {
+ "product": 9,
+ "name": "5 lb",
+ "sku": "REPLACE_87486",
+ "price": "75.00",
+ "weight": "5.0:lb",
+ "track_inventory": false,
+ "stock": null,
+ "sorting": 3,
+ "image": null,
+ "created_at": "2022-02-23T18:06:57.624Z",
+ "updated_at": "2022-02-23T18:06:57.624Z"
+ }
+}]
diff --git a/src/core/fixtures/shipping_rates.json b/src/core/fixtures/shipping_rates.json
new file mode 100644
index 0000000..17fbc42
--- /dev/null
+++ b/src/core/fixtures/shipping_rates.json
@@ -0,0 +1,19 @@
+[{
+ "model": "core.shippingrate",
+ "pk": 1,
+ "fields": {
+ "shipping_provider": "USPS",
+ "name": "Variable",
+ "container": "VARIABLE",
+ "min_order_weight": null,
+ "max_order_weight": null,
+ "is_selectable": false
+ }
+}, {
+ "model": "core.sitesettings",
+ "pk": 1,
+ "fields": {
+ "usps_user_id": "012BETTE1249",
+ "default_shipping_rate": 1
+ }
+}]
diff --git a/src/core/migrations/0003_alter_coupon_options.py b/src/core/migrations/0003_alter_coupon_options.py
new file mode 100644
index 0000000..c1cd2e1
--- /dev/null
+++ b/src/core/migrations/0003_alter_coupon_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.0.2 on 2022-11-02 00:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0002_shippingrate_is_selectable_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='coupon',
+ options={'ordering': ['valid_from', 'valid_to', 'code']},
+ ),
+ ]
diff --git a/src/core/migrations/0004_alter_coupon_options.py b/src/core/migrations/0004_alter_coupon_options.py
new file mode 100644
index 0000000..b849b55
--- /dev/null
+++ b/src/core/migrations/0004_alter_coupon_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.0.2 on 2022-11-02 00:31
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0003_alter_coupon_options'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='coupon',
+ options={'ordering': ['-valid_from', '-valid_to', 'code']},
+ ),
+ ]
diff --git a/src/core/migrations/0005_sitesettings_free_shipping_min.py b/src/core/migrations/0005_sitesettings_free_shipping_min.py
new file mode 100644
index 0000000..8aad562
--- /dev/null
+++ b/src/core/migrations/0005_sitesettings_free_shipping_min.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.2 on 2022-11-05 00:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0004_alter_coupon_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='sitesettings',
+ name='free_shipping_min',
+ field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True),
+ ),
+ ]
diff --git a/src/core/migrations/0006_productvariant_sorting_and_more.py b/src/core/migrations/0006_productvariant_sorting_and_more.py
new file mode 100644
index 0000000..09208fe
--- /dev/null
+++ b/src/core/migrations/0006_productvariant_sorting_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.0.2 on 2022-11-05 15:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0005_sitesettings_free_shipping_min'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='productvariant',
+ name='sorting',
+ field=models.PositiveIntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='sitesettings',
+ name='free_shipping_min',
+ field=models.DecimalField(blank=True, decimal_places=2, help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping', max_digits=12, null=True),
+ ),
+ ]
diff --git a/src/core/migrations/0007_alter_productvariant_options.py b/src/core/migrations/0007_alter_productvariant_options.py
new file mode 100644
index 0000000..0b0d344
--- /dev/null
+++ b/src/core/migrations/0007_alter_productvariant_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.0.2 on 2022-11-05 16:13
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0006_productvariant_sorting_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='productvariant',
+ options={'ordering': ['sorting', 'weight']},
+ ),
+ ]
diff --git a/src/core/migrations/0008_productvariant_image.py b/src/core/migrations/0008_productvariant_image.py
new file mode 100644
index 0000000..86c1278
--- /dev/null
+++ b/src/core/migrations/0008_productvariant_image.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.0.2 on 2022-11-05 17:26
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0007_alter_productvariant_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='productvariant',
+ name='image',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.productphoto'),
+ ),
+ ]
diff --git a/src/core/models.py b/src/core/models.py
index 3e290e7..d53decf 100644
--- a/src/core/models.py
+++ b/src/core/models.py
@@ -71,6 +71,14 @@ class ProductCategory(models.Model):
verbose_name_plural = 'Product Categories'
+class ProductManager(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().prefetch_related(
+ 'productphoto_set',
+ 'options'
+ )
+
+
class Product(models.Model):
category = models.ForeignKey(
ProductCategory,
@@ -92,6 +100,8 @@ 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
@@ -111,6 +121,29 @@ class Product(models.Model):
ordering = ['sorting', 'name']
+class ProductPhoto(models.Model):
+ product = models.ForeignKey(Product, on_delete=models.CASCADE)
+ image = models.ImageField(upload_to='products/images')
+
+ def __str__(self):
+ return f'{self.product.name} {self.image}'
+
+ def delete(self, *args, **kwargs):
+ storage, path = self.image.storage, self.image.path
+
+ super(ProductPhoto, self).delete(*args, **kwargs)
+ storage.delete(path)
+
+ # def save(self, *args, **kwargs):
+ # super().save(*args, **kwargs)
+
+ # img = Image.open(self.image.path)
+ # if img.height > 400 or img.width > 400:
+ # output_size = (400, 400)
+ # img.thumbnail(output_size)
+ # img.save(self.image.path)
+
+
class ProductVariantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
@@ -124,6 +157,12 @@ class ProductVariant(models.Model):
on_delete=models.CASCADE,
related_name='variants'
)
+ image = models.ForeignKey(
+ ProductPhoto,
+ on_delete=models.SET_NULL,
+ related_name='+',
+ null=True
+ )
name = models.CharField(max_length=255)
sku = models.CharField(max_length=255, unique=True)
stripe_id = models.CharField(max_length=255, blank=True)
@@ -145,6 +184,7 @@ class ProductVariant(models.Model):
null=True,
validators=[MinValueValidator(0)]
)
+ sorting = models.PositiveIntegerField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -155,7 +195,7 @@ class ProductVariant(models.Model):
return f'{self.product}: {self.name}'
class Meta:
- ordering = ['weight']
+ ordering = ['sorting', 'weight']
class ProductOption(models.Model):
@@ -178,29 +218,6 @@ class ProductOption(models.Model):
return f'{self.name}'
-class ProductPhoto(models.Model):
- product = models.ForeignKey(Product, on_delete=models.CASCADE)
- image = models.ImageField(upload_to='products/images')
-
- def __str__(self):
- return self.product.name
-
- def delete(self, *args, **kwargs):
- storage, path = self.image.storage, self.image.path
-
- super(ProductPhoto, self).delete(*args, **kwargs)
- storage.delete(path)
-
- # def save(self, *args, **kwargs):
- # super().save(*args, **kwargs)
-
- # img = Image.open(self.image.path)
- # if img.height > 400 or img.width > 400:
- # output_size = (400, 400)
- # img.thumbnail(output_size)
- # img.save(self.image.path)
-
-
class Coupon(models.Model):
type = models.CharField(
max_length=20,
@@ -226,7 +243,7 @@ class Coupon(models.Model):
users = models.ManyToManyField(User, blank=True)
class Meta:
- ordering = ("code",)
+ ordering = ['-valid_from', '-valid_to', 'code']
def __str__(self):
return self.name
@@ -491,6 +508,13 @@ class SiteSettings(SingletonBase):
related_name='+',
on_delete=models.SET_NULL
)
+ free_shipping_min = models.DecimalField(
+ max_digits=settings.DEFAULT_MAX_DIGITS,
+ decimal_places=settings.DEFAULT_DECIMAL_PLACES,
+ blank=True,
+ null=True,
+ help_text='Minimum dollar amount in the cart subtotal to qualify for free shipping'
+ )
def __str__(self):
return 'Site Settings'
diff --git a/src/core/tests/test_models.py b/src/core/tests/test_models.py
index 841f14f..d3df44f 100644
--- a/src/core/tests/test_models.py
+++ b/src/core/tests/test_models.py
@@ -23,10 +23,10 @@ class ProductModelTest(TestCase):
Product.objects.create(
name='Pantomime',
subtitle='Very Dark French Roast',
- description='Our darkest drip. A blend of five different beans roasted two ways. Organic Africa, Indonesia, and South and Central America.',
- sku='565656',
- price=Decimal('15.00'),
- weight=Weight(oz=16),
+ description='Our darkest drip. A blend of five different \
+ beans roasted two ways. Organic Africa, Indonesia, and \
+ South and Central America.',
+ checkout_limit=10,
visible_in_listings=True,
sorting=1,
)
diff --git a/src/dashboard/forms.py b/src/dashboard/forms.py
index 3d75eff..b43e9d0 100644
--- a/src/dashboard/forms.py
+++ b/src/dashboard/forms.py
@@ -3,6 +3,7 @@ from django import forms
from core import OrderStatus
from core.models import (
+ ProductVariant,
Order,
OrderLine,
ShippingRate,
@@ -14,6 +15,27 @@ from core.models import (
logger = logging.getLogger(__name__)
+class ProductVariantUpdateForm(forms.ModelForm):
+ class Meta:
+ model = ProductVariant
+ fields = [
+ 'name',
+ 'sku',
+ 'price',
+ 'weight',
+ 'track_inventory',
+ 'stock',
+ 'image'
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance:
+ self.fields['image'].queryset = ProductPhoto.objects.filter(
+ product=self.instance.product
+ )
+
+
class CouponForm(forms.ModelForm):
class Meta:
model = Coupon
diff --git a/src/dashboard/templates/dashboard/config.html b/src/dashboard/templates/dashboard/config.html
index 5e56eb0..70bf292 100644
--- a/src/dashboard/templates/dashboard/config.html
+++ b/src/dashboard/templates/dashboard/config.html
@@ -23,5 +23,17 @@
{% endfor %}
+
+ USPS User ID: {{ site_settings.usps_user_id }} Default shipping rate: {{ site_settings.default_shipping_rate }} Free shipping min: ${{ site_settings.free_shipping_min }}Site Settings
+ Edit
+
{{ key }}: {{ value }}
{% endfor %} @@ -54,6 +58,7 @@ +|
+ {% if item.variant.image %}
+ |
{{ item.variant }} {{item.customer_note}} |
{{item.quantity}} | -${{product.price}} | +${{item.unit_price}} | ${{item.get_total}} | {% endwith %}
| Shipping | -${{ cart.get_shipping_cost }} | +${{ cart.get_shipping_cost }} | |||
| Total | diff --git a/src/storefront/templates/storefront/product_list.html b/src/storefront/templates/storefront/product_list.html index ae9b85f..70535b9 100644 --- a/src/storefront/templates/storefront/product_list.html +++ b/src/storefront/templates/storefront/product_list.html @@ -15,8 +15,7 @@ {% block content %}
|---|