diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index aa44bc4..cdd7b89 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -27,11 +27,11 @@ }, "blinker": { "hashes": [ - "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d", - "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa" + "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", + "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182" ], - "markers": "python_version >= '3.7'", - "version": "==1.6.3" + "markers": "python_version >= '3.8'", + "version": "==1.7.0" }, "certifi": { "hashes": [ @@ -150,7 +150,7 @@ "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "markers": "platform_system == 'Windows'", + "markers": "sys_platform == 'win32'", "version": "==0.4.6" }, "dnspython": { @@ -344,11 +344,11 @@ }, "mako": { "hashes": [ - "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", - "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" + "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9", + "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.4" + "markers": "python_version >= '3.8'", + "version": "==1.3.0" }, "markupsafe": { "hashes": [ @@ -536,52 +536,52 @@ }, "pymupdf": { "hashes": [ - "sha256:264d5f6478d787c336520cf1a99e39bb6a0ef6d984550f925095c0e692dea7b5", - "sha256:29e1d82b16f7580280ae35a0ae78de55f15c92ec87b7f3a1372f40f37a053bf3", - "sha256:31405311c28fc8b3b2975a98b60bac388563748beaacb6da470f917678417e2d", - "sha256:332c1d5633c233458c4b65e6ad4a860391c507384bd2324a186b2702f8c64dfe", - "sha256:3400b582be3d71f1c0974701fcfda32f0c2ebb75a78c2aea430552b0c6896546", - "sha256:3a01c93c69e74068c1618631a750677fd088708d2b09b3c23809b099fa4ffa39", - "sha256:3f5fc705e8790217d23ab5e7ac2c05d82e050f6271b710300288adfe87a71072", - "sha256:435a108cf8b53302500b52adb2cccbf2afa51c94ab3c705b250245090b46f5da", - "sha256:4508ee04c46cac8356a9d04f0d9a63f845770d2abb54caf512b44d22f0e80300", - "sha256:460b47a1a17335d444ec441b68c083da5e51cdfcfa67a6638de69fe5e97f4ad2", - "sha256:4a53b2bf19be687160e4d18c27680e5326687aa39a7e31641d32a61edadbbfd9", - "sha256:4b1bd9a91dee18bc95d7af2c593a214857a03e4fcd9a1eb01588df432de24c58", - "sha256:53278c6a3d0a5dc8f221e0a77c065a61fd0598f9d8d9ef5be53de0c0a7d2df90", - "sha256:59755a600c25a282589b548ffa045aed59c2df7b76943978cabb1825f0c03ec4", - "sha256:64ab1097e3a077ae9db6a98d01e2e77087894ebd85b702edf5eb85d05ab8c0f1", - "sha256:693979ad4c8885729ac126b3202f1cb645f3392ad7e0964c2d924e61bc0e0a9d", - "sha256:705a7aed0a917c35bb5efa4d94a7e8092705b3395726f9770d2b888de775f437", - "sha256:9586bc98a322e546cf2e477309806aa4a3e1d18efc9b93fc2e2b3d8131e1b9f7", - "sha256:9bde3683e254661e6b0032006f0ef7025ade2a33d3e3045499e71b76ea99942c", - "sha256:b234805b615b2d45dcb1bfe5c2167dc4121e31d618ab557856a3153b94c1676b", - "sha256:bb345ef1120db4f78ec0f229514d333ea3e7d367875c1400423a9b3e2b48ffc0", - "sha256:c71b5e80a08272b9f3012314dc47ee2423270b30262d07ec7dd9709ae2bde1ac", - "sha256:d2e9cfa46193fab196c27cb07561e1bb0938450984c2f01b3739f254a31b639e", - "sha256:d3bef175707693a2f53fe0fe4e546e3187c7876aedabfe43d9a916060bac9073", - "sha256:da1b08b5348152f2940fa183d0265a6b6eb6f0292fae44b576eaf8e53723e336", - "sha256:dbce86df507f6bce118b12b33d893f1d3512013c898174211e903da78e1916aa", - "sha256:eafa1bce0860320ddbb7edb4ab5678e02051db5450251ba8e918713d9a70c03c", - "sha256:ee9e9ce1897eeac0fc33cf99084067c14250312a5dbc1372012c3d2f0e7a4af5", - "sha256:f7a8f91681b88ad216c36911e08ea25d2b3121350d52f4f8d76aeb0b7fcc6bef", - "sha256:fab599d23fa490725e5b5a70bfb6bc87acf5ceb70abe11ad2ef2b2f516961f31", - "sha256:faebdf8679706964f87617ee43b8d0107587d20b526892b538222146a4c32d43" + "sha256:0f852d125defc26716878b1796f4d68870e9065041d00cf46bde317fd8d30e68", + "sha256:1103eea4ab727e32b9cb93347b35f71562033018c333a7f3a17d115e980fea4a", + "sha256:19d1711d5908c4527ad2deef5af2d066649f3f9a12950faf30be5f7251d18abc", + "sha256:1cbcf05c06f314fdf3042ceee674e9a0ac7fae598347d5442e2138c6046d4e82", + "sha256:224c341fe254adda97c8f06a4c5838cdbcf609fa89e70b1fb179752533378f2f", + "sha256:271bdf6059bb8347f9c9c6b721329bd353a933681b1fc62f43241b410e7ab7ae", + "sha256:2885a26220a32fb45ea443443b72194bb7107d6862d8d546b59e4ad0c8a1f2c9", + "sha256:2c141f33e2733e48de8524dfd2de56d889feef0c7773b20a8cd216c03ab24793", + "sha256:2e27857a15c8a810d0b66455b8c8a79013640b6267a9b4ea808a5fe1f47711f2", + "sha256:361cab1be45481bd3dc4e00ec82628ebc189b4f4b6fd9bd78a00cfeed54e0034", + "sha256:3ce2d3678dbf822cff213b1902f2e59756313e543efd516a2b4f15bb0353bd6c", + "sha256:3f0f9b76bc4f039e7587003cbd40684d93a98441549dd033cab38ca07d61988d", + "sha256:4ac9673a6d6ee7e80cb242dacb43f9ca097b502d9c5e44687dbdffc2bce7961a", + "sha256:4d06751d5cd213e96f84f2faaa71a51cf4d641851e07579247ca1190121f173b", + "sha256:526b26a5207e923aab65877ad305644402851823a352cb92d362053426899354", + "sha256:57725e15872f7ab67a9fb3e06e5384d1047b2121e85755c93a6d4266d3ca8983", + "sha256:57e22bea69690450197b34dcde16bd9fe0265ac4425b4033535ccc5c044246fb", + "sha256:5bdf7020b90987412381acc42427dd1b7a03d771ee9ec273de003e570164ec1a", + "sha256:5cd05700c8f18c9dafef63ac2ed3b1099ca06017ca0c32deea13093cea1b8671", + "sha256:618b8e884190ac1cca9df1c637f87669d2d532d421d4ee7e4763c848dc4f3a1e", + "sha256:6e319c1f49476e07b9a12017c2d031687617713f8a46b7adcec03c636ed04607", + "sha256:761501a4965264e81acdd8f2224f993020bf24474e9b34fcdb5805a6826eda1c", + "sha256:8fd9c4ee1dd4744a515b9190d8ba9133348b0d94c362293ed77726aa1c13b0a6", + "sha256:951d280c1daafac2fd6a664b031f7f98b27eb2def55d39c92a19087bd8041c5d", + "sha256:991a37e1cba43775ce094da87cf0bf72172a5532a09644003276bc8bfdfe9f1a", + "sha256:c4eb71b88a22c1008f764b3121b36a9d25340f9920b870508356050a365d9ca1", + "sha256:c8ea81964c1433ea163ad4b53c56053a87a9ef6e1bd7a879d4d368a3988b60d1", + "sha256:e047571d799b30459ad7ee0bc6e68900a7f6b928876f956c976f279808814e72", + "sha256:e2d64799c6d9a3735be9e162a5d11061c0b7fbcb1e5fc7446e0993d0f815a93a", + "sha256:e33f8ec5ba7265fe78b30332840b8f454184addfa79f9c27f160f19789aa5ffd", + "sha256:fd8388e82b6045807d19addf310d8119d32908e89f76cc8bbf8cf1ec36fce947" ], "index": "pypi", - "version": "==1.23.5" + "version": "==1.23.6" }, "pymupdfb": { "hashes": [ - "sha256:2b71b5b7987f2ebe9f6893544151ede2de74ec30651eef584039eb5f9c7c02aa", - "sha256:80137c37a4b0d5abeb988434c7d7eb3f9087afdd0754f4bf2f8840a788e691ae", - "sha256:85cbc308085a4ec794e0da790965985cc5ccb21b2abc09732e072f6eaf10150b", - "sha256:d0095f28b2bcd64ed8a9636dfba193108eeb6c24d0ec71fa3f88cb15aee67a30", - "sha256:e26705e1a4ea42926b70c5655f2509d555a4774d1d1382ecc7e76466695209e6", - "sha256:f269814bafdffd5558d44af3de63eaa531d498de640a79cf6c7072011fd4088f" + "sha256:009e2cff166059e13bf71f93919e688f46b8fc11d122433574cfb0cc9134690e", + "sha256:7132b30e6ad6ff2013344e3a481b2287fe0be3710d80694807dd6e0d8635f085", + "sha256:7bef75988e6979b10ca804cf9487f817aae43b0fff1c6e315b3b9ee0cf1cc32f", + "sha256:9925816cbe3e05e920f9be925e5752c2eef42b793885b62075bb0f6a69178598", + "sha256:9d24ddadc204e895bee5000ddc7507c801643548e59f5a56aad6d32981d17eeb", + "sha256:e5af77580aad3d1103aeec57009d156bfca429cecda14a17c573fcbe97bafb30" ], "markers": "python_version >= '3.8'", - "version": "==1.23.5" + "version": "==1.23.6" }, "pytest": { "hashes": [ @@ -609,58 +609,58 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0b0b3f2686c3f162123adba3cb8b626ed7e9b8433ab528e36ed270b4f70d1cdb", - "sha256:0c1fea8c0abcb070ffe15311853abfda4e55bf7dc1d4889497b3403629f3bf00", - "sha256:0e1ce8ebd2e040357dde01a3fb7d30d9b5736b3e54a94002641dfd0aa12ae6ce", - "sha256:129415f89744b05741c6f0b04a84525f37fbabe5dc3774f7edf100e7458c48cd", - "sha256:13790cb42f917c45c9c850b39b9941539ca8ee7917dacf099cc0b569f3d40da7", - "sha256:14cd3bcbb853379fef2cd01e7c64a5d6f1d005406d877ed9509afb7a05ff40a5", - "sha256:154a32f3c7b00de3d090bc60ec8006a78149e221f1182e3edcf0376016be9396", - "sha256:19c6986cf2fb4bc8e0e846f97f4135a8e753b57d2aaaa87c50f9acbe606bd1db", - "sha256:2096d6b018d242a2bcc9e451618166f860bb0304f590d205173d317b69986c95", - "sha256:2c9bac865ee06d27a1533471405ad240a6f5d83195eca481f9fc4a71d8b87df8", - "sha256:3076740335e4aaadd7deb3fe6dcb96b3015f1613bd190a4e1634e1b99b02ec86", - "sha256:3940677d341f2b685a999bffe7078697b5848a40b5f6952794ffcf3af150c301", - "sha256:3aa1472bf44f61dd27987cd051f1c893b7d3b17238bff8c23fceaef4f1133868", - "sha256:40b1206a0d923e73aa54f0a6bd61419a96b914f1cd19900b6c8226899d9742ad", - "sha256:4bb062784f37b2d75fd9b074c8ec360ad5df71f933f927e9e95c50eb8e05323c", - "sha256:4e869a8ff7ee7a833b74868a0887e8462445ec462432d8cbeff5e85f475186da", - "sha256:4f6ff392b27a743c1ad346d215655503cec64405d3b694228b3454878bf21590", - "sha256:505f503763a767556fa4deae5194b2be056b64ecca72ac65224381a0acab7ebe", - "sha256:53a766cb0b468223cafdf63e2d37f14a4757476157927b09300c8c5832d88560", - "sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b", - "sha256:54bcceaf4eebef07dadfde424f5c26b491e4a64e61761dea9459103ecd6ccc95", - "sha256:55914d45a631b81a8a2cb1a54f03eea265cf1783241ac55396ec6d735be14883", - "sha256:564e9f9e4e6466273dbfab0e0a2e5fe819eec480c57b53a2cdee8e4fdae3ad5f", - "sha256:56a7e2bb639df9263bf6418231bc2a92a773f57886d371ddb7a869a24919face", - "sha256:58a3aba1bfb32ae7af68da3f277ed91d9f57620cf7ce651db96636790a78b736", - "sha256:625b72d77ac8ac23da3b1622e2da88c4aedaee14df47c8432bf8f6495e655de2", - "sha256:69fd9e41cf9368afa034e1c81f3570afb96f30fcd2eb1ef29cb4d9371c6eece2", - "sha256:6ac28bd6888fe3c81fbe97584eb0b96804bd7032d6100b9701255d9441373ec1", - "sha256:7c6c3e9350f9fb16de5b5e5fbf17b578811a52d71bb784cc5ff71acb7de2a7f9", - "sha256:7ee7ccf47aa503033b6afd57efbac6b9e05180f492aeed9fcf70752556f95624", - "sha256:875de9414393e778b655a3d97d60465eb3fae7c919e88b70cc10b40b9f56042d", - "sha256:8db5ba8b7da759b727faebc4289a9e6a51edadc7fc32207a30f7c6203a181592", - "sha256:92e512a6af769e4725fa5b25981ba790335d42c5977e94ded07db7d641490a85", - "sha256:9886a72c8e6371280cb247c5d32c9c8fa141dc560124348762db8a8b236f8692", - "sha256:9e55dff5ec115316dd7a083cdc1a52de63693695aecf72bc53a8e1468ce429e5", - "sha256:a42c9fa3abcda0dcfad053e49c4f752eef71ecd8c155221e18b99d4224621176", - "sha256:a571bc8ac092a3175a1d994794a8e7a1f2f651e7c744de24a19b4f740fe95034", - "sha256:af66001d7b76a3fab0d5e4c1ec9339ac45748bc4a399cbc2baa48c1980d3c1f4", - "sha256:b39a6e21110204a8c08d40ff56a73ba542ec60bab701c36ce721e7990df49fb9", - "sha256:b560f075c151900587ade06706b0c51d04b3277c111151997ea0813455378ae0", - "sha256:c8f1792d20d2f4e875ce7a113f43c3561ad12b34ff796b84002a256f37ce9437", - "sha256:cb9a758ad973e795267da334a92dd82bb7555cb36a0960dcabcf724d26299db8", - "sha256:ccca778c0737a773a1ad86b68bda52a71ad5950b25e120b6eb1330f0df54c3d0", - "sha256:ccd87c25e4c8559e1b918d46b4fa90b37f459c9b4566f1dfbce0eb8122571547", - "sha256:d143c5a9dada696bcfdb96ba2de4a47d5a89168e71d05a076e88a01386872f97", - "sha256:d80eeb5189d7d4b1af519fc3f148fe7521b9dfce8f4d6a0820e8f5769b005051", - "sha256:e04ab55cf49daf1aeb8c622c54d23fa4bec91cb051a43cc24351ba97e1dd09f5", - "sha256:f146c61ae128ab43ea3a0955de1af7e1633942c2b2b4985ac51cc292daf33222", - "sha256:f776c2c30f0e5f4db45c3ee11a5f2a8d9de68e81eb73ec4237de1e32e04ae81c" + "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3", + "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884", + "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74", + "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d", + "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc", + "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca", + "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d", + "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf", + "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846", + "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306", + "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221", + "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5", + "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89", + "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55", + "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72", + "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea", + "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8", + "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577", + "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df", + "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4", + "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d", + "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34", + "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4", + "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24", + "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6", + "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965", + "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35", + "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b", + "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab", + "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22", + "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4", + "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204", + "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855", + "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d", + "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab", + "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69", + "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693", + "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e", + "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8", + "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0", + "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45", + "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab", + "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1", + "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d", + "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda", + "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b", + "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18", + "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac", + "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60" ], "markers": "python_version >= '3.7'", - "version": "==2.0.22" + "version": "==2.0.23" }, "sqlalchemy-utils": { "hashes": [ diff --git a/backend/migrations/versions/015f4256bb4c_add_table_for_logintoken_de_activation_.py b/backend/migrations/versions/015f4256bb4c_add_table_for_logintoken_de_activation_.py new file mode 100644 index 0000000..4fb3d03 --- /dev/null +++ b/backend/migrations/versions/015f4256bb4c_add_table_for_logintoken_de_activation_.py @@ -0,0 +1,34 @@ +"""Add table for logintoken de-/activation dates + +Revision ID: 015f4256bb4c +Revises: 9a8c73f0ab11 +Create Date: 2023-11-19 20:52:03.377745 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '015f4256bb4c' +down_revision = '9a8c73f0ab11' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('login_token_dates', + sa.Column('token', sa.String(length=15), nullable=False), + sa.Column('activation_date', sa.Date(), server_default=sa.text('now()'), nullable=False), + sa.Column('deactivation_date', sa.Date(), nullable=True), + sa.ForeignKeyConstraint(['token'], ['login_token.token'], ), + sa.PrimaryKeyConstraint('token', 'activation_date') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('login_token_dates') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/9a8c73f0ab11_add_receiptitem_names.py b/backend/migrations/versions/9a8c73f0ab11_add_receiptitem_names.py new file mode 100644 index 0000000..55692f3 --- /dev/null +++ b/backend/migrations/versions/9a8c73f0ab11_add_receiptitem_names.py @@ -0,0 +1,35 @@ +"""add receiptitem names + +Revision ID: 9a8c73f0ab11 +Revises: 36f532800705 +Create Date: 2023-11-18 23:38:56.865780 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9a8c73f0ab11' +down_revision = '36f532800705' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('receipt_item', schema=None) as batch_op: + batch_op.add_column(sa.Column('name', sa.String(), nullable=True)) + op.execute("UPDATE receipt_item SET name='Unknown' WHERE name IS NULL;") + with op.batch_alter_table('receipt_item', schema=None) as batch_op: + batch_op.alter_column('name', nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('receipt_item', schema=None) as batch_op: + batch_op.drop_column('name') + + # ### end Alembic commands ### diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 8b57fc5..c968cb6 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -12,5 +12,6 @@ from .category import Category from .item import Item from .brand import Brand from .item_category import item_category +from .login_token_dates import LoginTokenDates from .schemas import * \ No newline at end of file diff --git a/backend/models/login_token.py b/backend/models/login_token.py index 752ab7e..2c7b7e3 100644 --- a/backend/models/login_token.py +++ b/backend/models/login_token.py @@ -8,6 +8,7 @@ class LoginToken(db.Model): 'establishment.id'), primary_key=True, server_onupdate=db.FetchedValue()) token = db.Column(db.String(15), nullable=False, unique=True) + LoginTokenDates = db.relationship("LoginTokenDates", backref='LoginToken', lazy='dynamic') Payment = db.relationship("Payment", backref='LoginToken', lazy='dynamic') Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic') diff --git a/backend/models/login_token_dates.py b/backend/models/login_token_dates.py new file mode 100644 index 0000000..d0739bc --- /dev/null +++ b/backend/models/login_token_dates.py @@ -0,0 +1,13 @@ +from src import db + +class LoginTokenDates(db.Model): + token = db.Column(db.ForeignKey('login_token.token'), + nullable = False, primary_key=True, + server_onupdate=db.FetchedValue()) + activation_date = db.Column(db.Date, + nullable=False, primary_key=True, + server_default=db.func.now()) + deactivation_date = db.Column(db.Date, nullable=True) + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/backend/models/receipt_item.py b/backend/models/receipt_item.py index 8da5820..42f4d22 100644 --- a/backend/models/receipt_item.py +++ b/backend/models/receipt_item.py @@ -5,6 +5,7 @@ class ReceiptItem(db.Model): receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue()) item = db.Column(db.SmallInteger, primary_key=True, nullable=False) + name = db.Column(db.String, nullable=False) amount = db.Column(db.SmallInteger, nullable=False, default=str(1)) price = db.Column(db.SmallInteger, nullable=False) diff --git a/backend/models/schemas/receipt_item.py b/backend/models/schemas/receipt_item.py index 501061f..1c215a6 100644 --- a/backend/models/schemas/receipt_item.py +++ b/backend/models/schemas/receipt_item.py @@ -10,5 +10,7 @@ class ReceiptItemSchema(ma.SQLAlchemySchema): include_fk = True Receipt = ma.Nested(ReceiptSchema) - price = ma.auto_field() + item = ma.auto_field() + name = ma.auto_field() amount = ma.auto_field() + price = ma.auto_field() diff --git a/backend/requirements.txt b/backend/requirements.txt index c6897f7..5c14654 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,10 @@ -i https://pypi.org/simple alembic==1.12.1; python_version >= '3.7' -blinker==1.6.3; python_version >= '3.7' +blinker==1.7.0; python_version >= '3.8' certifi==2023.7.22; python_version >= '3.6' charset-normalizer==3.3.2; python_full_version >= '3.7.0' click==8.1.7; python_version >= '3.7' -colorama==0.4.6; platform_system == 'Windows' +colorama==0.4.6; sys_platform == 'win32' dnspython==2.4.2; python_version >= '3.8' and python_version < '4.0' dominate==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' email-validator==2.1.0.post1 @@ -22,7 +22,7 @@ idna==3.4; python_version >= '3.5' iniconfig==2.0.0; python_version >= '3.7' itsdangerous==2.1.2; python_version >= '3.7' jinja2==3.1.2; python_version >= '3.7' -mako==1.2.4; python_version >= '3.7' +mako==1.3.0; python_version >= '3.8' markupsafe==2.1.3; python_version >= '3.7' marshmallow==3.20.1; python_version >= '3.8' marshmallow-sqlalchemy==0.29.0 @@ -30,12 +30,11 @@ packaging==23.2; python_version >= '3.7' pluggy==1.3.0; python_version >= '3.8' psycopg2-binary==2.9.9 pyjwt==2.8.0 -pymupdf==1.23.5 -pymupdfb==1.23.5; python_version >= '3.8' +pymupdf==1.23.6 pytest==7.4.3 python-dotenv==1.0.0 requests==2.31.0 -sqlalchemy==2.0.22; python_version >= '3.7' +sqlalchemy==2.0.23; python_version >= '3.7' sqlalchemy-utils==0.41.1 typing-extensions==4.8.0; python_version >= '3.8' urllib3==2.0.7; python_version >= '3.7' diff --git a/backend/src/establishment/overview/routes.py b/backend/src/establishment/overview/routes.py index e468775..f76379a 100644 --- a/backend/src/establishment/overview/routes.py +++ b/backend/src/establishment/overview/routes.py @@ -2,15 +2,15 @@ from flask import abort, request from flask.json import jsonify from flask_login import current_user, login_required from . import bp +from .utils import get_report, group_results, sum_entries from src import LOGGER -from models import Establishment -from src.utils import view_utils, database_utils +from models import Establishment, LoginToken from src.utils.routes_utils import render_custom_template as render_template @bp.route('/', methods=['GET']) @login_required def get_report_from_user(establishment_id): - Establishment.query.filter_by(id=int(establishment_id)).first_or_404() + establishment = Establishment.query.filter_by(id=int(establishment_id)).first_or_404() if current_user.is_anonymous: abort(403) if 'month' in request.args: @@ -23,14 +23,19 @@ def get_report_from_user(establishment_id): if (month > 12 or month < 1): abort(400) LOGGER.info("Getting results.") - results = database_utils.get_report(**request.args, **{"establishment": establishment_id}) + results = get_report(**request.args, **{"establishment": establishment_id}) LOGGER.debug(f"Results received.") LOGGER.debug(str(results)) if results: - result_list = view_utils.group_results(results) + result_list = group_results(results) + tokens = establishment.LoginToken.all() + token_dates = [] + for token in tokens: + token_dates.extend(token.LoginTokenDates.all()) + sum_entries(result_list, token_dates) else: result_list = [] - LOGGER.debug(result_list) + # LOGGER.debug(result_list) if request.content_type == "application/json": return jsonify(result_list) else: diff --git a/backend/src/establishment/overview/utils.py b/backend/src/establishment/overview/utils.py new file mode 100644 index 0000000..bdcd26d --- /dev/null +++ b/backend/src/establishment/overview/utils.py @@ -0,0 +1,158 @@ +from src import LOGGER +from datetime import date as dtdate, timedelta +from flask_login import current_user +from models import Establishment, Item, LoginToken, User, Receipt, ReceiptItem +from src import db, LOGGER +from src.utils.view_utils import bought_with_prices as bwp + +def group_results(results: tuple) -> list: + """Grouping results as following: + [ + { + id: , + email: , + sum: , + membership_dates: [ + (datetime.date(entry_date), datetime.date(exit_date)) + ] + item_infos: [ + { + date: datetime.date(items date), + item_list: [ + { + id: , + name: , + amount: , + price: + } + ] + } + ] + } + ] + """ + result_list = [] + LOGGER.debug("Grouping...") + for result in results: + try: + result_user_index = [result[1] == result_item['email'] for result_item in result_list].index(True) + except ValueError as e: + result_list.append({"id": result[0], "email": result[1], "sum": 0, "item_infos": []}) + result_user_index = -1 + result_user = result_list[result_user_index] + try: + result_date_index = [result[2] == result_list_date['date'] for result_list_date in result_user["item_infos"]].index(True) + except ValueError as e: + result_user["item_infos"].append({'date': result[2], 'item_list': []}) + result_date_index = -1 + result_date = result_user['item_infos'][result_date_index] + result_date['item_list'].append({'id': result[3], 'name': result[4], 'amount': result[5], 'price': result[6]}) + # for result_user in result_list: + # if result_user.get('id'): + # for result_date in result_user['item_infos']: + # for result_item in result_date['item_list']: + # result_user['sum'] += result_item['amount'] * result_item['price'] + LOGGER.debug("Grouped.") + return result_list + +def sum_entries(grouped_result_list, login_token_dates): + dict_people_modifier = {} + # dict_people_modifier: + # {datetime.date(x,y,z): { + # 'add': , + # 'remove: + # } + # } + for tokendate in login_token_dates: + dict_people_modifier[tokendate.activation_date] = dict_people_modifier.get(tokendate.activation_date, {"add":[]}) + dict_people_modifier[tokendate.activation_date]["add"].append(tokendate.token) + if tokendate.deactivation_date != None: + dict_people_modifier[tokendate.deactivation_date] = dict_people_modifier.get(tokendate.deactivation_date, {"remove":[]}) + dict_people_modifier[tokendate.deactivation_date]["remove"].append(tokendate.token) + list_people_amount_per_date = [{"date": key, "people": value} for key, value in dict_people_modifier.items()] + list_people_amount_per_date.sort(key=lambda x: x.get('date')) + list_people_per_date = [] + for i in range(len(list_people_amount_per_date)): + list_people_per_date.append({'date': list_people_amount_per_date[i].get('date'), 'sum': 0}) + if i == 0: + list_people_per_date[-1]['people'] = list_people_amount_per_date[i].get('people').get('add').copy() + else: + list_people_per_date[-1]['people'] = list_people_per_date[-2].get('people').copy() + if 'add' in list_people_amount_per_date[i].get('people'): + list_people_per_date[-1]['people'].extend(list_people_amount_per_date[i].get('people').get('add')) + if 'remove' in list_people_amount_per_date[i].get('people'): + for person in list_people_amount_per_date[i].get('people').get('remove'): + try: + list_people_per_date[-1]['people'].remove(person) + except ValueError as e: + LOGGER.debug(f'{person} not in list.') + for result_user in grouped_result_list: + relevant_date_index = 0 + if result_user.get('id'): + for result_date in result_user['item_infos']: + # TODO get relevant date index + for i in range(relevant_date_index + 1, len(list_people_per_date)): + + if list_people_per_date[i].get('date') > result_date.get('date'): + LOGGER.debug(f"{list_people_per_date[i].get('date')} > {result_date.get('date')}") + relevant_date_index = i-1 + break + if i == len(list_people_per_date)-1: + if list_people_per_date[i].get('date') < result_date.get('date'): + relevant_date_index=i + # LOGGER.debug(f"Relevant Date: {list_people_per_date[relevant_date_index].get('date')}, Index: {relevant_date_index}") + # LOGGER.debug(f"Result Date: {result_date.get('date')}") + for result_item in result_date['item_list']: + result_user['sum'] += result_item['amount'] * result_item['price'] + list_people_per_date[relevant_date_index]['sum'] += result_item['amount'] * result_item['price'] + LOGGER.debug(list_people_per_date) + for entry_people_per_date in list_people_per_date: + for result_user in grouped_result_list: + if result_user.get('id') in entry_people_per_date.get('people'): + LOGGER.debug(f"Reducing sum of {result_user.get('id')} by {entry_people_per_date.get('sum')/len(entry_people_per_date.get('people'))}") + result_user['sum'] -= entry_people_per_date.get('sum')/len(entry_people_per_date.get('people')) + +def get_report(**kwargs): + query_select_boughts = db.session.query( + bwp.c.token, User.email, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price) + query_select_boughts = query_select_boughts.select_from(User).join(LoginToken, LoginToken.user == User.id).join( + bwp, LoginToken.token == bwp.c.token, isouter = True).join(Item, Item.id == bwp.c.item, isouter = True) + query_select_receipts = db.session.query( + Receipt.from_user, User.email, Receipt.date, ReceiptItem.item, ReceiptItem.name, ReceiptItem.amount, -ReceiptItem.price) + query_select_receipts = query_select_receipts.select_from(User).join(LoginToken, LoginToken.user == User.id).join( + Receipt, LoginToken.token == Receipt.from_user).join(ReceiptItem, ReceiptItem.receipt == Receipt.id) + match kwargs: + case {"token": token}: + LOGGER.debug("Token present") + query_select_boughts = query_select_boughts.filter_by(token = token) + query_select_receipts = query_select_receipts.filter_by(token = token) + case {"establishment": establishment}: + LOGGER.debug("Establishment present") + query_select_boughts = query_select_boughts.filter(LoginToken.establishment == establishment) + query_select_receipts = query_select_receipts.filter(LoginToken.establishment == establishment) + if current_user.id != Establishment.query.get(int(establishment)).owner: + query_select_boughts = query_select_boughts.filter(User.id == current_user.id) + query_select_receipts = query_select_receipts.filter(User.id == current_user.id) + # if current_user.id == Establishment.query.get(int(establishment)).owner: + # _filter = db.session.query(LoginToken.token).filter_by( + # establishment=int(establishment)) + # else: + # _filter = db.session.query(LoginToken.token).filter_by( + # establishment=int(establishment), user=current_user.id) + # query_select = query_select.filter(bwp.c.token.in_(_filter)) + # LOGGER.debug(str(query_select)) + match kwargs: + case {"month": month}: + LOGGER.debug("Month present") + year = kwargs["year"] if "year" in kwargs else dtdate.today().year + query_select_boughts = query_select_boughts.filter(bwp.c.date.between(dtdate(int(year), int( + month), 1), dtdate(int(year), int(month)+1, 1)-timedelta(days=1))) + case {"year": year}: + LOGGER.debug("Year present") + query_select_boughts = query_select_boughts.filter(bwp.c.date.between( + dtdate(int(year), 1, 1), dtdate(int(year), 12, 31))) + query_select = query_select_boughts.union(query_select_receipts) + query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item) + LOGGER.debug(str(query_select)) + results = query_select.all() + return tuple(results) \ No newline at end of file diff --git a/backend/src/receipts/check_items/forms.py b/backend/src/receipts/check_items/forms.py index 9a58f03..b2ea846 100644 --- a/backend/src/receipts/check_items/forms.py +++ b/backend/src/receipts/check_items/forms.py @@ -1,8 +1,8 @@ from collections import namedtuple from flask_wtf import FlaskForm from src.utils.models.query_factories import all_brands, all_items -from wtforms import BooleanField, HiddenField, FieldList, Form, FormField, IntegerField, RadioField, StringField, SubmitField -from wtforms.validators import DataRequired, ValidationError +from wtforms import BooleanField, DecimalField, HiddenField, FieldList, Form, FormField, IntegerField, RadioField, StringField, SubmitField +from wtforms.validators import DataRequired, Optional, ValidationError from wtforms_sqlalchemy.fields import QuerySelectField @@ -21,11 +21,12 @@ def get_choices(): class CheckItemsEntryForm(Form): itemname = HiddenField('itemname', validators=[DataRequired()]) price = HiddenField('price', validators=[DataRequired()]) + amount = HiddenField('amount', validators=[DataRequired()]) requesting = BooleanField("", default=False, - render_kw={"class": "form-check-input"}) + render_kw={"class": "form-check-input form-requesting"}) new_or_existing = RadioField("", choices=get_choices(), - render_kw={"class": "form-check form-check-inline form-check-input"}, + render_kw={"class": "form-check form-check-inline form-check-input form-new_or_existing"}, coerce=int, default=3) # Fields for new Item new_ean = IntegerField("EAN ID", render_kw={ @@ -51,19 +52,25 @@ class CheckItemsEntryForm(Form): "Please choose if it's a new or existing Item.") +class CheckCustomItemsEntryForm(Form): + itemname = StringField('Item name', render_kw={"class": "form-control"}) + price = DecimalField('Price (€)', render_kw={"class": "form-control"}) + amount = IntegerField('Amount', render_kw={"class": "form-control"}, validators=[Optional()]) + class CheckItemsForm(FlaskForm): items = FieldList(FormField(CheckItemsEntryForm)) + custom_items = FieldList(FormField(CheckCustomItemsEntryForm)) submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) @classmethod def new(cls, itemarray): CheckItemsEntry = namedtuple( - "CheckItemsEntry", ["itemname", "price", "new_brand"]) + "CheckItemsEntry", ["itemname", "price", "amount", "new_brand"]) CheckItems = namedtuple("CheckItems", ["items"]) check_items_entry = [] for item in itemarray: check_items_entry.append(CheckItemsEntry( - item['itemname'], item['price'], 0)) + item['itemname'], item['price'], item['amount'] if 'amount' in item else 1, 0)) check_items = CheckItems(check_items_entry) form = cls(obj=check_items) diff --git a/backend/src/receipts/check_items/routes.py b/backend/src/receipts/check_items/routes.py index 99e2a17..965bf95 100644 --- a/backend/src/receipts/check_items/routes.py +++ b/backend/src/receipts/check_items/routes.py @@ -2,11 +2,11 @@ from datetime import date from flask import abort, request, url_for from flask_login import current_user, login_required from . import bp -from .forms import CheckItemsEntryForm, CheckItemsForm, get_choices +from .forms import CheckCustomItemsEntryForm, CheckItemsEntryForm, CheckItemsForm, get_choices from .utils import insert_existing_item, insert_new_item, insert_item_to_receipt, clear_receipt_items from src import db, LOGGER from models import AmountChange, Item, LoginToken, PriceChange, Receipt, ReceiptItem -from src.utils.pdf_receipt_parser import PDFReceipt +from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt from src.utils.routes_utils import render_custom_template as render_template PDFDir = "./" @@ -20,6 +20,7 @@ def confirm_receipt_items(receipt_id: int): if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner: receipt: PDFReceipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf") form: CheckItemsForm = CheckItemsForm.new(receipt.items) + _template = CheckCustomItemsEntryForm(prefix="custom_items-_-") # TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple # times and provide dropdown menu if necessary. If not, provide input field. # temp_choices = [] @@ -58,7 +59,15 @@ def confirm_receipt_items(receipt_id: int): else: LOGGER.debug("Cold catched") abort(400) - LOGGER.debug("At this point form.items.data will be returned") - # return form.items.data - return render_template("receipts/check_items.html", form=form) + for itempos, formcustomitem in enumerate(form.custom_items): + formcustomitemdata = formcustomitem.data + custom_item_dict = {} + custom_item_dict['amount'] = formcustomitemdata.get('amount', 1) + custom_item_dict['price'] = formcustomitemdata.get('price') + custom_item_dict['itemname'] = formcustomitemdata.get('itemname') + LOGGER.debug(formcustomitem) + LOGGER.debug(custom_item_dict) + insert_item_to_receipt(receipt=receipt_details, item_dict=custom_item_dict, item_index=len(form.items)+itempos) + LOGGER.debug("Iterating through form custom items") + return render_template("receipts/check_items.html", form=form, _template=_template) abort(403) \ No newline at end of file diff --git a/backend/src/receipts/check_items/utils.py b/backend/src/receipts/check_items/utils.py index 4a05834..f9ecb6b 100644 --- a/backend/src/receipts/check_items/utils.py +++ b/backend/src/receipts/check_items/utils.py @@ -35,7 +35,7 @@ def insert_existing_item(formitemdict: dict[str: str], receipt_date: date = None db.session.commit() def insert_item_to_receipt(receipt: Receipt, item_dict: dict[str: str], item_index:int=0): - receipt.ReceiptItem.append(ReceiptItem(item=item_index, amount=item_dict.get('amount'), price=int(item_dict.get('price').replace(',','')))) + receipt.ReceiptItem.append(ReceiptItem(item=item_index, name=item_dict.get('itemname'), amount=item_dict.get('amount'), price=int(str(item_dict.get('price')).replace(',','').replace('.', '')))) db.session.add(receipt) db.session.commit() diff --git a/backend/src/receipts/upload/routes.py b/backend/src/receipts/upload/routes.py index 827e7b1..a65a02e 100644 --- a/backend/src/receipts/upload/routes.py +++ b/backend/src/receipts/upload/routes.py @@ -7,7 +7,7 @@ from .forms import UploadReceiptForm from src import db, LOGGER from models.receipt import Receipt from models.login_token import LoginToken -from src.utils.pdf_receipt_parser import PDFReceipt +from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt from src.utils.routes_utils import render_custom_template as render_template PDFDir = "./" diff --git a/backend/src/utils/database_utils.py b/backend/src/utils/database_utils.py index 7736a73..3295b67 100644 --- a/backend/src/utils/database_utils.py +++ b/backend/src/utils/database_utils.py @@ -34,70 +34,6 @@ def insert_bought_items(token: str, dates: list[dict[str: any]]): del (dates[date_index]) return {'token': token, 'dates': dates} if dates else {} - -def get_report(**kwargs): - query_select = db.session.query( - bwp.c.token, User.email, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price) - query_select = query_select.select_from(User).join(LoginToken, LoginToken.user == User.id).join( - bwp, LoginToken.token == bwp.c.token, isouter = True).join(Item, Item.id == bwp.c.item, isouter = True) - match kwargs: - case {"token": token}: - LOGGER.debug("Token present") - query_select = query_select.filter_by(token = token) - case {"establishment": establishment}: - LOGGER.debug("Establishment present") - query_select = query_select.filter(LoginToken.establishment == establishment) - if current_user.id != Establishment.query.get(int(establishment)).owner: - query_select = query_select.filter(User.id == current_user.id) - # if current_user.id == Establishment.query.get(int(establishment)).owner: - # _filter = db.session.query(LoginToken.token).filter_by( - # establishment=int(establishment)) - # else: - # _filter = db.session.query(LoginToken.token).filter_by( - # establishment=int(establishment), user=current_user.id) - # query_select = query_select.filter(bwp.c.token.in_(_filter)) - # LOGGER.debug(str(query_select)) - match kwargs: - case {"month": month}: - LOGGER.debug("Month present") - year = kwargs["year"] if "year" in kwargs else dtdate.today().year - query_select = query_select.filter(bwp.c.date.between(dtdate(int(year), int( - month), 1), dtdate(int(year), int(month)+1, 1)-timedelta(days=1))) - case {"year": year}: - LOGGER.debug("Year present") - query_select = query_select.filter(bwp.c.date.between( - dtdate(int(year), 1, 1), dtdate(int(year), 12, 31))) - query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item) - LOGGER.debug(str(query_select)) - results = query_select.all() - return tuple(results) - - -def get_unregistered_and_register(intEstablishment: int): - LOGGER.debug("Getting unregistered") - establishment = Establishment.query.get(intEstablishment) - if current_user.id != establishment.owner: - LOGGER.debug("!!!Wrong User!!!") - return False - query_select = db.session.query( - bwp.c.token, User.email, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price) - query_select = query_select.select_from(bwp).join( - LoginToken, LoginToken.token == bwp.c.token).join(User, LoginToken.user == User.id) - query_select = query_select.join(Item, Item.id == bwp.c.item).join(Bought, and_( - Bought.token == bwp.c.token, Bought.item == bwp.c.item, Bought.date == bwp.c.date)) - query_select = query_select.filter(bwp.c.token.in_(db.session.query( - LoginToken.token).filter_by(establishment=intEstablishment))) - query_select = query_select.filter(Bought.registered == False) - query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item) - results = query_select.all() - unregistered_boughts = establishment.Bought.filter_by( - registered=False).all() - for x in unregistered_boughts: - x.registered = True - db.session.commit() - return results - - def generate_token(length=15, allowed_chars=ascii_letters + digits): new_token = "".join((rndchoice(allowed_chars) for i in range(length))) if not LoginToken.query.filter_by(token=new_token).first(): diff --git a/backend/src/utils/modules/receipt_parser/pdf_receipt_parser.py b/backend/src/utils/modules/receipt_parser/pdf_receipt_parser.py index 319618b..4541a06 100644 --- a/backend/src/utils/modules/receipt_parser/pdf_receipt_parser.py +++ b/backend/src/utils/modules/receipt_parser/pdf_receipt_parser.py @@ -1,5 +1,5 @@ import fitz -from datetime import datetime +from datetime import datetime, date from re import search class PDFReceipt: @@ -11,8 +11,14 @@ class PDFReceipt: Currently supported: 'edeka' """ def __init__(self, bPDFFile, parser: str = "edeka") -> None: - self.text = PDFReceipt._getTextFromPDF(bPDFFile) - self.id, self.date, self.items = PDFReceipt._getInfosFromText(self.text, parser) + try: + self.text = PDFReceipt._getTextFromPDF(bPDFFile) + self.id, self.date, self.items = PDFReceipt._getInfosFromText(self.text, parser) + except: + self.text = "PDF konnte nicht geladen werden." + self.date = date.today() + self.id = None + self.items = [] def _getTextFromPDF(file): with fitz.open(file, filetype="pdf") as doc: diff --git a/backend/src/utils/view_utils.py b/backend/src/utils/view_utils.py index f225402..8570213 100644 --- a/backend/src/utils/view_utils.py +++ b/backend/src/utils/view_utils.py @@ -4,31 +4,6 @@ from models.amount_change import AmountChange from models.bought import Bought from models.price_change import PriceChange -def group_results(results: tuple) -> list: - result_list = [] - LOGGER.debug("Grouping...") - for result in results: - try: - result_user_index = [result[1] == result_item['email'] for result_item in result_list].index(True) - except ValueError as e: - result_list.append({"id": result[0], "email": result[1], "sum": 0, "item_infos": []}) - result_user_index = -1 - result_user = result_list[result_user_index] - try: - result_date_index = [result[2] == result_list_date['date'] for result_list_date in result_user["item_infos"]].index(True) - except ValueError as e: - result_user["item_infos"].append({'date': result[2], 'item_list': []}) - result_date_index = -1 - result_date = result_user['item_infos'][result_date_index] - result_date['item_list'].append({'id': result[3], 'name': result[4], 'amount': result[5], 'price': result[6]}) - for result_user in result_list: - if result_user.get('id'): - for result_date in result_user['item_infos']: - for result_item in result_date['item_list']: - result_user['sum'] += result_item['amount'] * result_item['price'] - LOGGER.debug("Grouped.") - return result_list - def selectable_price_per_amount_view(): p = db.aliased(PriceChange, name="p") a = db.aliased(AmountChange, name="a") diff --git a/backend/web/static/receipts/check_items.js b/backend/web/static/receipts/check_items.js new file mode 100644 index 0000000..71a2cfc --- /dev/null +++ b/backend/web/static/receipts/check_items.js @@ -0,0 +1,154 @@ +function requesting_checkbox_pressed() { + var $radiobuttongroup = $(this).parent().next().next().next() + var $itemid = $(this).parent().parent().attr('id') + if ($(this).prop("checked")) { + $radiobuttongroup.show() + var $radiobuttons = $radiobuttongroup.children() + $radiobuttons.each(function (i) { + var $radiobutton = $(this).children().get(0) + if ($radiobutton.checked) { + switch (parseInt($radiobutton.value)) { + case 1: + $("#" + $itemid + "_new").show(); + break; + case 2: + $("#" + $itemid + "_existing").show(); + break; + } + } + }) + } else { + $radiobuttongroup.hide() + $("#" + $itemid + "_new").hide(); + $("#" + $itemid + "_existing").hide(); + } +} + +function new_or_existing_radiobutton_pressed() { + var $itemid = $(this).parent().parent().parent().attr('id') + switch (parseInt($(this).prop("value"))) { + case 1: + $("#" + $itemid + "_new").show(); + $("#" + $itemid + "_existing").hide(); + break; + case 2: + $("#" + $itemid + "_new").hide(); + $("#" + $itemid + "_existing").show(); + break; + case 3: + $("#" + $itemid + "_new").hide(); + $("#" + $itemid + "_existing").hide(); + break; + } +} + +// credit from here on goes to rmed +// https://www.rmedgar.com/blog/dynamic-fields-flask-wtf/ + +function replaceTemplateIndex(value, index) { + const ID_RE = /(custom_items-)_/ + return value.replace(ID_RE, '$1'+index); +} + +function adjustIndices(removedIndex) { + var $forms = $('.form-custom_item'); + + $forms.each(function(i) { + var $form = $(this); + var index = parseInt($form.data('index')); + var newIndex = index - 1; + + if (index < removedIndex) { + // Skip + return true; + } + + // This will replace the original index with the new one + // only if it is found in the format -num-, preventing + // accidental replacing of fields that may have numbers + // intheir names. + var regex = new RegExp('(custom_items-)'+index); + var repVal = '$1'+newIndex; + + // Change ID in form itself + $form.attr('id', $form.attr('id').replace(index, newIndex)); + $form.data('index', newIndex); + + // Change IDs in form fields + $form.find('label, input, select, textarea').each(function(j) { + var $item = $(this); + + if ($item.is('label')) { + // Update labels + $item.attr('for', $item.attr('for').replace(regex, repVal)); + return; + } + + // Update other fields + $item.attr('id', $item.attr('id').replace(regex, repVal)); + $item.attr('name', $item.attr('name').replace(regex, repVal)); + }); + }); +} + +function remove_custom_entry() { + var $removed_entry = $(this).closest('.form-custom_item'); + var removedIndex = parseInt($removed_entry.data('index')); + + $removed_entry.remove(); + + // Update indices + adjustIndices(removedIndex); +} + +function add_custom_entry() { + var $template_form = $("#custom_items-_") + var $last_entry = $('.form-custom_item').last(); + console.log($last_entry); + + var new_index = 0; + + if ($last_entry.length > 0) { + new_index = parseInt($last_entry.data('index')) + 1; + } + + var $new_form = $template_form.clone() + + console.log("New Index: " + new_index) + + $new_form.attr('id', replaceTemplateIndex($new_form.attr('id'), new_index)); + $new_form.data('index', new_index) + + console.log("NewFormData Index: " + $new_form.data('index')) + console.log("NewFormData ID: " + $new_form.data('id')) + + $new_form.find('label, input, select, textarea').each(function(idx) { + var $item = $(this); + + if ($item.is('label')) { + // Update labels + $item.attr('for', replaceTemplateIndex($item.attr('for'), new_index)); + return; + } + + // Update other fields + $item.attr('id', replaceTemplateIndex($item.attr('id'), new_index)); + $item.attr('name', replaceTemplateIndex($item.attr('name'), new_index)); + }); + + $("#custom_items-container").append($new_form); + + $new_form.addClass('form-custom_item'); + $new_form.removeClass('custom_items-template'); + + $new_form.find('.remove-custom_item').click(remove_custom_entry); + + $new_form.show(); +} + +$(document).ready(function () { + $(".form-requesting").click(requesting_checkbox_pressed); + $(".form-new_or_existing").click(new_or_existing_radiobutton_pressed); + $(".remove-custom_item").click(remove_custom_entry); + $("#append").click(add_custom_entry); +}); \ No newline at end of file diff --git a/backend/web/templates/establishment/overview/overview.html b/backend/web/templates/establishment/overview/overview.html index 2b8bb84..ff59069 100644 --- a/backend/web/templates/establishment/overview/overview.html +++ b/backend/web/templates/establishment/overview/overview.html @@ -13,7 +13,7 @@
{% if user.id %} diff --git a/backend/web/templates/receipts/check_items.html b/backend/web/templates/receipts/check_items.html index 96bc8cf..a2dcdc5 100644 --- a/backend/web/templates/receipts/check_items.html +++ b/backend/web/templates/receipts/check_items.html @@ -5,70 +5,55 @@ {% block app_content %}
{{ form.hidden_tag() }} - {% for item in form.items %} -

{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})

- {{ render_field(item.itemname) }} - {{ render_field(item.price) }} -