Merge branch 'release/2.0.0'

This commit is contained in:
Nathan Chapman 2022-10-29 22:59:24 -06:00
commit f0e240ee1a
88 changed files with 2908 additions and 1050 deletions

View File

@ -26,6 +26,7 @@ sentry-sdk = "*"
django-localflavor = "*" django-localflavor = "*"
django-analytical = "*" django-analytical = "*"
django-simple-captcha = "*" django-simple-captcha = "*"
stripe = "*"
[dev-packages] [dev-packages]
django-debug-toolbar = "*" django-debug-toolbar = "*"

674
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "e73567e23f2b566826ba328a2b92091773c433c5c654bbc6dddcd04157ecafb0" "sha256": "747e3ff37ed559ecc83348a6bc08ecf79c54eae60dc405f6ee2977b7e91026be"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -60,74 +60,88 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2022.5.18.1" "version": "==2022.6.15"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
"sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
"sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
"sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
"sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
"sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
"sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
"sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
"sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
"sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
"sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
"sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
"sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
"sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
"sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
"sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
"sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
"sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
"sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
"sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
"sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
"sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
"sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
"sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
"sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
"sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
"sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
"sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
"sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
"sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
"sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
"sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
"sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
"sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
"sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
"sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
"sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
"sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
"sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
"sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
"sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
"sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
"sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
"sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
"sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
"sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
"sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
"sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
"sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
"sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
"sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
"sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
"sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
"sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
"sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
"sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
"sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
"sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
"sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
"sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
"sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
"sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
"sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
"sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
], ],
"version": "==1.15.0" "version": "==1.15.1"
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.6'",
"version": "==2.0.12" "version": "==2.1.0"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -142,7 +156,7 @@
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
], ],
"markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", "markers": "python_version < '4.0' and python_full_version >= '3.6.2'",
"version": "==0.3.0" "version": "==0.3.0"
}, },
"click-plugins": { "click-plugins": {
@ -161,30 +175,30 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804", "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59",
"sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178", "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596",
"sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717", "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3",
"sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982", "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5",
"sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004", "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab",
"sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe", "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884",
"sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452", "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82",
"sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336", "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b",
"sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4", "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441",
"sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15", "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa",
"sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d", "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d",
"sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c", "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b",
"sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0", "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a",
"sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06", "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6",
"sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9", "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157",
"sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1", "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280",
"sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023", "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282",
"sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de", "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67",
"sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f", "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8",
"sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181", "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e", "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a" "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"
], ],
"version": "==37.0.2" "version": "==37.0.4"
}, },
"defusedxml": { "defusedxml": {
"hashes": [ "hashes": [
@ -254,11 +268,11 @@
}, },
"django-celery-results": { "django-celery-results": {
"hashes": [ "hashes": [
"sha256:b8c9416619dbcc38f13398e31bcb1f14a228cd1e8f65fb22d3b7fc68aaa5331a", "sha256:75aa51970db5691cbf242c6a0ff50c8cdf419e265cd0e9b772335d06436c4b99",
"sha256:bf24ecc29c42e49cc7eb30b9b3739471331e2a0ca517cc88ca53a0cf3a2031d1" "sha256:be91307c02fbbf0dda21993c3001c60edb74595444ccd6ad696552fe3689e85b"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.1" "version": "==2.4.0"
}, },
"django-compressor": { "django-compressor": {
"hashes": [ "hashes": [
@ -270,11 +284,11 @@
}, },
"django-filter": { "django-filter": {
"hashes": [ "hashes": [
"sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e", "sha256:ed429e34760127e3520a67f415bec4c905d4649fbe45d0d6da37e6ff5e0287eb",
"sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063" "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5"
], ],
"index": "pypi", "index": "pypi",
"version": "==21.1" "version": "==22.1"
}, },
"django-localflavor": { "django-localflavor": {
"hashes": [ "hashes": [
@ -343,7 +357,7 @@
"sha256:15746ed367a5a32eda76cfa2886eeec1de8cda79f519b7c5e12f87ed7cdbd663", "sha256:15746ed367a5a32eda76cfa2886eeec1de8cda79f519b7c5e12f87ed7cdbd663",
"sha256:199f211082eeac7e83563929b8ce41399c1c0f00dfc2f36bc00bea381027eaaa" "sha256:199f211082eeac7e83563929b8ce41399c1c0f00dfc2f36bc00bea381027eaaa"
], ],
"markers": "python_version >= '3.7' and python_full_version < '4.0.0'", "markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==5.0" "version": "==5.0"
}, },
"gunicorn": { "gunicorn": {
@ -372,72 +386,80 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:00f3a6f88fd5f4357844dd91a1abac5f466c6799f1b7f1da2df6665253845b11", "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318",
"sha256:024684e0c5cfa121c22140d3a0898a3a9b2ea0f0fd2c229b6658af4bdf1155e5", "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c",
"sha256:03370ec37fe562238d385e2c53089076dee53aabf8325cab964fdb04a9130fa0", "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b",
"sha256:0aa4cce579512c33373ca4c5e23c21e40c1aa1a33533a75e51b654834fd0e4f2", "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000",
"sha256:1057356b808d149bc14eb8f37bb89129f237df488661c1e0fc0376ca90e1d2c3", "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73",
"sha256:11d62c97ceff9bab94b6b29c010ea5fb6831743459bb759c917f49ba75601cd0", "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d",
"sha256:1254a79f8a67a3908de725caf59eae62d86738f6387b0a34b32e02abd6ae73db", "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb",
"sha256:1bfb791a8fcdbf55d1d41b8be940393687bec0e9b12733f0796668086d1a23ff", "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8",
"sha256:28cf04a1a38e961d4a764d2940af9b941b66263ed5584392ef875ee9c1e360a3", "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2",
"sha256:2b9c2341d96926b0d0e132e5c49ef85eb53fa92ae1c3a70f9072f3db0d32bc07", "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345",
"sha256:2d10659e6e5c53298e6d718fd126e793285bff904bb71d7239a17218f6a197b7", "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94",
"sha256:3af00ee88376022589ceeb8170eb67dacf5f7cd625ea59fa0977d719777d4ae8", "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e",
"sha256:3cf816aed8125cfc9e6e5c6c31ff94278320d591bd7970c4a0233bee0d1c8790", "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b",
"sha256:4becd16750ca5c2a1b1588269322b2cebd10c07738f336c922b658dbab96a61c", "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc",
"sha256:4cd69bca464e892ea4ed544ba6a7850aaff6f8d792f8055a10638db60acbac18", "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a",
"sha256:4e97c8fc761ad63909198acc892f34c20f37f3baa2c50a62d5ec5d7f1efc68a1", "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9",
"sha256:520461c36727268a989790aef08884347cd41f2d8ae855489ccf40b50321d8d7", "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc",
"sha256:53b0410b220766321759f7f9066da67b1d0d4a7f6636a477984cbb1d98483955", "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387",
"sha256:56e19fb6e4b8bd07fb20028d03d3bc67bcc0621347fbde64f248e44839771756", "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb",
"sha256:5a49ad78543925e1a4196e20c9c54492afa4f1502c2a563f73097e2044c75190", "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7",
"sha256:5d52e1173f52020392f593f87a6af2d4055dd800574a5cb0af4ea3878801d307", "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4",
"sha256:607224ffae9a0cf0a2f6e14f5f6bce43e83a6fbdaa647891729c103bdd6a5593", "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97",
"sha256:612ef8f2795a89ba3a1d4c8c1af84d8453fd53ee611aa5ad460fdd2cab426fc2", "sha256:49a866923e69bc7da45a0565636243707c22752fc38f6b9d5c8428a86121022c",
"sha256:615886ee84b6f42f1bdf1852a9669b5fe3b96b6ff27f1a7a330b67ad9911200a", "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67",
"sha256:63419db39df8dc5564f6f103102c4665f7e4d9cb64030e98cf7a74eae5d5760d", "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627",
"sha256:6467626fa74f96f4d80fc6ec2555799e97fff8f36e0bfc7f67769f83e59cff40", "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7",
"sha256:65b3b5f12c6fb5611e79157214f3cd533083f9b058bf2fc8a1c5cc5ee40fdc5a", "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd",
"sha256:686565ac77ff94a8965c11829af253d9e2ce3bf0d9225b1d2eb5c4d4666d0dca", "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3",
"sha256:6af7f51a6010748fc1bb71917318d953c9673e4ae3f6d285aaf93ef5b2eb11c1", "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7",
"sha256:70a198030d26f5e569367f0f04509b63256faa76a22886280eea69a4f535dd40", "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130",
"sha256:754a1dd04bff8a509a31146bd8f3a5dc8191a8694d582dd5fb71ff09f0722c22", "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b",
"sha256:75da29a0752c8f2395df0115ac1681cefbdd4418676015be8178b733704cbff2", "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036",
"sha256:81c29c8741fa07ecec8ec7417c3d8d1e2f18cf5a10a280f4e1c3f8c3590228b2", "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785",
"sha256:9093a359a86650a3dbd6532c3e4d21a6f58ba2cb60d0e72db0848115d24c10ba", "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca",
"sha256:915ecf7d486df17cc65aeefdb680d5ad4390cc8c857cf8db3fe241ed234f856a", "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91",
"sha256:94b181dd2777890139e49a5336bf3a9a3378ce66132c665fe8db4e8b7683cde2", "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc",
"sha256:94f2e45b054dd759bed137b6e14ae8625495f7d90ddd23cf62c7a68f72b62656", "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536",
"sha256:9af19eb789d674b59a9bee5005779757aab857c40bf9cc313cb01eafac55ce55", "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391",
"sha256:9cae837b988f44925d14d048fa6a8c54f197c8b1223fd9ee9c27084f84606143", "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3",
"sha256:aa7447bf7c1a15ef24e2b86a277b585dd3f055e8890ac7f97374d170187daa97", "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d",
"sha256:b1e22f3ee4d75ca261b6bffbf64f6f178cb194b1be3191065a09f8d98828daa9", "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21",
"sha256:b5031d151d6147eac53366d6ec87da84cd4d8c5e80b1d9948a667a7164116e39", "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3",
"sha256:b62d1431b4c40cda43cc986f19b8c86b1d2ae8918cfc00f4776fdf070b65c0c4", "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d",
"sha256:b71c52d69b91af7d18c13aef1b0cc3baee36b78607c711eb14a52bf3aa7c815e", "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29",
"sha256:b7679344f2270840dc5babc9ccbedbc04f7473c1f66d4676bb01680c0db85bcc", "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715",
"sha256:bb7c1b029e54e26e01b1d1d912fc21abb65650d16ea9a191d026def4ed0859ed", "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed",
"sha256:c2a57755e366e0ac7ebdb3e9207f159c3bf1afed02392ab18453ce81f5ee92ee", "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25",
"sha256:cf9ec915857d260511399ab87e1e70fa13d6b2972258f8e620a3959468edfc32", "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c",
"sha256:d0d03b9636f1326772e6854459728676354d4c7731dae9902b180e2065ba3da6", "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785",
"sha256:d1690c4d37674a5f0cdafbc5ed7e360800afcf06928c2a024c779c046891bf09", "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837",
"sha256:d76da27f5e3e9bc40eba6ed7a9e985f57547e98cf20521d91215707f2fb57e0f", "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4",
"sha256:d882c2f3345261e898b9f604be76b61c901fbfa4ac32e3f51d5dc1edc89da3cb", "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b",
"sha256:d8e5021e770b0a3084c30dda5901d5fce6d4474feaf0ced8f8e5a82702502fbb", "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2",
"sha256:dd00d28d1ab5fa7627f5abc957f29a6338a7395b724571a8cbff8fbed83aaa82", "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067",
"sha256:e35a298691b9e10e5a5631f8f0ba605b30ebe19208dc8f58b670462f53753641", "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448",
"sha256:e4d020ecf3740b7312bacab2cb966bb720fd4d3490562d373b4ad91dd1857c0d", "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d",
"sha256:e564d5a771b4015f34166a05ea2165b7e283635c41b1347696117f780084b46d", "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2",
"sha256:ea3f2e9eb41f973f73619e88bf7bd950b16b4c2ce73d15f24a11800ce1eaf276", "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc",
"sha256:eabdbe04ee0a7e760fa6cd9e799d2b020d098c580ba99107d52e1e5e538b1ecb", "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c",
"sha256:f17b9df97c5ecdfb56c5e85b3c9df9831246df698f8581c6e111ac664c7c656e", "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5",
"sha256:f386def57742aacc3d864169dfce644a8c396f95aa35b41b69df53f558d56dd0", "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84",
"sha256:f6d23a01921b741774f35e924d418a43cf03eca1444f3fdfd7978d35a5aaab8b", "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8",
"sha256:fcdf70191f0d1761d190a436db06a46f05af60e1410e1507935f0332280c9268" "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf",
"sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7",
"sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e",
"sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb",
"sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b",
"sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3",
"sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad",
"sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8",
"sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.0" "version": "==4.9.1"
}, },
"measurement": { "measurement": {
"hashes": [ "hashes": [
@ -486,55 +508,75 @@
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f", "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927",
"sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d", "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14",
"sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b", "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc",
"sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c", "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58",
"sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9", "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60",
"sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546", "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76",
"sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578", "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c",
"sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1", "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac",
"sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe", "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490",
"sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098", "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1",
"sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2", "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f",
"sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a", "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d",
"sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45", "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f",
"sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530", "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069",
"sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108", "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402",
"sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1", "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885",
"sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd", "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e",
"sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0", "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be",
"sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6", "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8",
"sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c", "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff",
"sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf", "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da",
"sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4", "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004",
"sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d", "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f",
"sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765", "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20",
"sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602", "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d",
"sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340", "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c",
"sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c", "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544",
"sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b", "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9",
"sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84", "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3",
"sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8", "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04",
"sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92", "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c",
"sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54", "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5",
"sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601", "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4",
"sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a", "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb",
"sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf", "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4",
"sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251", "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c",
"sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a", "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467",
"sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e" "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e",
"sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421",
"sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b",
"sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8",
"sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb",
"sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3",
"sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf",
"sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1",
"sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a",
"sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28",
"sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0",
"sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1",
"sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8",
"sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd",
"sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4",
"sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8",
"sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f",
"sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013",
"sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59",
"sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc",
"sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"
], ],
"index": "pypi", "index": "pypi",
"version": "==9.1.1" "version": "==9.2.0"
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752", "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0",
"sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7" "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"
], ],
"markers": "python_full_version >= '3.6.2'", "markers": "python_full_version >= '3.6.2'",
"version": "==3.0.29" "version": "==3.0.30"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@ -741,19 +783,19 @@
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:2f7a57cf4af15cd543c4394bcbe2b9148db2606a37edba755368836e3a1d053e", "sha256:a52d5694c9eb4292770084fa8c863f79367ca19884b329ab574d5cb2036b3e54",
"sha256:f57f8df5d238a8ecf92f499b6b21467bfee6c13d89953c27edf1e2bc673622e7" "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.3.3" "version": "==4.3.4"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
"sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
], ],
"markers": "python_version >= '3.7' and python_full_version < '4.0.0'", "markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==2.28.0" "version": "==2.28.1"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [
@ -790,19 +832,19 @@
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863", "sha256:6f460da98b730d671510af18f119f96d01e3ba027ac0e985871abb3aede1c514",
"sha256:778b53f0a6c83b1ee43d3b7886318ba86d975e686cb2c7906ccc35b334360be1" "sha256:95fd321f583dfcfaf279a0b2cdc83d8d28c8b7cca4d2959fc4539bb4fecb56a0"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.5.12" "version": "==1.7.2"
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:d1746e7fd520e83bbe210d02fff1aa1a425ad671c7a9da7d246ec2401a087198", "sha256:0d33c374d41c7863419fc8f6c10bfe25b7b498aa34164d135c622e52580c6b16",
"sha256:e7d11f3db616cda0751372244c2ba798e8e56a28e096ec4529010b803485f3fe" "sha256:c04b44a57a6265fe34a4a444e965884716d34bae963119a76353434d6f18e450"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==62.3.3" "version": "==63.2.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -820,6 +862,14 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==0.4.2" "version": "==0.4.2"
}, },
"stripe": {
"hashes": [
"sha256:08f74cae6619d4a7d78f8162ff72bc3e9c913f53ec96ecd5ddc7d823c2e79ddd",
"sha256:69d5bf4611624a503bcec84a61b1f2a2b874bfc828432e4fd75cd120bcc3efef"
],
"index": "pypi",
"version": "==3.5.0"
},
"sympy": { "sympy": {
"hashes": [ "hashes": [
"sha256:5939eeffdf9e152172601463626c022a2c27e75cf6278de8d401d50c9d58787b", "sha256:5939eeffdf9e152172601463626c022a2c27e75cf6278de8d401d50c9d58787b",
@ -838,11 +888,11 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4.0'",
"version": "==1.26.9" "version": "==1.26.10"
}, },
"usps-api": { "usps-api": {
"hashes": [ "hashes": [
@ -972,93 +1022,107 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2022.5.18.1" "version": "==2022.6.15"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
"sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
"sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
"sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
"sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
"sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
"sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
"sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
"sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
"sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
"sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
"sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
"sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
"sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
"sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
"sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
"sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
"sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
"sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
"sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
"sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
"sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
"sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
"sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
"sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
"sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
"sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
"sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
"sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
"sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
"sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
"sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
"sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
"sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
"sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
"sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
"sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
"sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
"sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
"sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
"sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
"sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
"sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
"sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
"sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
"sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
"sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
"sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
"sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
"sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
"sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
"sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
"sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
"sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
"sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
"sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
"sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
"sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
"sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
"sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
"sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
"sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
"sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
"sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
], ],
"version": "==1.15.0" "version": "==1.15.1"
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804", "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59",
"sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178", "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596",
"sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717", "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3",
"sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982", "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5",
"sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004", "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab",
"sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe", "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884",
"sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452", "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82",
"sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336", "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b",
"sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4", "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441",
"sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15", "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa",
"sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d", "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d",
"sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c", "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b",
"sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0", "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a",
"sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06", "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6",
"sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9", "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157",
"sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1", "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280",
"sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023", "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282",
"sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de", "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67",
"sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f", "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8",
"sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181", "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046",
"sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e", "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327",
"sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a" "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"
], ],
"version": "==37.0.2" "version": "==37.0.4"
}, },
"django": { "django": {
"hashes": [ "hashes": [
@ -1070,11 +1134,11 @@
}, },
"django-debug-toolbar": { "django-debug-toolbar": {
"hashes": [ "hashes": [
"sha256:42c1c2e9dc05bb57b53d641e3a6d131fc031b92377b34ae32e604a1fe439bb83", "sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661",
"sha256:ae6bec2c1ce0e6900b0ab0443e1427eb233d8e6f57a84a0b2705eeecb8874e22" "sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.4.0" "version": "==3.5.0"
}, },
"h11": { "h11": {
"hashes": [ "hashes": [
@ -1094,11 +1158,11 @@
}, },
"outcome": { "outcome": {
"hashes": [ "hashes": [
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.7'",
"version": "==1.1.0" "version": "==1.2.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -1133,10 +1197,10 @@
}, },
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:ba5b2633f43cf6fe9d308fa4a6996e00a101ab9cb1aad6fd91ae1f3dbe57f56f" "sha256:f67402b8f973aaa98d9c55b8f9aa63532009cd1859b2222a8b9800354942d8bc"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.2.0" "version": "==4.3.0"
}, },
"sniffio": { "sniffio": {
"hashes": [ "hashes": [
@ -1166,7 +1230,7 @@
"sha256:4dc0bf9d5cc78767fc4516325b6d80cc0968705a31d0eec2ecd7cdda466265b0", "sha256:4dc0bf9d5cc78767fc4516325b6d80cc0968705a31d0eec2ecd7cdda466265b0",
"sha256:523f39b7b69eef73501cebfe1aafd400a9aad5b03543a0eded52952488ff1c13" "sha256:523f39b7b69eef73501cebfe1aafd400a9aad5b03543a0eded52952488ff1c13"
], ],
"markers": "python_full_version >= '3.7.0'", "markers": "python_version >= '3.7'",
"version": "==0.21.0" "version": "==0.21.0"
}, },
"trio-websocket": { "trio-websocket": {
@ -1179,18 +1243,18 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4.0'",
"version": "==1.26.9" "version": "==1.26.10"
}, },
"wsproto": { "wsproto": {
"hashes": [ "hashes": [
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b", "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8" "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
], ],
"markers": "python_full_version >= '3.7.0'", "markers": "python_version >= '3.7'",
"version": "==1.1.0" "version": "==1.1.0"
} }
} }

View File

@ -4,3 +4,8 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig): class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts' name = 'accounts'
# def ready(self):
# from .signals import (
# user_saved
# )

View File

@ -1,4 +1,4 @@
# Generated by Django 4.0.2 on 2022-07-09 14:14 # Generated by Django 4.0.2 on 2022-07-23 17:28
from django.db import migrations, models from django.db import migrations, models

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2022-07-30 23:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_alter_address_state'),
]
operations = [
migrations.AddField(
model_name='user',
name='stripe_id',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@ -18,7 +18,12 @@ class Address(models.Model):
postal_code = models.CharField(max_length=20, blank=True) postal_code = models.CharField(max_length=20, blank=True)
def __str__(self): def __str__(self):
return f'{self.street_address_1}{self.city}' return f"""
{self.first_name} {self.last_name}
{self.street_address_1}
{self.street_address_2}
{self.city}, {self.state}, {self.postal_code}
"""
class User(AbstractUser): class User(AbstractUser):
@ -31,3 +36,4 @@ class User(AbstractUser):
default_billing_address = models.ForeignKey( default_billing_address = models.ForeignKey(
Address, related_name="+", null=True, blank=True, on_delete=models.SET_NULL Address, related_name="+", null=True, blank=True, on_delete=models.SET_NULL
) )
stripe_id = models.CharField(max_length=255, blank=True)

22
src/accounts/signals.py Normal file
View File

@ -0,0 +1,22 @@
import logging
import stripe
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import models
from django.conf import settings
from .models import Address, User
logger = logging.getLogger(__name__)
@receiver(post_save, sender=User, dispatch_uid='user_saved')
def user_saved(sender, instance, created, **kwargs):
logger.info('User was saved')
if created or not instance.stripe_id:
stripe.api_key = settings.STRIPE_API_KEY
response = stripe.Customer.create(
name=instance.first_name + instance.last_name
)
instance.stripe_id = response['id']
instance.save()

View File

@ -23,7 +23,7 @@ def get_or_create_customer(request, form, shipping_address):
user.save() user.save()
else: else:
user, u_created = User.objects.get_or_create( user, u_created = User.objects.get_or_create(
email=form.cleaned_data['email'], email=form.cleaned_data['email'].lower(),
defaults={ defaults={
'username': form.cleaned_data['email'].lower(), 'username': form.cleaned_data['email'].lower(),
'is_staff': False, 'is_staff': False,

View File

@ -66,13 +66,15 @@ class TransactionStatus:
] ]
class ShippingMethodType: class ShippingProvider:
PRICE_BASED = 'price' USPS = 'USPS'
WEIGHT_BASED = 'weight' # UPS = 'UPS'
# FEDEX = 'FEDEX'
CHOICES = [ CHOICES = [
(PRICE_BASED, 'Price based shipping'), (USPS, 'USPS'),
(WEIGHT_BASED, 'Weight based shipping'), # (UPS, 'UPS'),
# (FEDEX, 'FedEx'),
] ]
@ -125,3 +127,20 @@ class CoffeeGrind:
(PERCOLATOR, 'Percolator'), (PERCOLATOR, 'Percolator'),
(CAFE_STYLE, 'BLTC cafe pour over') (CAFE_STYLE, 'BLTC cafe pour over')
] ]
def build_usps_rate_request(weight, container, zip_destination):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': zip_destination,
'pounds': '0',
'ounces': weight,
'container': container,
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}

View File

@ -1,19 +1,27 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
SiteSettings,
ProductCategory,
Product, Product,
ProductPhoto, ProductPhoto,
ProductVariant,
ProductOption,
Coupon, Coupon,
ShippingMethod, ShippingRate,
Order, Order,
Transaction, Transaction,
OrderLine, OrderLine,
) )
admin.site.register(SiteSettings)
admin.site.register(ProductCategory)
admin.site.register(Product) admin.site.register(Product)
admin.site.register(ProductPhoto) admin.site.register(ProductPhoto)
admin.site.register(ProductVariant)
admin.site.register(ProductOption)
admin.site.register(Coupon) admin.site.register(Coupon)
admin.site.register(ShippingMethod) admin.site.register(ShippingRate)
admin.site.register(Order) admin.site.register(Order)
admin.site.register(Transaction) admin.site.register(Transaction)
admin.site.register(OrderLine) admin.site.register(OrderLine)

View File

@ -7,6 +7,7 @@ class CoreConfig(AppConfig):
def ready(self): def ready(self):
from .signals import ( from .signals import (
# variant_saved,
order_created, order_created,
transaction_created, transaction_created,
order_line_post_save, order_line_post_save,

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
# Generated by Django 4.0.2 on 2022-03-11 02:25 # Generated by Django 4.0.2 on 2022-10-16 02:36
import core.weight import core.weight
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
import django.contrib.postgres.fields
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -16,46 +17,101 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('accounts', '0001_initial'), ('accounts', '0003_user_stripe_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Coupon',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('entire_order', 'Entire order'), ('shipping', 'Shipping'), ('specific_product', 'Specific products, collections and categories')], default='entire_order', max_length=20)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('code', models.CharField(db_index=True, max_length=12, unique=True)),
('valid_from', models.DateTimeField(default=django.utils.timezone.now)),
('valid_to', models.DateTimeField(blank=True, null=True)),
('discount_value_type', models.CharField(choices=[('fixed', 'USD'), ('percentage', '%')], default='fixed', max_length=10)),
('discount_value', models.DecimalField(decimal_places=2, max_digits=12)),
],
options={
'ordering': ('code',),
},
),
migrations.CreateModel( migrations.CreateModel(
name='Order', name='Order',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32)), ('status', models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially_fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32)),
('total_net_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)), ('subtotal_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('weight', django_measurement.models.MeasurementField(default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass)), ('coupon_amount', models.CharField(blank=True, max_length=255)),
('shipping_total', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('weight', django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, 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)),
('billing_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')), ('billing_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
('coupon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.coupon')),
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)), ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)),
('shipping_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')), ('shipping_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
], ],
options={
'ordering': ('-created_at',),
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Product', name='Product',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250)), ('name', models.CharField(max_length=250)),
('subtitle', models.CharField(blank=True, max_length=250)),
('description', models.TextField(blank=True)), ('description', models.TextField(blank=True)),
('sku', models.CharField(max_length=255, unique=True)), ('checkout_limit', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])),
('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)),
('visible_in_listings', models.BooleanField(default=False)), ('visible_in_listings', models.BooleanField(default=False)),
('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)),
], ],
options={
'ordering': ['sorting', 'name'],
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='ShippingMethod', name='ProductCategory',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)), ('name', models.CharField(max_length=255)),
('type', models.CharField(choices=[('price', 'Price based shipping'), ('weight', 'Weight based shipping')], max_length=30)), ('main_category', models.BooleanField(default=True)),
], ],
options={
'verbose_name': 'Product Category',
'verbose_name_plural': 'Product Categories',
},
),
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( migrations.CreateModel(
name='Transaction', name='Transaction',
@ -67,6 +123,47 @@ class Migration(migrations.Migration):
('order', models.OneToOneField(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.order')), ('order', models.OneToOneField(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.order')),
], ],
), ),
migrations.CreateModel(
name='TrackingNumber',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tracking_id', models.CharField(max_length=256)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='tracking_numbers', to='core.order')),
],
options={
'verbose_name': 'Tracking Number',
'verbose_name_plural': 'Tracking Numbers',
},
),
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.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)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='core.product')),
],
options={
'ordering': ['weight'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='ProductPhoto', name='ProductPhoto',
fields=[ fields=[
@ -75,6 +172,20 @@ class Migration(migrations.Migration):
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
], ],
), ),
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)),
('products', 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'),
),
migrations.CreateModel( migrations.CreateModel(
name='OrderLine', name='OrderLine',
fields=[ fields=[
@ -86,29 +197,17 @@ class Migration(migrations.Migration):
('unit_price', models.DecimalField(decimal_places=2, max_digits=12)), ('unit_price', models.DecimalField(decimal_places=2, max_digits=12)),
('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.0'), max_digits=5)), ('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.0'), max_digits=5)),
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='core.order')), ('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='core.order')),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.product')), ('variant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.productvariant')),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='order', model_name='coupon',
name='shipping_method', name='products',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.shippingmethod'), field=models.ManyToManyField(blank=True, to='core.Product'),
), ),
migrations.CreateModel( migrations.AddField(
name='Coupon', model_name='coupon',
fields=[ name='users',
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
('type', models.CharField(choices=[('entire_order', 'Entire order'), ('shipping', 'Shipping'), ('specific_product', 'Specific products, collections and categories')], default='entire_order', max_length=20)),
('name', models.CharField(blank=True, max_length=255, null=True)),
('code', models.CharField(db_index=True, max_length=12, unique=True)),
('valid_from', models.DateTimeField(default=django.utils.timezone.now)),
('valid_to', models.DateTimeField(blank=True, null=True)),
('discount_value_type', models.CharField(choices=[('fixed', 'USD'), ('percentage', '%')], default='fixed', max_length=10)),
('discount_value', models.DecimalField(decimal_places=2, max_digits=12)),
('products', models.ManyToManyField(blank=True, to='core.Product')),
],
options={
'ordering': ('code',),
},
), ),
] ]

View File

@ -1,36 +0,0 @@
# Generated by Django 4.0.2 on 2022-03-23 16:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='shippingmethod',
name='price',
field=models.DecimalField(decimal_places=2, default=0, max_digits=12),
),
migrations.AlterField(
model_name='order',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially_fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32),
),
migrations.CreateModel(
name='TrackingNumber',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tracking_id', models.CharField(max_length=256)),
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='tracking_numbers', to='core.order')),
],
options={
'verbose_name': 'Tracking Number',
'verbose_name_plural': 'Tracking Numbers',
},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.0.2 on 2022-10-29 14:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='shippingrate',
name='is_selectable',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='sitesettings',
name='default_shipping_rate',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.shippingrate'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.0.2 on 2022-03-23 17:04
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0002_shippingmethod_price_alter_order_status_and_more'),
]
operations = [
migrations.AddField(
model_name='trackingnumber',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='trackingnumber',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.0.2 on 2022-03-23 21:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0003_trackingnumber_created_at_trackingnumber_updated_at'),
]
operations = [
migrations.AddField(
model_name='order',
name='coupon',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.coupon'),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 4.0.2 on 2022-03-28 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_order_coupon'),
]
operations = [
migrations.AlterModelOptions(
name='product',
options={'ordering': ['sorting', 'name']},
),
migrations.AddField(
model_name='product',
name='sorting',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 4.0.2 on 2022-04-24 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_alter_product_options_product_sorting'),
]
operations = [
migrations.AlterModelOptions(
name='order',
options={'ordering': ('-created_at',)},
),
migrations.AddField(
model_name='order',
name='shipping_total',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.0.2 on 2022-04-30 15:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_alter_order_options_order_shipping_total'),
]
operations = [
migrations.AddField(
model_name='product',
name='subtitle',
field=models.CharField(blank=True, max_length=250),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 4.0.2 on 2022-04-30 23:11
import core.weight
from django.db import migrations, models
import django.db.models.deletion
import django_measurement.models
import measurement.measures.mass
class Migration(migrations.Migration):
dependencies = [
('core', '0007_product_subtitle'),
]
operations = [
migrations.AlterField(
model_name='order',
name='coupon',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.coupon'),
),
migrations.AlterField(
model_name='order',
name='weight',
field=django_measurement.models.MeasurementField(blank=True, default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass, null=True),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.0.2 on 2022-05-11 00:55
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0008_alter_order_coupon_alter_order_weight'),
]
operations = [
migrations.AddField(
model_name='coupon',
name='users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -8,8 +8,10 @@ from django.db.models.functions import Coalesce
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from django.core.cache import cache
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.serializers.json import DjangoJSONEncoder 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.forms.models import model_to_dict
from django_measurement.models import MeasurementField from django_measurement.models import MeasurementField
@ -21,42 +23,67 @@ from . import (
VoucherType, VoucherType,
TransactionStatus, TransactionStatus,
OrderStatus, OrderStatus,
ShippingMethodType ShippingProvider,
ShippingContainer,
build_usps_rate_request
) )
from .weight import WeightUnits, zero_weight from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProductEncoder(DjangoJSONEncoder): class SingletonBase(models.Model):
def default(self, obj): def set_cache(self):
logger.info(f"\n{obj}\n") cache.set(self.__class__.__name__, self)
return super().default(obj)
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): class ProductCategory(models.Model):
def get_queryset(self): name = models.CharField(max_length=255)
return super().get_queryset().annotate( main_category = models.BooleanField(default=True)
num_ordered=models.Sum('order_lines__quantity')
) 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): class Product(models.Model):
category = models.ForeignKey(
ProductCategory,
blank=True,
null=True,
on_delete=models.SET_NULL
)
name = models.CharField(max_length=250) name = models.CharField(max_length=250)
subtitle = models.CharField(max_length=250, blank=True) subtitle = models.CharField(max_length=250, blank=True)
description = models.TextField(blank=True) description = models.TextField(blank=True)
sku = models.CharField(max_length=255, unique=True) checkout_limit = models.IntegerField(
price = models.DecimalField( default=0,
max_digits=settings.DEFAULT_MAX_DIGITS, validators=[MinValueValidator(0)]
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
blank=True,
null=True,
)
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
blank=True,
null=True
) )
visible_in_listings = models.BooleanField(default=False) visible_in_listings = models.BooleanField(default=False)
@ -65,8 +92,6 @@ class Product(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
objects = ProductManager()
def __str__(self): def __str__(self):
return self.name return self.name
@ -86,6 +111,73 @@ class Product(models.Model):
ordering = ['sorting', 'name'] ordering = ['sorting', 'name']
class ProductVariantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
num_ordered=models.Sum('order_lines__quantity')
)
class ProductVariant(models.Model):
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): class ProductPhoto(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.ImageField(upload_to='products/images') image = models.ImageField(upload_to='products/images')
@ -148,17 +240,36 @@ class Coupon(models.Model):
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk}) return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
class ShippingMethod(models.Model): class ShippingRate(models.Model):
name = models.CharField(max_length=100) shipping_provider = models.CharField(
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES) max_length=255,
price = models.DecimalField( choices=ShippingProvider.CHOICES,
max_digits=settings.DEFAULT_MAX_DIGITS, default=ShippingProvider.USPS
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
default=0,
) )
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
)
is_selectable = models.BooleanField(default=True)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:shipmeth-detail', kwargs={'pk': self.pk}) return reverse('dashboard:rate-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): class OrderManager(models.Manager):
@ -186,7 +297,7 @@ class OrderManager(models.Manager):
class Order(models.Model): class Order(models.Model):
customer = models.ForeignKey( customer = models.ForeignKey(
User, User,
related_name="orders", related_name='orders',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True null=True
) )
@ -209,14 +320,6 @@ class Order(models.Model):
null=True, null=True,
on_delete=models.SET_NULL 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 = models.ForeignKey(
Coupon, Coupon,
related_name='orders', related_name='orders',
@ -224,19 +327,22 @@ class Order(models.Model):
blank=True, blank=True,
null=True null=True
) )
subtotal_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0
)
coupon_amount = models.CharField(max_length=255, blank=True)
shipping_total = models.DecimalField( shipping_total = models.DecimalField(
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=0 default=0
) )
total_amount = models.DecimalField(
total_net_amount = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
default=0 default=0
) )
weight = MeasurementField( weight = MeasurementField(
measurement=Weight, measurement=Weight,
unit_choices=WeightUnits.CHOICES, unit_choices=WeightUnits.CHOICES,
@ -250,6 +356,14 @@ class Order(models.Model):
objects = OrderManager() objects = OrderManager()
def minus_stock(self):
for line in self.lines.all():
line.minus_stock()
def add_stock(self):
for line in self.lines.all():
line.add_stock()
def get_total_quantity(self): def get_total_quantity(self):
return sum([line.quantity for line in self]) return sum([line.quantity for line in self])
@ -258,11 +372,11 @@ class Order(models.Model):
if self.coupon.discount_value_type == DiscountValueType.FIXED: if self.coupon.discount_value_type == DiscountValueType.FIXED:
return self.coupon.discount_value return self.coupon.discount_value
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount return (self.coupon.discount_value / Decimal('100')) * self.subtotal_amount
return Decimal('0') return Decimal('0')
def get_total_price_after_discount(self): def get_total_price_after_discount(self):
return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 2) return round((self.subtotal_amount - self.get_discount()) + self.shipping_total, 2)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
@ -292,13 +406,13 @@ class Transaction(models.Model):
class OrderLine(models.Model): class OrderLine(models.Model):
order = models.ForeignKey( order = models.ForeignKey(
Order, Order,
related_name="lines", related_name='lines',
editable=False, editable=False,
on_delete=models.CASCADE on_delete=models.CASCADE
) )
product = models.ForeignKey( variant = models.ForeignKey(
Product, ProductVariant,
related_name="order_lines", related_name='order_lines',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
blank=True, blank=True,
null=True, null=True,
@ -307,20 +421,17 @@ class OrderLine(models.Model):
quantity_fulfilled = models.IntegerField( quantity_fulfilled = models.IntegerField(
validators=[MinValueValidator(0)], default=0 validators=[MinValueValidator(0)], default=0
) )
customer_note = models.TextField(blank=True, default="") customer_note = models.TextField(blank=True, default='')
currency = models.CharField( currency = models.CharField(
max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH, max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
default=settings.DEFAULT_CURRENCY, default=settings.DEFAULT_CURRENCY,
) )
unit_price = models.DecimalField( unit_price = models.DecimalField(
max_digits=settings.DEFAULT_MAX_DIGITS, max_digits=settings.DEFAULT_MAX_DIGITS,
decimal_places=settings.DEFAULT_DECIMAL_PLACES, decimal_places=settings.DEFAULT_DECIMAL_PLACES,
) )
tax_rate = models.DecimalField( tax_rate = models.DecimalField(
max_digits=5, decimal_places=2, default=Decimal("0.0") max_digits=5, decimal_places=2, default=Decimal('0.0')
) )
def get_total(self): def get_total(self):
@ -330,6 +441,16 @@ class OrderLine(models.Model):
def quantity_unfulfilled(self): def quantity_unfulfilled(self):
return self.quantity - self.quantity_fulfilled return self.quantity - self.quantity_fulfilled
def minus_stock(self):
if self.variant.track_inventory:
self.variant.stock -= self.quantity
self.variant.save()
def add_stock(self):
if self.variant.track_inventory:
self.variant.stock += self.quantity
self.variant.save()
class TrackingNumber(models.Model): class TrackingNumber(models.Model):
order = models.ForeignKey( order = models.ForeignKey(
@ -349,3 +470,31 @@ class TrackingNumber(models.Model):
def __str__(self): def __str__(self):
return self.tracking_id return self.tracking_id
class Subscription(models.Model):
stripe_id = models.CharField(max_length=255, blank=True)
customer = models.OneToOneField(
User,
related_name='subscription',
on_delete=models.SET_NULL,
null=True
)
class SiteSettings(SingletonBase):
usps_user_id = models.CharField(max_length=255)
default_shipping_rate = models.ForeignKey(
ShippingRate,
blank=True,
null=True,
related_name='+',
on_delete=models.SET_NULL
)
def __str__(self):
return 'Site Settings'
class Meta:
verbose_name = 'Site Settings'
verbose_name_plural = 'Site Settings'

View File

@ -1,12 +1,16 @@
import logging import logging
import stripe
from io import BytesIO from io import BytesIO
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.db import models from django.db import models
from django.conf import settings
from . import OrderStatus, TransactionStatus from . import OrderStatus, TransactionStatus
from .models import Order, OrderLine, Transaction, TrackingNumber from .models import (
Product, ProductVariant, Order, OrderLine, Transaction, TrackingNumber
)
from .tasks import ( from .tasks import (
send_order_confirmation_email, send_order_confirmation_email,
send_order_shipped_email send_order_shipped_email
@ -14,6 +18,36 @@ from .tasks import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# @receiver(post_save, sender=ProductVariant, dispatch_uid='variant_created')
# def variant_saved(sender, instance, created, **kwargs):
# logger.info('Product was saved')
# if created or not instance.stripe_id:
# stripe.api_key = settings.STRIPE_API_KEY
# prod_response = stripe.Product.create(
# name=instance.product.name + ': ' + instance.name,
# description=instance.product.description
# )
# price_response = stripe.Price.create(
# unit_amount=int(instance.price * 100),
# currency=settings.DEFAULT_CURRENCY,
# product=prod_response['id']
# )
# instance.stripe_id = prod_response['id']
# instance.stripe_price_id = price_response['id']
# instance.save()
# else:
# stripe.Product.modify(
# instance.stripe_id,
# name=instance.product.name + ': ' + instance.name,
# description=instance.product.description
# )
# stripe.Price.modify(
# instance.stripe_price_id,
# unit_amount=int(instance.price * 100)
# )
@receiver(post_save, sender=Order, dispatch_uid="order_created") @receiver(post_save, sender=Order, dispatch_uid="order_created")
def order_created(sender, instance, created, **kwargs): def order_created(sender, instance, created, **kwargs):
if created: if created:
@ -37,6 +71,7 @@ def transaction_created(sender, instance, created, **kwargs):
instance.confirmation_email_sent = True instance.confirmation_email_sent = True
instance.save() instance.save()
@receiver(post_save, sender=TrackingNumber, dispatch_uid="trackingnumber_postsave") @receiver(post_save, sender=TrackingNumber, dispatch_uid="trackingnumber_postsave")
def trackingnumber_postsave(sender, instance, created, **kwargs): def trackingnumber_postsave(sender, instance, created, **kwargs):
if created: if created:
@ -59,6 +94,7 @@ def get_order_status(total_quantity_fulfilled, total_quantity_ordered):
else: else:
return OrderStatus.UNFULFILLED return OrderStatus.UNFULFILLED
@receiver(post_save, sender=OrderLine, dispatch_uid="order_line_post_save") @receiver(post_save, sender=OrderLine, dispatch_uid="order_line_post_save")
def order_line_post_save(sender, instance, created, **kwargs): def order_line_post_save(sender, instance, created, **kwargs):
if not created: if not created:

View File

@ -15,6 +15,7 @@ SHIP_ORDER_TEMPLATE = 'storefront/order_shipped'
ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel' ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
ORDER_REFUND_TEMPLATE = 'storefront/order_refund' ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
@shared_task(name='send_order_confirmation_email') @shared_task(name='send_order_confirmation_email')
def send_order_confirmation_email(order): def send_order_confirmation_email(order):
send_templated_mail( send_templated_mail(
@ -26,6 +27,7 @@ def send_order_confirmation_email(order):
logger.info(f"Order confirmation email sent to {order['email']}") logger.info(f"Order confirmation email sent to {order['email']}")
@shared_task(name='send_order_shipped_email') @shared_task(name='send_order_shipped_email')
def send_order_shipped_email(data): def send_order_shipped_email(data):
send_templated_mail( send_templated_mail(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,10 @@
<p>No other addresses.</p> <p>No other addresses.</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="panel__item">
<strong>Stripe ID</strong><br>
<p>{{ customer.stripe_id }}</p>
</div>
</section> </section>
{% with order_list=customer.orders.all %} {% with order_list=customer.orders.all %}
<section class="object__list"> <section class="object__list">
@ -67,7 +71,7 @@
<span class="order__status--display"> <span class="order__status--display">
<div class="status__dot order__status--{{order.status}}"></div> <div class="status__dot order__status--{{order.status}}"></div>
{{order.get_status_display}}</span> {{order.get_status_display}}</span>
<span>${{order.total_net_amount}}</span> <span>${{order.total_amount}}</span>
</a> </a>
{% empty %} {% empty %}
<span class="object__item">No orders</span> <span class="object__item">No orders</span>

View File

@ -22,5 +22,24 @@
<span class="object__item">No customers</span> <span class="object__item">No customers</span>
{% endfor %} {% endfor %}
</section> </section>
<section>
<div class="pagination">
<p class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</p>
</div>
</section>
</article> </article>
{% endblock content %} {% endblock content %}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,15 +4,12 @@
{% block content %} {% block content %}
<article> <article>
<header class="object__header"> <header class="object__header">
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1> <div>
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1>
<p>Date: {{ order.created_at }}</p>
</div>
<div class="object__menu"> <div class="object__menu">
<div class="dropdown"> <a class="action-button action-button--warning" href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<span class="dropdown__menu">Options &darr;</span>
<div class="dropdown__child">
<a href="{% url 'dashboard:order-cancel' order.pk %}">Cancel order</a>
<a href="">Return order</a>
</div>
</div>
<span class="order__status order__status--{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span> <span class="order__status order__status--{{order.status}}">{{order.get_status_display}} ({{order.total_quantity_fulfilled}} / {{order.total_quantity_ordered}})</span>
</div> </div>
</header> </header>
@ -26,14 +23,14 @@
</div> </div>
{% for item in order.lines.all %} {% for item in order.lines.all %}
<div class="object__item object__item--col5"> <div class="object__item object__item--col5">
{% with product=item.product %} {% with product=item.variant.product %}
<figure class="item__figure"> <figure class="item__figure">
<img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="product__image product__image--small" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
<figcaption><strong>{{product.name}}</strong><br>Grind: {{item.customer_note}}</figcaption> <figcaption><strong>{{item.variant}}</strong><br>{{item.customer_note}}</figcaption>
</figure> </figure>
<span>{{product.sku}}</span> <span>{{product.sku}}</span>
<span>{{item.quantity}}</span> <span>{{item.quantity}}</span>
<span>${{product.price}}</span> <span>${{item.variant.price}}</span>
<span>${{item.get_total}}</span> <span>${{item.get_total}}</span>
{% endwith %} {% endwith %}
</div> </div>
@ -103,12 +100,12 @@
</div> </div>
<div class="panel__item"> <div class="panel__item">
<p> <p>
<span>Subtotal: ${{order.total_net_amount}}</span><br> <span>Subtotal: ${{order.subtotal_amount}}</span><br>
{% if order.coupon %} {% if order.coupon %}
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br> <span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %} {% endif %}
<span>Shipping: ${{order.shipping_total}}</span><br> <span>Shipping: ${{order.shipping_total}}</span><br>
<span>Total: ${{order.get_total_price_after_discount}}</span> <span>Total: ${{order.total_amount}}</span>
</p> </p>
</div> </div>
</section> </section>

View File

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

View File

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

View File

@ -4,24 +4,23 @@
{% block content %} {% block content %}
<article> <article>
<header class="object__header"> <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> <a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
</header> </header>
<section class="object__list"> <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></span>
<span>Name</span> <span>Name</span>
<span>Visible</span> <span>Visible</span>
<span>Price</span>
</div> </div>
{% for product in product_list %} {% 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"> <figure class="product__figure">
<img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}"> <img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure> </figure>
<strong>{{product.name}}</strong> <strong>{{product.name}}</strong>
<span>{{product.visible_in_listings|yesno:"Yes,No"}}</span> <span>{{product.visible_in_listings|yesno:"Yes,No"}}</span>
<span>${{product.price}}</span>
</a> </a>
{% endfor %} {% endfor %}
</section> </section>

View File

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

View File

@ -2,13 +2,13 @@
{% block content %} {% block content %}
<article> <article>
<h1>Create Shipping Method</h1> <h1>Create Shipping Rate</h1>
<section> <section>
<form method="POST" action="{% url 'dashboard:shipmeth-create' %}"> <form method="POST" action="{% url 'dashboard:rate-create' %}">
{% 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 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> </p>
</form> </form>
</section> </section>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
{% extends "dashboard.html" %}
{% load static %}
{% block content %}
<article>
<header class="object__header">
<h1><img src="{% static 'images/warehouse.png' %}" alt=""> Stock</h1>
<p><strong>Total in warehouse</strong> = available stock + unfulfilled</p>
</header>
<section class="object__list">
<div class="object__item panel__header object__item--col5">
<span>Product</span>
<span>SKU</span>
<span>Available Stock</span>
<span>Total in warehouse</span>
</div>
{% for variant in variant_list %}
<div class="object__item object__item--col5">
{% with product=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>{{variant}}</strong></figcaption>
</figure>
<span>{{ variant.sku }}</span>
<span>{{ variant.stock }}</span>
<span>{{ variant.total_in_warehouse }}</span>
<a href="{% url 'dashboard:variant-restock' product.pk variant.pk %}" class="action-button">Restock &rarr;</a>
{% endwith %}
</div>
{% endfor %}
</section>
</article>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
{% extends "dashboard.html" %}
{% block content %}
<article class="product">
<header class="object__header">
<h1>Restock variant</h1>
</header>
<section class="object__panel">
<form class="panel__item" method="POST" action="{% url 'dashboard:variant-restock' product.pk variant.pk %}">
{% csrf_token %}
{{form.as_p}}
<p>Total in warehouse: {{ variant.total_in_warehouse }}</p>
<p class="form__submit">
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:stock' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

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

View File

@ -2,47 +2,245 @@ from django.urls import path, include
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('', views.DashboardHomeView.as_view(), name='home'), path(
path('config/', views.DashboardConfigView.as_view(), name='config'), '',
views.DashboardHomeView.as_view(),
name='home'
),
path(
'config/',
views.DashboardConfigView.as_view(),
name='config'
),
path(
'catalog/',
views.CatalogView.as_view(),
name='catalog'
),
path(
'stock/',
views.StockView.as_view(),
name='stock'
),
path('shipping-methods/new/', views.ShippingMethodCreateView.as_view(), name='shipmeth-create'), path(
path('shipping-methods/<int:pk>/', include([ 'shipping-rates/new/',
path('', views.ShippingMethodDetailView.as_view(), name='shipmeth-detail'), views.ShippingRateCreateView.as_view(),
name='rate-create'
),
path('shipping-rates/<int:pk>/', include([
path(
'',
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'
),
])), ])),
path('coupons/', views.CouponListView.as_view(), name='coupon-list'), path(
path('coupons/new/', views.CouponCreateView.as_view(), name='coupon-create'), 'coupons/',
views.CouponListView.as_view(),
name='coupon-list'
),
path(
'coupons/new/',
views.CouponCreateView.as_view(),
name='coupon-create'
),
path('coupons/<int:pk>/', include([ path('coupons/<int:pk>/', include([
path('', views.CouponDetailView.as_view(), name='coupon-detail'), path(
path('update/', views.CouponUpdateView.as_view(), name='coupon-update'), '',
path('delete/', views.CouponDeleteView.as_view(), name='coupon-delete'), views.CouponDetailView.as_view(),
name='coupon-detail'
),
path(
'update/',
views.CouponUpdateView.as_view(),
name='coupon-update'
),
path(
'delete/',
views.CouponDeleteView.as_view(),
name='coupon-delete'
),
])), ])),
path('orders/', views.OrderListView.as_view(), name='order-list'), path(
'orders/',
views.OrderListView.as_view(),
name='order-list'
),
path('orders/<int:pk>/', include([ path('orders/<int:pk>/', include([
path('', views.OrderDetailView.as_view(), name='order-detail'), path(
path('fulfill/', views.OrderFulfillView.as_view(), name='order-fulfill'), '',
path('cancel/', views.OrderCancelView.as_view(), name='order-cancel'), views.OrderDetailView.as_view(),
path('ship/', views.OrderTrackingView.as_view(), name='order-ship'), name='order-detail'
),
path(
'fulfill/',
views.OrderFulfillView.as_view(),
name='order-fulfill'
),
path(
'cancel/',
views.OrderCancelView.as_view(),
name='order-cancel'
),
path(
'ship/',
views.OrderTrackingView.as_view(),
name='order-ship'
),
])), ])),
path('products/', views.ProductListView.as_view(), name='product-list'), # Categories
path('products/new/', views.ProductCreateView.as_view(), name='product-create'), path('categories/', include([
path('products/<int:pk>/', include([ path(
path('', views.ProductDetailView.as_view(), name='product-detail'), '',
path('update/', views.ProductUpdateView.as_view(), name='product-update'), views.CategoryListView.as_view(),
path('delete/', views.ProductDeleteView.as_view(), name='product-delete'), name='category-list'
),
path('photos/new/', views.ProductPhotoCreateView.as_view(), name='prodphoto-create'), path(
path('photos/<int:photo_pk>/', include([ 'new/',
path('delete/', views.ProductPhotoDeleteView.as_view(), name='prodphoto-delete'), 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('customers/', views.CustomerListView.as_view(), name='customer-list'), path(
'products/',
views.ProductListView.as_view(),
name='product-list'
),
path(
'products/new/',
views.ProductCreateView.as_view(),
name='product-create'
),
path('products/<int:pk>/', include([
path(
'',
views.ProductDetailView.as_view(),
name='product-detail'
),
path(
'update/',
views.ProductUpdateView.as_view(),
name='product-update'
),
path(
'delete/',
views.ProductDeleteView.as_view(),
name='product-delete'
),
path(
'photos/new/',
views.ProductPhotoCreateView.as_view(),
name='prodphoto-create'
),
path('photos/<int:photo_pk>/', include([
path(
'delete/',
views.ProductPhotoDeleteView.as_view(),
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'
),
path(
'restock/',
views.ProductVariantStockUpdateView.as_view(),
name='variant-restock'
),
])),
])),
])),
# 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(
'customers/',
views.CustomerListView.as_view(),
name='customer-list'
),
path('customers/<int:pk>/', include([ path('customers/<int:pk>/', include([
path('', views.CustomerDetailView.as_view(), name='customer-detail'), path(
path('update/', views.CustomerUpdateView.as_view(), name='customer-update'), '',
# path('delete/', views.CustomerDeleteView.as_view(), name='customer-delete'), views.CustomerDetailView.as_view(),
name='customer-detail'
),
path(
'update/',
views.CustomerUpdateView.as_view(),
name='customer-update'
),
])), ])),
] ]

View File

@ -18,7 +18,8 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import ( from django.db.models import (
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value,
ExpressionWrapper, IntegerField
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@ -26,11 +27,14 @@ from accounts.models import User
from accounts.utils import get_or_create_customer from accounts.utils import get_or_create_customer
from accounts.forms import AddressForm from accounts.forms import AddressForm
from core.models import ( from core.models import (
ProductCategory,
Product, Product,
ProductPhoto, ProductPhoto,
ProductVariant,
ProductOption,
Order, Order,
OrderLine, OrderLine,
ShippingMethod, ShippingRate,
Transaction, Transaction,
TrackingNumber, TrackingNumber,
Coupon Coupon
@ -39,8 +43,7 @@ from core.models import (
from core import ( from core import (
DiscountValueType, DiscountValueType,
VoucherType, VoucherType,
OrderStatus, OrderStatus
ShippingMethodType
) )
from .forms import ( from .forms import (
OrderLineFulfillForm, OrderLineFulfillForm,
@ -72,7 +75,7 @@ class DashboardHomeView(LoginRequiredMixin, TemplateView):
status=OrderStatus.DRAFT status=OrderStatus.DRAFT
).filter( ).filter(
created_at__date=today created_at__date=today
).aggregate(total=Sum('total_net_amount'))['total'] ).aggregate(total=Sum('total_amount'))['total']
return context return context
@ -81,23 +84,71 @@ class DashboardConfigView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
today = timezone.localtime(timezone.now()).date() context['shipping_rate_list'] = ShippingRate.objects.all()
context['shipping_method_list'] = ShippingMethod.objects.all()
return context return context
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): class CatalogView(ListView):
model = ShippingMethod model = ProductCategory
template_name = 'dashboard/shipmeth_create_form.html' 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 StockView(ListView):
model = ProductVariant
context_object_name = 'variant_list'
template_name = 'dashboard/stock.html'
def get_queryset(self):
object_list = ProductVariant.objects.filter(
track_inventory=True
).prefetch_related('order_lines', 'product').annotate(
total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)
) - Sum('order_lines__quantity_fulfilled', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0)
).order_by('product')
return object_list
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__' fields = '__all__'
success_message = '%(name)s created.' success_message = '%(name)s created.'
class ShippingMethodDetailView(LoginRequiredMixin, DetailView): class ShippingRateUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = ShippingMethod model = ShippingRate
template_name = 'dashboard/shipmeth_detail.html' 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): class CouponListView(LoginRequiredMixin, ListView):
@ -168,10 +219,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
).select_related( ).select_related(
'customer', 'customer',
'billing_address', 'billing_address',
'shipping_address', 'shipping_address'
'shipping_method'
).prefetch_related( ).prefetch_related(
'lines__product__productphoto_set' 'lines__variant__product__productphoto_set'
) )
obj = queryset.get() obj = queryset.get()
return obj return obj
@ -183,9 +233,9 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Order model = Order
template_name = "dashboard/order_fulfill.html" template_name = 'dashboard/order_fulfill.html'
form_class = OrderLineFormset form_class = OrderLineFormset
success_message = "Order saved." success_message = 'Order saved.'
def form_valid(self, form): def form_valid(self, form):
form.save() form.save()
@ -204,6 +254,11 @@ class OrderCancelView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
'status': OrderStatus.CANCELED 'status': OrderStatus.CANCELED
} }
def form_valid(self, form):
form.instance.add_stock()
form.instance.save()
return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
@ -222,6 +277,42 @@ class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk}) 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): class ProductListView(LoginRequiredMixin, ListView):
model = Product model = Product
template_name = 'dashboard/product_list.html' template_name = 'dashboard/product_list.html'
@ -240,6 +331,20 @@ class ProductDetailView(LoginRequiredMixin, DetailView):
model = Product model = Product
template_name = 'dashboard/product_detail.html' 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): class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Product model = Product
@ -292,9 +397,149 @@ class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']}) return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
class ProductVariantCreateView(SuccessMessageMixin, CreateView):
model = ProductVariant
success_message = '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 ProductVariantStockUpdateView(LoginRequiredMixin, UpdateView):
model = ProductVariant
pk_url_kwarg = 'variant_pk'
success_message = 'ProductVariant saved.'
success_url = reverse_lazy('dashboard:stock')
template_name = 'dashboard/variant_restock.html'
fields = [
'stock',
]
context_object_name = 'variant'
def get_queryset(self):
queryset = ProductVariant.objects.annotate(
total_in_warehouse=F('stock') + Coalesce(Sum('order_lines__quantity', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)
) - Sum('order_lines__quantity_fulfilled', filter=Q(
order_lines__order__status=OrderStatus.UNFULFILLED) | Q(
order_lines__order__status=OrderStatus.PARTIALLY_FULFILLED)), 0)
).prefetch_related('order_lines', 'product')
return queryset
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)
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): class CustomerListView(LoginRequiredMixin, ListView):
model = User model = User
template_name = 'dashboard/customer_list.html' template_name = 'dashboard/customer_list.html'
paginate_by = 100
def get_queryset(self): def get_queryset(self):
object_list = User.objects.filter( object_list = User.objects.filter(
@ -305,7 +550,7 @@ class CustomerListView(LoginRequiredMixin, ListView):
'orders' 'orders'
).annotate( ).annotate(
num_orders=Count('orders') num_orders=Count('orders')
) ).order_by('first_name', 'last_name')
return object_list return object_list

View File

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

View File

@ -23,6 +23,8 @@ SENTRY_ENV = os.environ.get('SENTRY_ENV', 'development')
FACEBOOK_PIXEL_ID = os.environ.get('FACEBOOK_PIXEL_ID', '') FACEBOOK_PIXEL_ID = os.environ.get('FACEBOOK_PIXEL_ID', '')
STRIPE_API_KEY = os.environ.get('STRIPE_API_KEY', '')
PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '') PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '')
PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '') PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '')
PAYPAL_ENVIRONMENT = os.environ.get('PAYPAL_ENVIRONMENT', 'SANDBOX') PAYPAL_ENVIRONMENT = os.environ.get('PAYPAL_ENVIRONMENT', 'SANDBOX')

View File

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

View File

@ -9,6 +9,7 @@ urlpatterns = [
path('dashboard/', include(('dashboard.urls', 'dashboard'), namespace='dashboard')), path('dashboard/', include(('dashboard.urls', 'dashboard'), namespace='dashboard')),
path('accounts/', include('allauth.urls')), path('accounts/', include('allauth.urls')),
path('accounts/', include(('accounts.urls', 'accounts'), namespace='accounts')), path('accounts/', include(('accounts.urls', 'accounts'), namespace='accounts')),
# path('accounts/', include('django.contrib.auth.urls')), # path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,114 @@
// This is your test publishable API key.
const stripe = Stripe('pk_test_WuL7S3g73f4j9Y69pMF10r3k00G5IPCCSc')
// The items the customer wants to buy
const items = [{ id: 'xl-tshirt' }]
let elements
initialize()
checkStatus()
document
.querySelector('#payment-form')
.addEventListener('submit', handleSubmit)
// Fetches a payment intent and captures the client secret
async function initialize () {
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
})
const { clientSecret } = await response.json()
const appearance = {
theme: 'stripe'
}
elements = stripe.elements({ appearance, clientSecret })
const paymentElement = elements.create('payment')
paymentElement.mount('#payment-element')
}
async function handleSubmit (e) {
e.preventDefault()
setLoading(true)
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: 'http://localhost:4242/checkout.html'
}
})
// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === 'card_error' || error.type === 'validation_error') {
showMessage(error.message)
} else {
showMessage('An unexpected error occurred.')
}
setLoading(false)
}
// Fetches the payment intent status after payment submission
async function checkStatus () {
const clientSecret = new URLSearchParams(window.location.search).get(
'payment_intent_client_secret'
)
if (!clientSecret) {
return
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret)
switch (paymentIntent.status) {
case 'succeeded':
showMessage('Payment succeeded!')
break
case 'processing':
showMessage('Your payment is processing.')
break
case 'requires_payment_method':
showMessage('Your payment was not successful, please try again.')
break
default:
showMessage('Something went wrong.')
break
}
}
// ------- UI helpers -------
function showMessage (messageText) {
const messageContainer = document.querySelector('#payment-message')
messageContainer.classList.remove('hidden')
messageContainer.textContent = messageText
setTimeout(function () {
messageContainer.classList.add('hidden')
messageText.textContent = ''
}, 4000)
}
// Show a spinner on payment submission
function setLoading (isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector('#submit').disabled = true
document.querySelector('#spinner').classList.remove('hidden')
document.querySelector('#button-text').classList.add('hidden')
} else {
document.querySelector('#submit').disabled = false
document.querySelector('#spinner').classList.add('hidden')
document.querySelector('#button-text').classList.remove('hidden')
}
}

View File

@ -0,0 +1,128 @@
class Subscription {
static TWELVE_OZ
static SIXTEEN_OZ
static FIVE_LBS
static TWELVE_SHIPPING
static SIXTEEN_SHIPPING
static FIVE_SHIPPING
constructor(element, output) {
this.TWELVE_OZ = '12'
this.SIXTEEN_OZ = '16'
this.FIVE_LBS = '75'
this.TWELVE_SHIPPING = 7
this.SIXTEEN_SHIPPING = 5
this.FIVE_SHIPPING = 1
this.element = element
this.output = this.element.querySelector('.output')
this.shippingDiscount = 10
this.price = this.element.querySelector('select[name=size]')
this.products = this.element.querySelectorAll('input[name^=product]')
this.element.addEventListener('change', this.render.bind(this))
this.render()
}
get total_qty() {
return Array.from(this.products).reduce((total, current) => {
return total + Number(current.value)
}, 0)
}
get hasFreeShipping() {
switch(this.price.value) {
case this.TWELVE_OZ:
if (parseInt(this.total_qty) >= this.TWELVE_SHIPPING) {
return true
} else {
return false
}
break
case this.SIXTEEN_OZ:
if (parseInt(this.total_qty) >= this.SIXTEEN_SHIPPING) {
return true
} else {
return false
}
break
case this.FIVE_LBS:
if (parseInt(this.total_qty) >= this.FIVE_SHIPPING) {
return true
} else {
return false
}
break
default:
throw 'Something is wrong with the price'
}
}
get countToFreeShipping() {
switch(this.price.value) {
case this.TWELVE_OZ:
return this.TWELVE_SHIPPING - this.total_qty
break
case this.SIXTEEN_OZ:
return this.SIXTEEN_SHIPPING - this.total_qty
break
case this.FIVE_LBS:
return this.FIVE_SHIPPING
break
default:
throw 'Something is wrong with the price'
break
}
}
get shippingStatus() {
let items = 0
if (this.hasFreeShipping) {
return 'You have free shipping!'
} else {
return `Add ${this.countToFreeShipping} more item(s) for free shipping!`
}
}
get totalRetailPrice() {
let totalPrice = Array.from(this.products).reduce((total, current) => {
return total + (Number(this.price.value) * current.value);
}, 0);
return new Intl.NumberFormat('en-US', {
currency: 'USD',
style: 'currency',
}).format(totalPrice)
}
get totalPrice() {
let totalPrice = Array.from(this.products).reduce((total, current) => {
return total + (Number(this.price.value) * current.value);
}, 0);
let percentage = (this.shippingDiscount / 100) * totalPrice
return new Intl.NumberFormat('en-US', {
currency: 'USD',
style: 'currency',
}).format(totalPrice - percentage)
}
render() {
this.output.querySelector('.retail-price').innerText = this.totalRetailPrice
this.output.querySelector('.price').innerText = this.totalPrice
this.output.querySelector('.shipping').innerText = this.shippingStatus
}
add_item(item) {
this.items.push(item)
return this.items
}
}
const subCreateFromEl = document.querySelector('.subscription-create-form')
const sub = new Subscription(subCreateFromEl)

View File

@ -0,0 +1,77 @@
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #5469d4;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #5469d4;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@media only screen and (max-width: 600px) {
form {
width: 80vw;
min-width: initial;
}
}

View File

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

View File

@ -526,6 +526,22 @@ section:not(:last-child) {
} }
/* Breadcrumbs
========================================================================== */
.breadcrumbs {
margin-bottom: 1.5rem;
}
.breadcrumbs menu {
margin: 0;
padding: 0 1rem;
line-height: 1.75;
list-style: none;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ========================================================================== /* ==========================================================================
Articles Articles
========================================================================== */ ========================================================================== */
@ -639,6 +655,28 @@ article + article {
} }
.subscription-create-form {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.product__subscription-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
justify-items: center;
gap: 6rem;
overflow-y: scroll;
max-height: 50rem;
border-bottom: var(--default-border);
padding: 0 2rem 2rem;
}
.product__subscription-list div {
/*max-width: 10rem;*/
}
/* Product Detail /* Product Detail
========================================================================== */ ========================================================================== */
.product__detail { .product__detail {
@ -751,6 +789,7 @@ article + article {
.item__price { .item__price {
justify-self: end; justify-self: end;
text-align: right;
} }
.item__form, .item__form,
@ -889,3 +928,8 @@ footer > section {
text-align: center; text-align: center;
} }
.show-modal {
white-space: unset;
}

View File

@ -6,99 +6,131 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import OuterRef, Q, Subquery
from core.models import Product, OrderLine, Coupon from core.models import (
Product, ProductVariant, OrderLine, Coupon, ShippingRate
)
from core.usps import USPSApi from core.usps import USPSApi
from core import ( from core import (
DiscountValueType, DiscountValueType,
VoucherType, VoucherType,
TransactionStatus, TransactionStatus,
OrderStatus, OrderStatus,
ShippingMethodType,
ShippingService, ShippingService,
ShippingContainer, ShippingContainer,
CoffeeGrind CoffeeGrind,
build_usps_rate_request
) )
from .forms import UpdateCartItemForm
from .payments import CreateOrder from .payments import CreateOrder
logger = logging.getLogger(__name__) 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: class Cart:
item_class = CartItem
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
self.session = request.session self.session = request.session
self.coupon_code = self.session.get('coupon_code') self.coupon_code = self.session.get('coupon_code')
self.container = self.session.get('shipping_container')
cart = self.session.get(settings.CART_SESSION_ID) cart = self.session.get(settings.CART_SESSION_ID)
if not cart: if not cart:
cart = self.session[settings.CART_SESSION_ID] = {} cart = self.session[settings.CART_SESSION_ID] = []
self.cart = cart self.cart = cart
def add( def add(self, request, item, update_quantity=False):
self, request, product, quantity=1, grind='', update_quantity=False
):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'variations': {},
'price': str(product.price)
}
self.cart[product_id]['variations'][grind] = {'quantity': 0}
if update_quantity: if update_quantity:
self.cart[product_id]['variations'][grind]['quantity'] = quantity self.cart[item['variant']]['quantity'] = item['quantity']
else: else:
if not grind in self.cart[product_id]['variations']: self.add_or_update_item(item)
# create it
self.cart[product_id]['variations'][grind] = {'quantity': quantity} # TODO: abstract this to a function that will check the max amount of item in the cart
else:
# add to it
self.cart[product_id]['variations'][grind]['quantity'] += quantity
if len(self) <= 20: if len(self) <= 20:
self.check_item_stock_quantities(request)
self.save() self.save()
else: else:
messages.warning(request, "Cart is full: 20 items or less.") messages.warning(request, "Cart is full: 20 items or less.")
def add_or_update_item(self, new_item):
new_item_pk = int(new_item['variant'])
for item in self:
if new_item_pk == item['variant'].pk:
if new_item['options'] == item['options']:
item['quantity'] += new_item['quantity']
return
else:
continue
self.cart.append(new_item)
def save(self): def save(self):
self.session[settings.CART_SESSION_ID] = self.cart self.session[settings.CART_SESSION_ID] = self.cart
self.session.modified = True self.session.modified = True
logger.info(f'\nCart:\n{self.cart}\n') logger.info(f'\nCart:\n{self.cart}\n')
def remove(self, product, grind): def check_item_stock_quantities(self, request):
product_id = str(product.id) for item in self:
if product_id in self.cart: if item['variant'].track_inventory:
del self.cart[product_id]['variations'][grind] if item['quantity'] > item['variant'].stock:
if not self.cart[product_id]['variations']: messages.warning(request, 'Quantity added exceeds available stock.')
del self.cart[product_id] item['quantity'] = item['variant'].stock
self.save() self.save()
def remove(self, pk):
self.cart.pop(pk)
self.save()
def __iter__(self): def __iter__(self):
product_ids = self.cart.keys() for item in self.cart:
products = Product.objects.filter(id__in=product_ids) pk = item['variant'].pk if isinstance(item['variant'], ProductVariant) else item['variant']
for product in products: item['variant'] = ProductVariant.objects.get(pk=pk)
self.cart[str(product.id)]['product'] = product item['price_total'] = item['variant'].price * item['quantity']
for item in self.cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = Decimal(sum(self.get_item_prices()))
item['quantity'] = self.get_single_item_total_quantity(item)
yield item yield item
def __len__(self): def __len__(self):
return sum(self.get_all_item_quantities()) return sum([item['quantity'] for item in self.cart])
def get_all_item_quantities(self): def get_all_item_quantities(self):
for item in self.cart.values(): for item in self.cart:
yield sum([value['quantity'] for value in item['variations'].values()]) yield item['quantity']
def get_single_item_total_quantity(self, item): def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()]) return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self): def get_item_prices(self):
for item in self.cart.values(): for item in self:
yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()]) yield item['price_total']
# for item in self.cart.values():
# yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])
def get_total_price(self): def get_total_price(self):
return sum(self.get_item_prices()) return sum(self.get_item_prices())
@ -106,32 +138,38 @@ class Cart:
def get_total_weight(self): def get_total_weight(self):
if len(self) > 0: if len(self) > 0:
for item in self: for item in self:
return item['product'].weight.value * sum(self.get_all_item_quantities()) return item['variant'].weight.value * sum(self.get_all_item_quantities())
else: else:
return 0 return 0
def get_shipping_box(self, container=None): def get_shipping_container_choices(self):
if container: is_selectable = Q(
return container is_selectable=True
)
if self.container: min_weight_matched = Q(
return self.container min_order_weight__lte=self.get_total_weight()) | Q(
min_order_weight__isnull=True
if len(self) > 6 and len(self) <= 10: )
return ShippingContainer.LG_FLAT_RATE_BOX max_weight_matched = Q(
elif len(self) > 3 and len(self) <= 6: max_order_weight__gte=self.get_total_weight()) | Q(
return ShippingContainer.REGIONAL_RATE_BOX_B max_order_weight__isnull=True
elif len(self) <= 3: )
return ShippingContainer.REGIONAL_RATE_BOX_A containers = ShippingRate.objects.filter(
else: is_selectable & min_weight_matched & max_weight_matched
return ShippingContainer.VARIABLE )
return containers
def get_shipping_cost(self, container=None): def get_shipping_cost(self, container=None):
if len(self) > 0 and self.session.get("shipping_address"): if container is None:
try: container = self.session.get('shipping_container').container
usps_rate_request = self.build_usps_rate_request(container)
except TypeError as e: if len(self) > 0 and self.session.get('shipping_address'):
return Decimal('0.00') usps_rate_request = build_usps_rate_request(
str(self.get_total_weight()),
container,
str(self.session.get('shipping_address')['postal_code'])
)
usps = USPSApi(settings.USPS_USER_ID, test=True) usps = USPSApi(settings.USPS_USER_ID, test=True)
try: try:
@ -142,10 +180,11 @@ class Cart:
) )
logger.info(validation.result) logger.info(validation.result)
if 'Error' not in validation.result['RateV4Response']['Package']: package = dict(validation.result['RateV4Response']['Package'])
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate'] if 'Error' not in package:
rate = package['Postage']['CommercialRate']
else: else:
logger.error("USPS Rate error") logger.error('USPS Rate error')
rate = '0.00' rate = '0.00'
return Decimal(rate) return Decimal(rate)
else: else:
@ -159,22 +198,6 @@ class Cart:
pass pass
self.session.modified = True self.session.modified = True
def build_usps_rate_request(self, container=None):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
'pounds': '0',
'ounces': f'{self.get_total_weight()}',
'container': f'{self.get_shipping_box(container)}',
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}
def build_order_params(self, container=None): def build_order_params(self, container=None):
return \ return \
{ {
@ -187,7 +210,9 @@ class Cart:
'shipping_method': 'US POSTAL SERVICE ' + ( 'shipping_method': 'US POSTAL SERVICE ' + (
container if container else '' container if container else ''
), ),
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), 'shipping_address': self.build_shipping_address(
self.session.get('shipping_address')
),
} }
def create_order(self, container=None): def create_order(self, container=None):
@ -199,20 +224,22 @@ class Cart:
response = CreateOrder().create_order(params) response = CreateOrder().create_order(params)
return response return response
def get_line_options(self, options_dict):
options = ''
for key, value in options_dict.items():
options += f'{key}: {value}; '
return options
def build_bulk_list(self, order): def build_bulk_list(self, order):
bulk_list = [] bulk_list = []
for item in self: for item in self:
for key, value in item['variations'].items(): bulk_list.append(OrderLine(
bulk_list.append(OrderLine( order=order,
order=order, variant=item['variant'],
product=item['product'], customer_note=self.get_line_options(item['options']),
customer_note=next((v[1] for i, v in enumerate(CoffeeGrind.GRIND_CHOICES) if v[0] == key), None), unit_price=item['variant'].price,
unit_price=item['price'], quantity=item['quantity']
quantity=value['quantity'], ))
tax_rate=2,
))
return bulk_list return bulk_list
def build_shipping_address(self, address): def build_shipping_address(self, address):
@ -232,12 +259,25 @@ class Cart:
return Coupon.objects.get(code=self.coupon_code) return Coupon.objects.get(code=self.coupon_code)
return None return None
def get_coupon_total_for_specific_products(self):
for item in self.cart:
if item['variant'].product in self.coupon.products.all():
yield item['price_total']
def get_discount(self): def get_discount(self):
# SHIPPING
# ENTIRE_ORDER
# SPECIFIC_PRODUCT
if self.coupon: if self.coupon:
if self.coupon.discount_value_type == DiscountValueType.FIXED: if self.coupon.discount_value_type == DiscountValueType.FIXED:
return round(self.coupon.discount_value, 2) return round(self.coupon.discount_value, 2)
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE: elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
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') return Decimal('0')
def get_subtotal_price_after_discount(self): def get_subtotal_price_after_discount(self):

View File

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

View File

@ -1,5 +1,6 @@
import logging import logging
import json import json
import stripe
from requests import ConnectionError from requests import ConnectionError
from urllib.parse import quote from urllib.parse import quote
from django import forms from django import forms
@ -10,23 +11,36 @@ from localflavor.us.us_states import USPS_CHOICES
from usps import USPSApi, Address from usps import USPSApi, Address
from captcha.fields import CaptchaField from captcha.fields import CaptchaField
from core.models import Order from core.models import Order, ProductVariant
from core import CoffeeGrind, ShippingContainer from core import CoffeeGrind, ShippingContainer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form): class AddToCartForm(forms.Form):
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
def __init__(self, variants, options, *args, **kwargs):
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): class UpdateCartItemForm(forms.Form):
item_pk = forms.IntegerField(widget=forms.HiddenInput())
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
update = forms.BooleanField( update = forms.BooleanField(
required=False, required=False,
initial=True, initial=True,
widget=forms.HiddenInput widget=forms.HiddenInput()
) )
@ -105,15 +119,14 @@ class AddressForm(forms.Form):
class CheckoutShippingForm(forms.Form): class CheckoutShippingForm(forms.Form):
SHIPPING_CHOICES = [ def __init__(self, containers, *args, **kwargs):
(ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'), super().__init__(*args, **kwargs)
(ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
]
shipping_method = forms.ChoiceField( self.fields['shipping_method'] = forms.ChoiceField(
widget=forms.RadioSelect, label='',
choices=SHIPPING_CHOICES widget=forms.RadioSelect,
) choices=[(container.pk, f'{container.name} ${container.s_cost}') for container in containers]
)
class OrderCreateForm(forms.ModelForm): class OrderCreateForm(forms.ModelForm):
@ -124,14 +137,66 @@ class OrderCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Order model = Order
fields = ( fields = (
'total_net_amount', 'total_amount',
'shipping_total', 'shipping_total',
) )
widgets = { widgets = {
'total_net_amount': forms.HiddenInput(), 'total_amount': forms.HiddenInput(),
'shipping_total': forms.HiddenInput() 'shipping_total': forms.HiddenInput()
} }
class CouponApplyForm(forms.Form): class CouponApplyForm(forms.Form):
code = forms.CharField(label='Coupon code') code = forms.CharField(label='Coupon code')
class ContactForm(forms.Form):
GOOGLE = 'Google Search'
SHOP = 'The coffee shop'
WOM = 'Word of mouth'
PRODUCT = 'Coffee Bag'
STORE = 'Store'
OTHER = 'Other'
REFERAL_CHOICES = [
(GOOGLE, 'Google Search'),
(SHOP, '"Better Living Through Coffee" coffee shop'),
(WOM, 'Friend/Relative'),
(PRODUCT, 'Our Coffee Bag'),
(STORE, 'PT Food Coop/other store'),
(OTHER, 'Other (please describe below)'),
]
full_name = forms.CharField()
email_address = forms.EmailField()
referal = forms.ChoiceField(
label='How did you find our website?',
choices=REFERAL_CHOICES
)
subject = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
captcha = CaptchaField()
class SubscriptionCreateForm(forms.Form):
SEVEN_DAYS = 7
FOURTEEN_DAYS = 14
THIRTY_DAYS = 30
SCHEDULE_CHOICES = [
(SEVEN_DAYS, 'Every 7 days'),
(FOURTEEN_DAYS, 'Every 14 days'),
(THIRTY_DAYS, 'Every 30 days'),
]
TWELVE_OZ = 12
SIXTEEN_OZ = 16
FIVE_LBS = 75
SIZE_CHOICES = [
(TWELVE_OZ, '12 oz ($10.80)'),
(SIXTEEN_OZ, '16 oz ($14.40)'),
(FIVE_LBS, '5 lbs ($67.50)'),
]
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)
size = forms.ChoiceField(choices=SIZE_CHOICES)

View File

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

View File

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

View File

@ -0,0 +1,38 @@
{% 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>
<article>
<div class="breadcrumbs">
<menu>
<li><strong><a href="{% url 'storefront:product-list' %}">Shop</a></strong></li>
<span></span>
<li><strong>{{ category }}</strong></a></li>
</menu>
</div>
<section class="product__list">
{% for product in category.product_set.all %}
<a class="product__item" href="{% url 'storefront:product-detail' product.pk %}">
<figure class="product__figure">
<img class="product__image product__with-img-swap" data-altimg-src="{{product.get_second_img.image.url}}" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
<div>
<h3>{{ product.name }}</h3>
<h5>{{ product.subtitle }}</h5>
<p>{{product.description|truncatewords:20}}</p>
<p>$<strong>{{product.variants.first.price}}</strong></p>
</div>
</a>
{% endfor %}
</section>
</article>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,14 @@
<script defer src="{% static 'scripts/product_list.js' %}"></script> <script defer src="{% static 'scripts/product_list.js' %}"></script>
{% endblock %} {% 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 %} {% block content %}
<div class="site__banner site__banner--site"> <div class="site__banner site__banner--site">
<h1>Welcome to our new website!</h1> <h1>Welcome to our new website!</h1>
@ -21,7 +29,7 @@
<h3>{{ product.name }}</h3> <h3>{{ product.name }}</h3>
<h5>{{ product.subtitle }}</h5> <h5>{{ product.subtitle }}</h5>
<p>{{product.description|truncatewords:20}}</p> <p>{{product.description|truncatewords:20}}</p>
<p>$<strong>{{product.price}}</strong> | {{product.weight.oz|floatformat}}oz</p> <p>$<strong>{{product.variants.first.price}}</strong></p>
</div> </div>
</a> </a>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% load static %}
{% block head %}
<script src="{% static 'scripts/subscriptions.js' %}" defer></script>
{% endblock %}
{% block content %}
<div class="site__banner site__banner--site">
<h1>Subscriptions</h1>
<h4>SUBSCRIBE AND SAVE</h4>
</div>
<article>
<section class="">
<form action="" class="subscription-create-form">
{% csrf_token %}
<div>
<h4>Pick your coffee</h4>
<div class="product__subscription-list">
{% for product in product_list %}
<div>
<label for="">{{ product.name }}
<figure class="product__figure">
<img class="product__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
</figure>
</label>
<label for="">Quantity</label>
<input type="number" min="0" max="20" name="product_{{ product.pk }}">
</div>
{% endfor %}
</div>
</div>
<div class="output">
<h4>Pick your options</h4>
{{ form.as_p }}
<div class="cart__table-wrapper">
<table class="cart__totals">
<tr>
<td>Retail total</td>
<td><del class="retail-price"></del></td>
</tr>
<tr>
<td>Save</td>
<td>10%</td>
</tr>
<tr>
<th>Subscription total</th>
<td><strong class="price"></strong></td>
</tr>
</table>
</div>
<p class="shipping"></p>
<p>
<input type="submit" value="Continue to payment">
</p>
</div>
</form>
</section>
</article>
{% endblock %}

View File

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

View File

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

View File

@ -5,7 +5,18 @@ urlpatterns = [
path('about/', views.AboutView.as_view(), name='about'), path('about/', views.AboutView.as_view(), name='about'),
path('fair-trade/', views.FairTradeView.as_view(), name='fair-trade'), path('fair-trade/', views.FairTradeView.as_view(), name='fair-trade'),
path('reviews/', views.ReviewListView.as_view(), name='reviews'), 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(
'categories/<int:pk>/',
views.ProductCategoryDetailView.as_view(),
name='category-detail'
),
path('', views.ProductListView.as_view(), name='product-list'), path('', views.ProductListView.as_view(), name='product-list'),
path('products/<int:pk>/', include([ path('products/<int:pk>/', include([
path('', views.ProductDetailView.as_view(), name='product-detail'), path('', views.ProductDetailView.as_view(), name='product-detail'),
@ -18,12 +29,12 @@ urlpatterns = [
name='cart-add' name='cart-add'
), ),
path( path(
'cart/<int:pk>/update/<slug:grind>/', 'cart/<int:pk>/update/',
views.CartUpdateProductView.as_view(), views.CartUpdateProductView.as_view(),
name='cart-update', name='cart-update',
), ),
path( path(
'cart/<int:pk>/remove/<slug:grind>/', 'cart/<int:pk>/remove/',
views.cart_remove_product_view, views.cart_remove_product_view,
name='cart-remove', name='cart-remove',
), ),
@ -37,11 +48,6 @@ urlpatterns = [
views.paypal_order_transaction_capture, views.paypal_order_transaction_capture,
name='paypal-capture', name='paypal-capture',
), ),
path(
'paypal/webhooks/',
views.paypal_webhook_endpoint,
name='paypal-webhook'
),
path( path(
'checkout/address/', 'checkout/address/',
views.CheckoutAddressView.as_view(), views.CheckoutAddressView.as_view(),
@ -52,8 +58,16 @@ urlpatterns = [
views.CheckoutShippingView.as_view(), views.CheckoutShippingView.as_view(),
name='checkout-shipping', name='checkout-shipping',
), ),
path('checkout/', views.OrderCreateView.as_view(), name='order-create'), path(
path('done/', views.PaymentDoneView.as_view(), name='payment-done'), 'checkout/',
views.OrderCreateView.as_view(),
name='order-create'
),
path(
'done/',
views.PaymentDoneView.as_view(),
name='payment-done'
),
path( path(
'canceled/', 'canceled/',
views.PaymentCanceledView.as_view(), views.PaymentCanceledView.as_view(),
@ -90,5 +104,5 @@ urlpatterns = [
views.CustomerAddressUpdateView.as_view(), views.CustomerAddressUpdateView.as_view(),
name='address-update', name='address-update',
) )
])) ])),
] ]

View File

@ -1,14 +1,16 @@
import logging import logging
import requests import requests
import json import json
import stripe
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import JsonResponse, HttpResponseRedirect from django.http import JsonResponse, HttpResponseRedirect
from django.views.generic.base import RedirectView, TemplateView from django.views.generic.base import View, RedirectView, TemplateView
from django.views.generic.edit import ( from django.views.generic.edit import (
FormView, CreateView, UpdateView, DeleteView, FormMixin FormView, CreateView, UpdateView, DeleteView, FormMixin
) )
@ -21,6 +23,9 @@ from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.forms.models import model_to_dict 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.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
@ -30,13 +35,18 @@ from accounts.utils import get_or_create_customer
from accounts.forms import ( from accounts.forms import (
AddressForm as AccountAddressForm, CustomerUpdateForm AddressForm as AccountAddressForm, CustomerUpdateForm
) )
from core.models import Product, Order, Transaction, OrderLine, Coupon from core.models import (
from core.forms import ShippingMethodForm ProductCategory, Product, ProductVariant, ProductOption,
Order, Transaction, OrderLine, Coupon, ShippingRate,
SiteSettings
)
from core.forms import ShippingRateForm
from core import OrderStatus, ShippingContainer from core import OrderStatus, ShippingContainer
from .forms import ( from .forms import (
AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddToCartForm, UpdateCartItemForm, OrderCreateForm,
AddressForm, CouponApplyForm, CheckoutShippingForm, AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm,
SubscriptionCreateForm
) )
from .cart import Cart from .cart import Cart
from .payments import CaptureOrder from .payments import CaptureOrder
@ -50,13 +60,13 @@ class CartView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
cart = Cart(self.request) cart = Cart(self.request)
for item in cart: for i, item in enumerate(cart):
for variation in item['variations'].values(): item['update_quantity_form'] = UpdateCartItemForm(
variation['update_quantity_form'] = UpdateCartItemForm( initial={
initial={ 'item_pk': i,
'quantity': variation['quantity'] 'quantity': item['quantity']
} }
) )
context['cart'] = cart context['cart'] = cart
context['coupon_apply_form'] = CouponApplyForm() context['coupon_apply_form'] = CouponApplyForm()
return context return context
@ -70,23 +80,35 @@ class CartAddProductView(SingleObjectMixin, FormView):
def get_success_url(self): def get_success_url(self):
return reverse('storefront:cart-detail') return reverse('storefront:cart-detail')
def get_form(self, form_class=None):
variants = self.get_object().variants.filter(
Q(track_inventory=False) | Q(
track_inventory=True,
stock__gt=0
)
)
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): def post(self, request, *args, **kwargs):
cart = Cart(request) cart = Cart(request)
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cleaned_data = form.cleaned_data
cart.add( cart.add(
request=request, request=request,
product=self.get_object(), item={
grind=form.cleaned_data['grind'], 'variant': cleaned_data.pop('variant'),
quantity=form.cleaned_data['quantity'] 'quantity': cleaned_data.pop('quantity'),
'options': cleaned_data
}
) )
return self.form_valid(form) return self.form_valid(form)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
def form_valid(self, form):
return super().form_valid(form)
class CartUpdateProductView(SingleObjectMixin, FormView): class CartUpdateProductView(SingleObjectMixin, FormView):
model = Product model = Product
@ -102,9 +124,10 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
if form.is_valid(): if form.is_valid():
cart.add( cart.add(
request=request, request=request,
product=self.get_object(), item={
grind=kwargs['grind'], 'variant': form.cleaned_data['item_pk'],
quantity=form.cleaned_data['quantity'], 'quantity': form.cleaned_data['quantity']
},
update_quantity=form.cleaned_data['update'] update_quantity=form.cleaned_data['update']
) )
return self.form_valid(form) return self.form_valid(form)
@ -115,10 +138,9 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
return super().form_valid(form) return super().form_valid(form)
def cart_remove_product_view(request, pk, grind): def cart_remove_product_view(request, pk):
cart = Cart(request) cart = Cart(request)
product = get_object_or_404(Product, id=pk) cart.remove(pk)
cart.remove(product, grind)
return redirect('storefront:cart-detail') return redirect('storefront:cart-detail')
@ -143,14 +165,33 @@ class CouponApplyView(FormView):
return super().form_valid(form) 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(
Q(visible_in_listings=True),
Q(variants__track_inventory=False) |
Q(variants__track_inventory=True) & Q(variants__stock__gt=0)
).distinct()
)
)
return object_list
class ProductListView(ListView):
model = Product model = Product
template_name = 'storefront/product_list.html' template_name = 'storefront/product_list.html'
form_class = AddToCartForm
ordering = 'sorting' ordering = 'sorting'
queryset = Product.objects.filter( queryset = Product.objects.filter(
visible_in_listings=True visible_in_listings=True,
category__main_category=True
) )
@ -159,6 +200,18 @@ class ProductDetailView(FormMixin, DetailView):
template_name = 'storefront/product_detail.html' template_name = 'storefront/product_detail.html'
form_class = AddToCartForm form_class = AddToCartForm
def get_form(self, form_class=None):
variants = self.object.variants.filter(
Q(track_inventory=False) | Q(
track_inventory=True,
stock__gt=0
)
)
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): class CheckoutAddressView(FormView):
template_name = 'storefront/checkout_address.html' template_name = 'storefront/checkout_address.html'
@ -171,7 +224,7 @@ class CheckoutAddressView(FormView):
if user.is_authenticated and user.default_shipping_address: if user.is_authenticated and user.default_shipping_address:
address = user.default_shipping_address address = user.default_shipping_address
initial = { initial = {
'full_name': address.first_name+' '+address.last_name, 'full_name': address.first_name + ' ' + address.last_name,
'email': user.email, 'email': user.email,
'street_address_1': address.street_address_1, 'street_address_1': address.street_address_1,
'street_address_2': address.street_address_2, 'street_address_2': address.street_address_2,
@ -182,7 +235,7 @@ class CheckoutAddressView(FormView):
elif self.request.session.get('shipping_address'): elif self.request.session.get('shipping_address'):
address = self.request.session.get('shipping_address') address = self.request.session.get('shipping_address')
initial = { initial = {
'full_name': address['first_name']+' '+address['last_name'], 'full_name': address['first_name'] + ' ' + address['last_name'],
'email': address['email'], 'email': address['email'],
'street_address_1': address['street_address_1'], 'street_address_1': address['street_address_1'],
'street_address_2': address['street_address_2'], 'street_address_2': address['street_address_2'],
@ -216,40 +269,47 @@ class CheckoutShippingView(FormView):
template_name = 'storefront/checkout_shipping_form.html' template_name = 'storefront/checkout_shipping_form.html'
form_class = CheckoutShippingForm form_class = CheckoutShippingForm
success_url = reverse_lazy('storefront:order-create') success_url = reverse_lazy('storefront:order-create')
containers = None
def get_containers(self, request):
if self.containers is None:
cart = Cart(request)
self.containers = cart.get_shipping_container_choices()
return self.containers
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cart = Cart(request) if not self.request.session.get('shipping_address'):
if len(cart) != 6:
if 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
return HttpResponseRedirect(
reverse('storefront:order-create')
)
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.') messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
site_settings = cache.get('SiteSettings')
cart = Cart(self.request)
if len(self.get_containers(request)) == 0:
self.request.session['shipping_container'] = site_settings.default_shipping_rate
return HttpResponseRedirect(
reverse('storefront:order-create')
)
elif len(self.get_containers(request)) == 1:
self.request.session['shipping_container'] = self.get_containers(request)[0]
return HttpResponseRedirect(
reverse('storefront:order-create')
)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_form(self, form_class=None):
cart = Cart(self.request) cart = Cart(self.request)
context = super().get_context_data(**kwargs) for container in self.get_containers(self.request):
context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost( container.s_cost = cart.get_shipping_cost(container.container)
ShippingContainer.MD_FLAT_RATE_BOX if form_class is None:
) form_class = self.get_form_class()
context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost( return form_class(self.get_containers(self.request), **self.get_form_kwargs())
ShippingContainer.REGIONAL_RATE_BOX_B
)
return context
def form_valid(self, form): def form_valid(self, form):
cleaned_data = form.cleaned_data shipping_container = ShippingRate.objects.get(
self.request.session['shipping_container'] = cleaned_data.get( pk=form.cleaned_data.get('shipping_method')
'shipping_method'
) )
self.request.session['shipping_container'] = shipping_container
return super().form_valid(form) return super().form_valid(form)
@ -260,17 +320,13 @@ class OrderCreateView(CreateView):
success_url = reverse_lazy('storefront:payment-done') success_url = reverse_lazy('storefront:payment-done')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cart = Cart(request) if not self.request.session.get('shipping_address'):
if len(cart) != 6 and 'shipping_container' in self.request.session:
del self.request.session['shipping_container']
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.') messages.warning(request, 'Please add a shipping address.')
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
elif self.request.session.get('coupon_code'): elif self.request.session.get('coupon_code'):
address = self.request.session.get("shipping_address") address = self.request.session.get('shipping_address')
coupon = Coupon.objects.get( coupon = Coupon.objects.get(
code=self.request.session.get('coupon_code') code=self.request.session.get('coupon_code')
) )
@ -281,19 +337,22 @@ class OrderCreateView(CreateView):
if user in coupon.users.all(): if user in coupon.users.all():
del self.request.session['coupon_code'] del self.request.session['coupon_code']
messages.warning(request, 'Coupon already used.') messages.warning(request, 'Coupon already used.')
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_initial(self): def get_initial(self):
cart = Cart(self.request) cart = Cart(self.request)
shipping_container = self.request.session.get(
'shipping_container'
).container
try: try:
shipping_cost = cart.get_shipping_cost() shipping_cost = cart.get_shipping_cost(shipping_container)
except Exception as e: except Exception as e:
raise e('Could not get shipping information') logger.error('Could not get shipping information')
raise
shipping_cost = Decimal('0.00') shipping_cost = Decimal('0.00')
initial = { initial = {
'total_net_amount': cart.get_total_price(), 'total_amount': cart.get_total_price(),
'shipping_total': shipping_cost 'shipping_total': shipping_cost
} }
if self.request.session.get('shipping_address'): if self.request.session.get('shipping_address'):
@ -315,8 +374,12 @@ class OrderCreateView(CreateView):
def form_valid(self, form): def form_valid(self, form):
cart = Cart(self.request) cart = Cart(self.request)
form.instance.subtotal_amount = cart.get_subtotal_price_after_discount()
form.instance.coupon_amount = cart.get_discount()
form.instance.total_amount = cart.get_total_price_after_discount()
form.instance.weight = cart.get_total_weight()
shipping_address = self.request.session.get('shipping_address') shipping_address = self.request.session.get('shipping_address')
shipping_container = self.request.session.get('shipping_container') shipping_container = self.request.session.get('shipping_container').container
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
form.instance.status = OrderStatus.DRAFT form.instance.status = OrderStatus.DRAFT
self.object = form.save() self.object = form.save()
@ -339,6 +402,7 @@ def paypal_order_transaction_capture(request, transaction_id):
cart = Cart(request) cart = Cart(request)
order = Order.objects.get(pk=request.session.get('order_id')) order = Order.objects.get(pk=request.session.get('order_id'))
order.status = OrderStatus.UNFULFILLED order.status = OrderStatus.UNFULFILLED
order.minus_stock()
try: try:
coupon = Coupon.objects.get( coupon = Coupon.objects.get(
code=request.session.get('coupon_code') code=request.session.get('coupon_code')
@ -356,20 +420,11 @@ def paypal_order_transaction_capture(request, transaction_id):
transaction.save() transaction.save()
cart.clear() cart.clear()
logger.debug(f'\nPayPal Response data: {data}\n') logger.debug(f'\nPayPal Response data: {data}\n')
return JsonResponse(data) return JsonResponse(data)
else: else:
return JsonResponse({'details': 'invalid request'}) return JsonResponse({'details': 'invalid request'})
@csrf_exempt
@require_POST
def paypal_webhook_endpoint(request):
data = json.loads(request.body)
logger.info(data)
return JsonResponse(data)
class PaymentDoneView(TemplateView): class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html' template_name = 'storefront/payment_done.html'
@ -491,3 +546,43 @@ class FairTradeView(TemplateView):
class ReviewListView(TemplateView): class ReviewListView(TemplateView):
template_name = 'storefront/reviews.html' template_name = 'storefront/reviews.html'
class ContactFormView(FormView, SuccessMessageMixin):
template_name = 'storefront/contact_form.html'
form_class = ContactForm
success_url = reverse_lazy('storefront:product-list')
success_message = 'Message sent.'
def form_valid(self, form):
form.send_email()
return super().form_valid(form)
class SubscriptionCreateView(FormView):
template_name = 'storefront/subscriptions.html'
form_class = SubscriptionCreateForm
success_url = reverse_lazy('storefront:payment-done')
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY
context['product_list'] = Product.objects.filter(
visible_in_listings=True
)
return context
class CreatePayment(View):
def post(self, request, *args, **kwargs):
stripe.api_key = settings.STRIPE_API_KEY
intent = stripe.PaymentIntent.create(
amount=2000,
currency=settings.DEFAULT_CURRENCY,
automatic_payment_methods={
'enabled': True,
},
)
return JsonResponse({
'clientSecret': intent['client_secret']
})

View File

@ -17,8 +17,8 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Vollkorn:wght@900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Vollkorn:wght@900&display=swap" rel="stylesheet">
{% compress css %} {% compress css %}
<link rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}"> <link rel="stylesheet" type="text/css" href="{% static 'styles/normalize.css' %}">
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}"> <link rel="stylesheet" type="text/css" href="{% static 'styles/main.css' %}">
{% endcompress %} {% endcompress %}
<script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script> <script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script>
@ -46,7 +46,15 @@
<nav class="site__nav"> <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> <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"> <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:fair-trade' %}">Fair trade</a></li>
<li><a class="nav__link" href="{% url 'storefront:reviews' %}">Reviews</a></li> <li><a class="nav__link" href="{% url 'storefront:reviews' %}">Reviews</a></li>
<li><a class="nav__link" href="{% url 'storefront:about' %}">About</a></li> <li><a class="nav__link" href="{% url 'storefront:about' %}">About</a></li>

View File

@ -29,10 +29,14 @@
<img src="{% static 'images/store.png' %}" alt=""> <img src="{% static 'images/store.png' %}" alt="">
Home Home
</a> </a>
<a href="{% url 'dashboard:product-list' %}"> <a href="{% url 'dashboard:catalog' %}">
<img src="{% static 'images/cubes.png' %}" alt=""> <img src="{% static 'images/cubes.png' %}" alt="">
Catalog Catalog
</a> </a>
<a href="{% url 'dashboard:stock' %}">
<img src="{% static 'images/warehouse.png' %}" alt="">
Stock
</a>
<a href="{% url 'dashboard:order-list' %}"> <a href="{% url 'dashboard:order-list' %}">
<img src="{% static 'images/box.png' %}" alt=""> <img src="{% static 'images/box.png' %}" alt="">
Orders Orders