Merge branch 'release/2.0.3'
This commit is contained in:
commit
951562e971
369
src/core/fixtures/merchandise.json
Normal file
369
src/core/fixtures/merchandise.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}]
|
||||||
289
src/core/fixtures/new_coffee_variants.json
Normal file
289
src/core/fixtures/new_coffee_variants.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}]
|
||||||
19
src/core/fixtures/shipping_rates.json
Normal file
19
src/core/fixtures/shipping_rates.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}]
|
||||||
17
src/core/migrations/0003_alter_coupon_options.py
Normal file
17
src/core/migrations/0003_alter_coupon_options.py
Normal file
@ -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']},
|
||||||
|
),
|
||||||
|
]
|
||||||
17
src/core/migrations/0004_alter_coupon_options.py
Normal file
17
src/core/migrations/0004_alter_coupon_options.py
Normal file
@ -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']},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
src/core/migrations/0005_sitesettings_free_shipping_min.py
Normal file
18
src/core/migrations/0005_sitesettings_free_shipping_min.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
src/core/migrations/0006_productvariant_sorting_and_more.py
Normal file
23
src/core/migrations/0006_productvariant_sorting_and_more.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
src/core/migrations/0007_alter_productvariant_options.py
Normal file
17
src/core/migrations/0007_alter_productvariant_options.py
Normal file
@ -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']},
|
||||||
|
),
|
||||||
|
]
|
||||||
19
src/core/migrations/0008_productvariant_image.py
Normal file
19
src/core/migrations/0008_productvariant_image.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -71,6 +71,14 @@ class ProductCategory(models.Model):
|
|||||||
verbose_name_plural = 'Product Categories'
|
verbose_name_plural = 'Product Categories'
|
||||||
|
|
||||||
|
|
||||||
|
class ProductManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().prefetch_related(
|
||||||
|
'productphoto_set',
|
||||||
|
'options'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
ProductCategory,
|
ProductCategory,
|
||||||
@ -92,6 +100,8 @@ class Product(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = ProductManager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@ -111,6 +121,29 @@ class Product(models.Model):
|
|||||||
ordering = ['sorting', 'name']
|
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):
|
class ProductVariantManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().annotate(
|
return super().get_queryset().annotate(
|
||||||
@ -124,6 +157,12 @@ class ProductVariant(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='variants'
|
related_name='variants'
|
||||||
)
|
)
|
||||||
|
image = models.ForeignKey(
|
||||||
|
ProductPhoto,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
sku = models.CharField(max_length=255, unique=True)
|
sku = models.CharField(max_length=255, unique=True)
|
||||||
stripe_id = models.CharField(max_length=255, blank=True)
|
stripe_id = models.CharField(max_length=255, blank=True)
|
||||||
@ -145,6 +184,7 @@ class ProductVariant(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(0)]
|
validators=[MinValueValidator(0)]
|
||||||
)
|
)
|
||||||
|
sorting = models.PositiveIntegerField(blank=True, null=True)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@ -155,7 +195,7 @@ class ProductVariant(models.Model):
|
|||||||
return f'{self.product}: {self.name}'
|
return f'{self.product}: {self.name}'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['weight']
|
ordering = ['sorting', 'weight']
|
||||||
|
|
||||||
|
|
||||||
class ProductOption(models.Model):
|
class ProductOption(models.Model):
|
||||||
@ -178,29 +218,6 @@ class ProductOption(models.Model):
|
|||||||
return f'{self.name}'
|
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):
|
class Coupon(models.Model):
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
@ -226,7 +243,7 @@ class Coupon(models.Model):
|
|||||||
users = models.ManyToManyField(User, blank=True)
|
users = models.ManyToManyField(User, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("code",)
|
ordering = ['-valid_from', '-valid_to', 'code']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -491,6 +508,13 @@ class SiteSettings(SingletonBase):
|
|||||||
related_name='+',
|
related_name='+',
|
||||||
on_delete=models.SET_NULL
|
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):
|
def __str__(self):
|
||||||
return 'Site Settings'
|
return 'Site Settings'
|
||||||
|
|||||||
@ -23,10 +23,10 @@ class ProductModelTest(TestCase):
|
|||||||
Product.objects.create(
|
Product.objects.create(
|
||||||
name='Pantomime',
|
name='Pantomime',
|
||||||
subtitle='Very Dark French Roast',
|
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.',
|
description='Our darkest drip. A blend of five different \
|
||||||
sku='565656',
|
beans roasted two ways. Organic Africa, Indonesia, and \
|
||||||
price=Decimal('15.00'),
|
South and Central America.',
|
||||||
weight=Weight(oz=16),
|
checkout_limit=10,
|
||||||
visible_in_listings=True,
|
visible_in_listings=True,
|
||||||
sorting=1,
|
sorting=1,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from django import forms
|
|||||||
|
|
||||||
from core import OrderStatus
|
from core import OrderStatus
|
||||||
from core.models import (
|
from core.models import (
|
||||||
|
ProductVariant,
|
||||||
Order,
|
Order,
|
||||||
OrderLine,
|
OrderLine,
|
||||||
ShippingRate,
|
ShippingRate,
|
||||||
@ -14,6 +15,27 @@ from core.models import (
|
|||||||
logger = logging.getLogger(__name__)
|
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 CouponForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Coupon
|
model = Coupon
|
||||||
|
|||||||
@ -23,5 +23,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="object__panel">
|
||||||
|
<div class="object__item panel__header panel__header--flex">
|
||||||
|
<h4>Site Settings</h4>
|
||||||
|
<a href="{% url 'dashboard:settings-update' site_settings.pk %}" class="action-button order__fulfill">Edit</a>
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<p>USPS User ID: {{ site_settings.usps_user_id }}</p>
|
||||||
|
<p>Default shipping rate: {{ site_settings.default_shipping_rate }}</p>
|
||||||
|
<p>Free shipping min: ${{ site_settings.free_shipping_min }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</article>
|
</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -25,12 +25,16 @@
|
|||||||
<div class="object__item object__item--col5">
|
<div class="object__item object__item--col5">
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<figure class="item__figure">
|
<figure class="item__figure">
|
||||||
<img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
{% if item.variant.image %}
|
||||||
|
<img class="item__image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
|
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
<figcaption><strong>{{item.variant}}</strong><br>{{item.customer_note}}</figcaption>
|
<figcaption><strong>{{item.variant}}</strong><br>{{item.customer_note}}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<span>{{product.sku}}</span>
|
<span>{{product.sku}}</span>
|
||||||
<span>{{item.quantity}}</span>
|
<span>{{item.quantity}}</span>
|
||||||
<span>${{item.variant.price}}</span>
|
<span>${{item.unit_price}}</span>
|
||||||
<span>${{item.get_total}}</span>
|
<span>${{item.get_total}}</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{form.as_p}}
|
{{form.as_p}}
|
||||||
<p class="form__submit">
|
<p class="form__submit">
|
||||||
<input class="action-button" type="submit" value="Create product"> or <a href="{% url 'dashboard:product-list' %}">cancel</a>
|
<input class="action-button" type="submit" value="Create product"> or <a href="{% url 'dashboard:catalog' %}">cancel</a>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
18
src/dashboard/templates/dashboard/settings_form.html
Normal file
18
src/dashboard/templates/dashboard/settings_form.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="product">
|
||||||
|
<header class="object__header">
|
||||||
|
<h1>Update Site Settings</h1>
|
||||||
|
</header>
|
||||||
|
<section class="object__panel">
|
||||||
|
<form class="panel__item" method="POST" action="{% url 'dashboard:settings-update' settings.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p class="form__submit">
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:config' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
@ -19,7 +19,11 @@
|
|||||||
<div class="object__item object__item--col5">
|
<div class="object__item object__item--col5">
|
||||||
{% with product=variant.product %}
|
{% with product=variant.product %}
|
||||||
<figure class="item__figure">
|
<figure class="item__figure">
|
||||||
|
{% if variant.image %}
|
||||||
|
<img class="item__image" src="{{variant.image.image.url}}" alt="{{variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
<img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
<img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
<figcaption><strong>{{variant}}</strong></figcaption>
|
<figcaption><strong>{{variant}}</strong></figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<span>{{ variant.sku }}</span>
|
<span>{{ variant.sku }}</span>
|
||||||
|
|||||||
@ -20,8 +20,12 @@ from dashboard.forms import (
|
|||||||
from dashboard.views import (
|
from dashboard.views import (
|
||||||
DashboardHomeView,
|
DashboardHomeView,
|
||||||
DashboardConfigView,
|
DashboardConfigView,
|
||||||
ShippingRateCreateView,
|
CatalogView,
|
||||||
|
StockView,
|
||||||
ShippingRateDetailView,
|
ShippingRateDetailView,
|
||||||
|
ShippingRateCreateView,
|
||||||
|
ShippingRateUpdateView,
|
||||||
|
ShippingRateDeleteView,
|
||||||
CouponListView,
|
CouponListView,
|
||||||
CouponCreateView,
|
CouponCreateView,
|
||||||
CouponDetailView,
|
CouponDetailView,
|
||||||
@ -30,24 +34,74 @@ from dashboard.views import (
|
|||||||
OrderListView,
|
OrderListView,
|
||||||
OrderDetailView,
|
OrderDetailView,
|
||||||
OrderFulfillView,
|
OrderFulfillView,
|
||||||
|
OrderCancelView,
|
||||||
OrderTrackingView,
|
OrderTrackingView,
|
||||||
|
CategoryListView,
|
||||||
|
CategoryCreateView,
|
||||||
|
CategoryDetailView,
|
||||||
|
CategoryUpdateView,
|
||||||
|
CategoryDeleteView,
|
||||||
ProductListView,
|
ProductListView,
|
||||||
ProductDetailView,
|
ProductDetailView,
|
||||||
ProductUpdateView,
|
|
||||||
ProductCreateView,
|
ProductCreateView,
|
||||||
|
ProductUpdateView,
|
||||||
ProductDeleteView,
|
ProductDeleteView,
|
||||||
ProductPhotoCreateView,
|
ProductPhotoCreateView,
|
||||||
ProductPhotoDeleteView,
|
ProductPhotoDeleteView,
|
||||||
|
ProductVariantCreateView,
|
||||||
|
ProductVariantUpdateView,
|
||||||
|
ProductVariantDeleteView,
|
||||||
|
ProductVariantStockUpdateView,
|
||||||
|
ProductOptionDetailView,
|
||||||
|
ProductOptionCreateView,
|
||||||
|
ProductOptionUpdateView,
|
||||||
|
ProductOptionDeleteView,
|
||||||
CustomerListView,
|
CustomerListView,
|
||||||
CustomerDetailView,
|
CustomerDetailView,
|
||||||
CustomerUpdateView
|
CustomerUpdateView,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCreateViewTests(TestCase):
|
||||||
|
fixtures = [
|
||||||
|
'shipping_rates.json',
|
||||||
|
'accounts.json',
|
||||||
|
'coupons.json',
|
||||||
|
'products.json',
|
||||||
|
'orders.json'
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.admin_user = User.objects.get(pk=1)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(self.admin_user)
|
||||||
|
|
||||||
|
def test_view_url_exists_at_desired_location(self):
|
||||||
|
response = self.client.get('/dashboard/products/new/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_view_url_accesible_by_name(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('dashboard:product-create')
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_view_uses_correct_template(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('dashboard:product-create')
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'dashboard/product_create_form.html')
|
||||||
|
|
||||||
|
|
||||||
class OrderCancelViewTests(TestCase):
|
class OrderCancelViewTests(TestCase):
|
||||||
fixtures = [
|
fixtures = [
|
||||||
|
'shipping_rates.json',
|
||||||
'accounts.json',
|
'accounts.json',
|
||||||
'coupons.json',
|
'coupons.json',
|
||||||
'products.json',
|
'products.json',
|
||||||
|
|||||||
@ -12,6 +12,11 @@ urlpatterns = [
|
|||||||
views.DashboardConfigView.as_view(),
|
views.DashboardConfigView.as_view(),
|
||||||
name='config'
|
name='config'
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
'settings/<int:pk>/update/',
|
||||||
|
views.SiteSettingsUpdateView.as_view(),
|
||||||
|
name='settings-update'
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
'catalog/',
|
'catalog/',
|
||||||
views.CatalogView.as_view(),
|
views.CatalogView.as_view(),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django import forms
|
||||||
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
@ -37,7 +38,8 @@ from core.models import (
|
|||||||
ShippingRate,
|
ShippingRate,
|
||||||
Transaction,
|
Transaction,
|
||||||
TrackingNumber,
|
TrackingNumber,
|
||||||
Coupon
|
Coupon,
|
||||||
|
SiteSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
from core import (
|
from core import (
|
||||||
@ -46,6 +48,7 @@ from core import (
|
|||||||
OrderStatus
|
OrderStatus
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
|
ProductVariantUpdateForm,
|
||||||
OrderLineFulfillForm,
|
OrderLineFulfillForm,
|
||||||
OrderLineFormset,
|
OrderLineFormset,
|
||||||
OrderCancelForm,
|
OrderCancelForm,
|
||||||
@ -88,6 +91,15 @@ class DashboardConfigView(TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class SiteSettingsUpdateView(UpdateView):
|
||||||
|
model = SiteSettings
|
||||||
|
context_object_name = 'settings'
|
||||||
|
template_name = 'dashboard/settings_form.html'
|
||||||
|
fields = '__all__'
|
||||||
|
success_url = reverse_lazy('dashboard:config')
|
||||||
|
success_message = 'Settings saved.'
|
||||||
|
|
||||||
|
|
||||||
class CatalogView(ListView):
|
class CatalogView(ListView):
|
||||||
model = ProductCategory
|
model = ProductCategory
|
||||||
context_object_name = 'category_list'
|
context_object_name = 'category_list'
|
||||||
@ -346,13 +358,6 @@ class ProductDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
|
||||||
model = Product
|
|
||||||
template_name = 'dashboard/product_update_form.html'
|
|
||||||
fields = '__all__'
|
|
||||||
success_message = '%(name)s saved.'
|
|
||||||
|
|
||||||
|
|
||||||
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'dashboard/product_create_form.html'
|
template_name = 'dashboard/product_create_form.html'
|
||||||
@ -360,10 +365,17 @@ class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
|||||||
success_message = '%(name)s created.'
|
success_message = '%(name)s created.'
|
||||||
|
|
||||||
|
|
||||||
|
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'dashboard/product_update_form.html'
|
||||||
|
fields = '__all__'
|
||||||
|
success_message = '%(name)s saved.'
|
||||||
|
|
||||||
|
|
||||||
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'dashboard/product_confirm_delete.html'
|
template_name = 'dashboard/product_confirm_delete.html'
|
||||||
success_url = reverse_lazy('dashboard:product-list')
|
success_url = reverse_lazy('dashboard:catalog')
|
||||||
success_message = 'Product deleted.'
|
success_message = 'Product deleted.'
|
||||||
|
|
||||||
|
|
||||||
@ -428,14 +440,7 @@ class ProductVariantUpdateView(SuccessMessageMixin, UpdateView):
|
|||||||
pk_url_kwarg = 'variant_pk'
|
pk_url_kwarg = 'variant_pk'
|
||||||
success_message = 'ProductVariant saved.'
|
success_message = 'ProductVariant saved.'
|
||||||
template_name = 'dashboard/variant_form.html'
|
template_name = 'dashboard/variant_form.html'
|
||||||
fields = [
|
form_class = ProductVariantUpdateForm
|
||||||
'name',
|
|
||||||
'sku',
|
|
||||||
'price',
|
|
||||||
'weight',
|
|
||||||
'track_inventory',
|
|
||||||
'stock',
|
|
||||||
]
|
|
||||||
context_object_name = 'variant'
|
context_object_name = 'variant'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
|||||||
|
|
||||||
|
|
||||||
class AddressTests(StaticLiveServerTestCase):
|
class AddressTests(StaticLiveServerTestCase):
|
||||||
fixtures = ['products.json']
|
fixtures = ['shipping_rates.json', 'products.json']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|||||||
@ -19,7 +19,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class CouponTests(StaticLiveServerTestCase):
|
class CouponTests(StaticLiveServerTestCase):
|
||||||
fixtures = ['products.json', 'accounts.json', 'coupons.json']
|
fixtures = [
|
||||||
|
'shipping_rates.json', 'products.json', 'accounts.json', 'coupons.json'
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@ -87,7 +89,8 @@ class CouponTests(StaticLiveServerTestCase):
|
|||||||
state_select.select_by_value('UT')
|
state_select.select_by_value('UT')
|
||||||
postal_code_input = self.browser.find_element(By.NAME, 'postal_code')
|
postal_code_input = self.browser.find_element(By.NAME, 'postal_code')
|
||||||
postal_code_input.send_keys('84321')
|
postal_code_input.send_keys('84321')
|
||||||
self.browser.find_element(By.XPATH,
|
self.browser.find_element(
|
||||||
|
By.XPATH,
|
||||||
'//input[@value="Continue"]'
|
'//input[@value="Continue"]'
|
||||||
).click()
|
).click()
|
||||||
|
|
||||||
@ -106,7 +109,8 @@ class CouponTests(StaticLiveServerTestCase):
|
|||||||
def test_apply_used_coupon_to_order_returns_message(self):
|
def test_apply_used_coupon_to_order_returns_message(self):
|
||||||
# Add item to cart
|
# Add item to cart
|
||||||
self.browser.get(self.live_server_url + '/products/1/')
|
self.browser.get(self.live_server_url + '/products/1/')
|
||||||
self.browser.find_element(By.XPATH,
|
self.browser.find_element(
|
||||||
|
By.XPATH,
|
||||||
'//input[@value="Add to cart"]'
|
'//input[@value="Add to cart"]'
|
||||||
).click()
|
).click()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -118,7 +122,8 @@ class CouponTests(StaticLiveServerTestCase):
|
|||||||
coupon_input = self.browser.find_element(By.ID, 'id_code')
|
coupon_input = self.browser.find_element(By.ID, 'id_code')
|
||||||
coupon_input.send_keys('MAY2022')
|
coupon_input.send_keys('MAY2022')
|
||||||
self.browser.find_element(By.XPATH, '//input[@value="Apply"]').click()
|
self.browser.find_element(By.XPATH, '//input[@value="Apply"]').click()
|
||||||
self.browser.find_element(By.XPATH,
|
self.browser.find_element(
|
||||||
|
By.XPATH,
|
||||||
'//a[contains(text(), "Proceed to Checkout")]'
|
'//a[contains(text(), "Proceed to Checkout")]'
|
||||||
).click()
|
).click()
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,21 @@ load_dotenv()
|
|||||||
|
|
||||||
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
||||||
|
|
||||||
DATABASE_CONFIG = {
|
if DEBUG:
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
DATABASE_CONFIG = {
|
||||||
'OPTIONS': {
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'service': 'pg_service',
|
'USER': 'django',
|
||||||
'passfile': '.pgpass'
|
'NAME': 'ptcoffee_dev'
|
||||||
}
|
}
|
||||||
}
|
else:
|
||||||
|
DATABASE_CONFIG = {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'OPTIONS': {
|
||||||
|
'service': 'pg_service',
|
||||||
|
'passfile': '.pgpass'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY', '')
|
SECRET_KEY = os.environ.get('SECRET_KEY', '')
|
||||||
CACHE_CONFIG = {
|
CACHE_CONFIG = {
|
||||||
'LOCATION': 'redis://127.0.0.1:6379',
|
'LOCATION': 'redis://127.0.0.1:6379',
|
||||||
|
|||||||
@ -9,9 +9,13 @@ const productItemImages = document.querySelectorAll('.product__with-img-swap')
|
|||||||
|
|
||||||
productItemImages.forEach(productItemImage => {
|
productItemImages.forEach(productItemImage => {
|
||||||
productItemImage.addEventListener('mouseover', event => {
|
productItemImage.addEventListener('mouseover', event => {
|
||||||
[event.target.src, event.target.dataset.altimgSrc] = valueSwap(event.target.src, event.target.dataset.altimgSrc)
|
if (event.target.dataset.altimgSrc != '') {
|
||||||
|
[event.target.src, event.target.dataset.altimgSrc] = valueSwap(event.target.src, event.target.dataset.altimgSrc)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
productItemImage.addEventListener('mouseout', event => {
|
productItemImage.addEventListener('mouseout', event => {
|
||||||
[event.target.src, event.target.dataset.altimgSrc] = valueSwap(event.target.src, event.target.dataset.altimgSrc)
|
if (event.target.dataset.altimgSrc != '') {
|
||||||
|
[event.target.src, event.target.dataset.altimgSrc] = valueSwap(event.target.src, event.target.dataset.altimgSrc)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -441,13 +441,14 @@ section:not(:last-child) {
|
|||||||
/* Site Banner
|
/* Site Banner
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
.site__banner {
|
.site__banner {
|
||||||
background-color: rgba(0, 0, 0, 0.44);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
background-blend-mode: multiply;
|
background-blend-mode: multiply;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
color: #f1e8e2;
|
color: #f1e8e2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
|
text-shadow: 1px 1px 2px black;
|
||||||
font-family: 'Vollkorn', serif;
|
font-family: 'Vollkorn', serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,6 +793,10 @@ article + article {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item__variant {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
.item__form,
|
.item__form,
|
||||||
.coupon__form p {
|
.coupon__form p {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -6,10 +6,12 @@ from django.conf import settings
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect, reverse
|
from django.shortcuts import redirect, reverse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models import OuterRef, Q, Subquery
|
from django.db.models import OuterRef, Q, Subquery
|
||||||
|
|
||||||
from core.models import (
|
from core.models import (
|
||||||
Product, ProductVariant, OrderLine, Coupon, ShippingRate
|
Product, ProductVariant, OrderLine, Coupon, ShippingRate,
|
||||||
|
SiteSettings
|
||||||
)
|
)
|
||||||
from core.usps import USPSApi
|
from core.usps import USPSApi
|
||||||
from core import (
|
from core import (
|
||||||
@ -58,6 +60,7 @@ class CartItem:
|
|||||||
|
|
||||||
class Cart:
|
class Cart:
|
||||||
item_class = CartItem
|
item_class = CartItem
|
||||||
|
# site_settings = SiteSettings.load()
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
@ -101,8 +104,15 @@ class Cart:
|
|||||||
for item in self:
|
for item in self:
|
||||||
if item['variant'].track_inventory:
|
if item['variant'].track_inventory:
|
||||||
if item['quantity'] > item['variant'].stock:
|
if item['quantity'] > item['variant'].stock:
|
||||||
messages.warning(request, 'Quantity added exceeds available stock.')
|
if item['quantity'] > item['variant'].product.checkout_limit:
|
||||||
|
messages.warning(request, 'Quantity exceeds checkout limit.')
|
||||||
|
item['quantity'] = item['variant'].product.checkout_limit
|
||||||
|
continue
|
||||||
|
messages.warning(request, 'Quantity exceeds available stock.')
|
||||||
item['quantity'] = item['variant'].stock
|
item['quantity'] = item['variant'].stock
|
||||||
|
elif item['quantity'] > item['variant'].product.checkout_limit:
|
||||||
|
messages.warning(request, 'Quantity exceeds checkout limit.')
|
||||||
|
item['quantity'] = item['variant'].product.checkout_limit
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def remove(self, pk):
|
def remove(self, pk):
|
||||||
@ -160,6 +170,10 @@ class Cart:
|
|||||||
return containers
|
return containers
|
||||||
|
|
||||||
def get_shipping_cost(self, container=None):
|
def get_shipping_cost(self, container=None):
|
||||||
|
# free_shipping_min = self.site_settings.free_shipping_min
|
||||||
|
# if self.get_total_price() >= free_shipping_min:
|
||||||
|
# return Decimal('0.00')
|
||||||
|
|
||||||
if container is None:
|
if container is None:
|
||||||
container = self.session.get('shipping_container').container
|
container = self.session.get('shipping_container').container
|
||||||
|
|
||||||
|
|||||||
@ -13,11 +13,15 @@
|
|||||||
<div class="cart__item">
|
<div class="cart__item">
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<figure class="item__figure">
|
<figure class="item__figure">
|
||||||
|
{% if item.variant.image %}
|
||||||
|
<img class="item__image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
</figure>
|
</figure>
|
||||||
<div class="item__info">
|
<div class="item__info">
|
||||||
<h3>{{product.name}}</h3>
|
<h3>{{product.name}}</h3>
|
||||||
<h4>{{ item.variant.name }}</h4>
|
<h2 class="item__variant">{{ item.variant.name }}</h2>
|
||||||
{% for key, value in item.options.items %}
|
{% for key, value in item.options.items %}
|
||||||
<p><strong>{{ key }}</strong>: {{ value }}</p>
|
<p><strong>{{ key }}</strong>: {{ value }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -54,6 +58,7 @@
|
|||||||
<input type="submit" value="Apply" class="action-button">
|
<input type="submit" value="Apply" class="action-button">
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
<!-- <h5>Free shipping on orders over ${{ site_settings.free_shipping_min|floatformat:"2" }}</h5> -->
|
||||||
<div class="cart__table-wrapper">
|
<div class="cart__table-wrapper">
|
||||||
<table class="cart__totals">
|
<table class="cart__totals">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -23,14 +23,18 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<td>
|
<td>
|
||||||
|
{% if item.variant.image %}
|
||||||
|
<img class="line__image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
<img class="line__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
<img class="line__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ item.variant }}</strong><br>
|
<strong>{{ item.variant }}</strong><br>
|
||||||
{{item.customer_note}}
|
{{item.customer_note}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{item.quantity}}</td>
|
<td>{{item.quantity}}</td>
|
||||||
<td>${{product.price}}</td>
|
<td>${{item.unit_price}}</td>
|
||||||
<td>${{item.get_total}}</td>
|
<td>${{item.get_total}}</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -34,7 +34,11 @@
|
|||||||
<div class="cart__item">
|
<div class="cart__item">
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<figure>
|
<figure>
|
||||||
|
{% if item.variant.image %}
|
||||||
|
<img src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
<img src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
<img src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
</figure>
|
</figure>
|
||||||
<div>
|
<div>
|
||||||
<h3>{{product.name}}</h3>
|
<h3>{{product.name}}</h3>
|
||||||
@ -75,7 +79,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Shipping</td>
|
<td>Shipping</td>
|
||||||
<td>${{ cart.get_shipping_cost }}</small></td>
|
<td>${{ cart.get_shipping_cost }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
|
|||||||
@ -15,8 +15,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="site__banner site__banner--site">
|
<div class="site__banner site__banner--site">
|
||||||
<h1>Welcome to our new website!</h1>
|
<h1>Now three different size bags to choose from!</h1>
|
||||||
<h4>NEW COOL LOOK, SAME GREAT COFFEE</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<section class="product__list">
|
<section class="product__list">
|
||||||
|
|||||||
@ -178,6 +178,11 @@ class ProductCategoryDetailView(DetailView):
|
|||||||
Q(visible_in_listings=True),
|
Q(visible_in_listings=True),
|
||||||
Q(variants__track_inventory=False) |
|
Q(variants__track_inventory=False) |
|
||||||
Q(variants__track_inventory=True) & Q(variants__stock__gt=0)
|
Q(variants__track_inventory=True) & Q(variants__stock__gt=0)
|
||||||
|
).prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'variants',
|
||||||
|
queryset=ProductVariant.objects.all().order_by('sorting', 'weight')
|
||||||
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -192,6 +197,11 @@ class ProductListView(ListView):
|
|||||||
queryset = Product.objects.filter(
|
queryset = Product.objects.filter(
|
||||||
visible_in_listings=True,
|
visible_in_listings=True,
|
||||||
category__main_category=True
|
category__main_category=True
|
||||||
|
).prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'variants',
|
||||||
|
queryset=ProductVariant.objects.all().order_by('sorting', 'weight')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -206,7 +216,7 @@ class ProductDetailView(FormMixin, DetailView):
|
|||||||
track_inventory=True,
|
track_inventory=True,
|
||||||
stock__gt=0
|
stock__gt=0
|
||||||
)
|
)
|
||||||
)
|
).order_by('name')
|
||||||
options = ProductOption.objects.filter(products__pk=self.object.pk)
|
options = ProductOption.objects.filter(products__pk=self.object.pk)
|
||||||
if form_class is None:
|
if form_class is None:
|
||||||
form_class = self.get_form_class()
|
form_class = self.get_form_class()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user