major: add dynamic receipt item forms

New:
- Added the possibility to insert custom items on receipt upload in case
the receipt couldn't be read.
Changes:
- Changed the display of the sum at the overview. You now see what you
owe your establishment instead of just seeing how much you bought.
- In addition the sum in the overview now accounts for membership time.
You don't have to pay for something bought after you moved out for
example.
This commit is contained in:
Lunaresk 2023-11-26 23:16:05 +01:00
parent 4d1e7eb944
commit 721db16250
24 changed files with 595 additions and 274 deletions

196
backend/Pipfile.lock generated
View File

@ -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": [

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 *

View File

@ -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')

View File

@ -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"<LoginTokenDates {self.token, self.activation_date, self.deactivation_date}>"

View File

@ -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)

View File

@ -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()

View File

@ -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'

View File

@ -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('/<int:establishment_id>', 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:

View File

@ -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: <usertoken>,
email: <usermail>,
sum: <sum of itemamounts*itemprices>,
membership_dates: [
(datetime.date(entry_date), datetime.date(exit_date))
]
item_infos: [
{
date: datetime.date(items date),
item_list: [
{
id: <itemid>,
name: <itemname>,
amount: <itemamount>,
price: <itemprice (ct)>
}
]
}
]
}
]
"""
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': <list of usertokens>,
# 'remove: <list of usertokens>
# }
# }
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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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 = "./"

View File

@ -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():

View File

@ -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:
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:

View File

@ -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")

View File

@ -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);
});

View File

@ -13,7 +13,7 @@
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
<h3>{{ user.email }}: {{ (user.sum | int)/100 }} €</h3>
</div>
</button>
{% if user.id %}

View File

@ -5,70 +5,55 @@
{% block app_content %}
<form action="" method="post" novalidate enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div id="items-container">
{% for item in form.items %}
<div id="{{ item.id }}" class="form-item" data-index="{{ loop.index-1 }}">
<h4>{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})</h4>
{{ render_field(item.itemname) }}
{{ render_field(item.price) }}
<div class="{{ item.new_or_existing.id }}" style="display: none;">
<div id="{{ item.new_or_existing.id }}" class="new_or_existing-subform" style="display: none;">
{{ render_field(item.new_or_existing) }}
</div>
<div class="{{ item.id }}_new" style="display: none;">
<div id="{{ item.id }}_new" class="new-subform" style="display: none;">
{{ render_field(item.new_ean) }}
{{ render_field(item.new_description) }}
{{ render_field(item.new_amount_change) }}
{{ render_field(item.new_brand) }}
</div>
<div class="{{ item.id }}_existing" style="display: none;">
<div id="{{ item.id }}_existing" class="existing-subform" style="display: none;">
{{ render_field(item.existing_item) }}
</div>
</div>
<br>
{% endfor %}
</div>
<div id="custom_items-container">
{% for item in form.custom_items %}
<div id="{{ item.id }}" class="form-custom_item" data-index="{{ loop.index-1 }}">
{{ render_field(item.itemname) }}
{{ render_field(item.price) }}
{{ render_field(item.amount) }}
</div>
<br>
{% endfor %}
</div>
<button type="button" id="append" class="btn btn-success">Append</button>
<br>
{{ form.submit() }}
</form>
<div id="custom_items-_" class="custom_items-template" data-index="_"
style="display: none">
{{ render_field(_template.itemname) }}
{{ render_field(_template.price) }}
{{ render_field(_template.amount) }}
<br>
<button type="button" class="btn btn-danger remove-custom_item">Remove</button>
<br>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{% for item in form.items %}
<script>
$(document).ready(function () {
$("#{{ item.requesting.id }}").change(function () {
if ($(this).prop("checked")) {
$(".{{ item.new_or_existing.id }}").show();
if ($("#{{ item.new_or_existing.id }}-1").prop("checked")) {
$(".{{ item.id }}_new").show();
}
if ($("#{{ item.new_or_existing.id }}-2").prop("checked")) {
$(".{{ item.id }}_existing").show();
}
}
else {
$(".{{ item.new_or_existing.id }}").hide();
$(".{{ item.id }}_new").hide();
$(".{{ item.id }}_existing").hide();
}
});
$("#{{ item.new_or_existing.id }}-1").change(function () {
if ($(this).prop("checked")) {
$(".{{ item.id }}_new").show();
$(".{{ item.id }}_existing").hide();
}
});
$("#{{ item.new_or_existing.id }}-2").change(function () {
if ($(this).prop("checked")) {
$(".{{ item.id }}_new").hide();
$(".{{ item.id }}_existing").show();
}
});
$("#{{ item.new_or_existing.id }}-3").change(function () {
if ($(this).prop("checked")) {
$(".{{ item.id }}_new").hide();
$(".{{ item.id }}_existing").hide();
}
});
});
</script>
{% endfor %}
{{ super() }}
<script src="{{ url_for('static', filename='receipts/check_items.js') }}"></script>
{% endblock %}

Binary file not shown.

Binary file not shown.