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:
parent
4d1e7eb944
commit
721db16250
196
backend/Pipfile.lock
generated
196
backend/Pipfile.lock
generated
@ -27,11 +27,11 @@
|
|||||||
},
|
},
|
||||||
"blinker": {
|
"blinker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d",
|
"sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
|
||||||
"sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa"
|
"sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.6.3"
|
"version": "==1.7.0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -150,7 +150,7 @@
|
|||||||
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
||||||
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
||||||
],
|
],
|
||||||
"markers": "platform_system == 'Windows'",
|
"markers": "sys_platform == 'win32'",
|
||||||
"version": "==0.4.6"
|
"version": "==0.4.6"
|
||||||
},
|
},
|
||||||
"dnspython": {
|
"dnspython": {
|
||||||
@ -344,11 +344,11 @@
|
|||||||
},
|
},
|
||||||
"mako": {
|
"mako": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
|
"sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9",
|
||||||
"sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"
|
"sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.2.4"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -536,52 +536,52 @@
|
|||||||
},
|
},
|
||||||
"pymupdf": {
|
"pymupdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:264d5f6478d787c336520cf1a99e39bb6a0ef6d984550f925095c0e692dea7b5",
|
"sha256:0f852d125defc26716878b1796f4d68870e9065041d00cf46bde317fd8d30e68",
|
||||||
"sha256:29e1d82b16f7580280ae35a0ae78de55f15c92ec87b7f3a1372f40f37a053bf3",
|
"sha256:1103eea4ab727e32b9cb93347b35f71562033018c333a7f3a17d115e980fea4a",
|
||||||
"sha256:31405311c28fc8b3b2975a98b60bac388563748beaacb6da470f917678417e2d",
|
"sha256:19d1711d5908c4527ad2deef5af2d066649f3f9a12950faf30be5f7251d18abc",
|
||||||
"sha256:332c1d5633c233458c4b65e6ad4a860391c507384bd2324a186b2702f8c64dfe",
|
"sha256:1cbcf05c06f314fdf3042ceee674e9a0ac7fae598347d5442e2138c6046d4e82",
|
||||||
"sha256:3400b582be3d71f1c0974701fcfda32f0c2ebb75a78c2aea430552b0c6896546",
|
"sha256:224c341fe254adda97c8f06a4c5838cdbcf609fa89e70b1fb179752533378f2f",
|
||||||
"sha256:3a01c93c69e74068c1618631a750677fd088708d2b09b3c23809b099fa4ffa39",
|
"sha256:271bdf6059bb8347f9c9c6b721329bd353a933681b1fc62f43241b410e7ab7ae",
|
||||||
"sha256:3f5fc705e8790217d23ab5e7ac2c05d82e050f6271b710300288adfe87a71072",
|
"sha256:2885a26220a32fb45ea443443b72194bb7107d6862d8d546b59e4ad0c8a1f2c9",
|
||||||
"sha256:435a108cf8b53302500b52adb2cccbf2afa51c94ab3c705b250245090b46f5da",
|
"sha256:2c141f33e2733e48de8524dfd2de56d889feef0c7773b20a8cd216c03ab24793",
|
||||||
"sha256:4508ee04c46cac8356a9d04f0d9a63f845770d2abb54caf512b44d22f0e80300",
|
"sha256:2e27857a15c8a810d0b66455b8c8a79013640b6267a9b4ea808a5fe1f47711f2",
|
||||||
"sha256:460b47a1a17335d444ec441b68c083da5e51cdfcfa67a6638de69fe5e97f4ad2",
|
"sha256:361cab1be45481bd3dc4e00ec82628ebc189b4f4b6fd9bd78a00cfeed54e0034",
|
||||||
"sha256:4a53b2bf19be687160e4d18c27680e5326687aa39a7e31641d32a61edadbbfd9",
|
"sha256:3ce2d3678dbf822cff213b1902f2e59756313e543efd516a2b4f15bb0353bd6c",
|
||||||
"sha256:4b1bd9a91dee18bc95d7af2c593a214857a03e4fcd9a1eb01588df432de24c58",
|
"sha256:3f0f9b76bc4f039e7587003cbd40684d93a98441549dd033cab38ca07d61988d",
|
||||||
"sha256:53278c6a3d0a5dc8f221e0a77c065a61fd0598f9d8d9ef5be53de0c0a7d2df90",
|
"sha256:4ac9673a6d6ee7e80cb242dacb43f9ca097b502d9c5e44687dbdffc2bce7961a",
|
||||||
"sha256:59755a600c25a282589b548ffa045aed59c2df7b76943978cabb1825f0c03ec4",
|
"sha256:4d06751d5cd213e96f84f2faaa71a51cf4d641851e07579247ca1190121f173b",
|
||||||
"sha256:64ab1097e3a077ae9db6a98d01e2e77087894ebd85b702edf5eb85d05ab8c0f1",
|
"sha256:526b26a5207e923aab65877ad305644402851823a352cb92d362053426899354",
|
||||||
"sha256:693979ad4c8885729ac126b3202f1cb645f3392ad7e0964c2d924e61bc0e0a9d",
|
"sha256:57725e15872f7ab67a9fb3e06e5384d1047b2121e85755c93a6d4266d3ca8983",
|
||||||
"sha256:705a7aed0a917c35bb5efa4d94a7e8092705b3395726f9770d2b888de775f437",
|
"sha256:57e22bea69690450197b34dcde16bd9fe0265ac4425b4033535ccc5c044246fb",
|
||||||
"sha256:9586bc98a322e546cf2e477309806aa4a3e1d18efc9b93fc2e2b3d8131e1b9f7",
|
"sha256:5bdf7020b90987412381acc42427dd1b7a03d771ee9ec273de003e570164ec1a",
|
||||||
"sha256:9bde3683e254661e6b0032006f0ef7025ade2a33d3e3045499e71b76ea99942c",
|
"sha256:5cd05700c8f18c9dafef63ac2ed3b1099ca06017ca0c32deea13093cea1b8671",
|
||||||
"sha256:b234805b615b2d45dcb1bfe5c2167dc4121e31d618ab557856a3153b94c1676b",
|
"sha256:618b8e884190ac1cca9df1c637f87669d2d532d421d4ee7e4763c848dc4f3a1e",
|
||||||
"sha256:bb345ef1120db4f78ec0f229514d333ea3e7d367875c1400423a9b3e2b48ffc0",
|
"sha256:6e319c1f49476e07b9a12017c2d031687617713f8a46b7adcec03c636ed04607",
|
||||||
"sha256:c71b5e80a08272b9f3012314dc47ee2423270b30262d07ec7dd9709ae2bde1ac",
|
"sha256:761501a4965264e81acdd8f2224f993020bf24474e9b34fcdb5805a6826eda1c",
|
||||||
"sha256:d2e9cfa46193fab196c27cb07561e1bb0938450984c2f01b3739f254a31b639e",
|
"sha256:8fd9c4ee1dd4744a515b9190d8ba9133348b0d94c362293ed77726aa1c13b0a6",
|
||||||
"sha256:d3bef175707693a2f53fe0fe4e546e3187c7876aedabfe43d9a916060bac9073",
|
"sha256:951d280c1daafac2fd6a664b031f7f98b27eb2def55d39c92a19087bd8041c5d",
|
||||||
"sha256:da1b08b5348152f2940fa183d0265a6b6eb6f0292fae44b576eaf8e53723e336",
|
"sha256:991a37e1cba43775ce094da87cf0bf72172a5532a09644003276bc8bfdfe9f1a",
|
||||||
"sha256:dbce86df507f6bce118b12b33d893f1d3512013c898174211e903da78e1916aa",
|
"sha256:c4eb71b88a22c1008f764b3121b36a9d25340f9920b870508356050a365d9ca1",
|
||||||
"sha256:eafa1bce0860320ddbb7edb4ab5678e02051db5450251ba8e918713d9a70c03c",
|
"sha256:c8ea81964c1433ea163ad4b53c56053a87a9ef6e1bd7a879d4d368a3988b60d1",
|
||||||
"sha256:ee9e9ce1897eeac0fc33cf99084067c14250312a5dbc1372012c3d2f0e7a4af5",
|
"sha256:e047571d799b30459ad7ee0bc6e68900a7f6b928876f956c976f279808814e72",
|
||||||
"sha256:f7a8f91681b88ad216c36911e08ea25d2b3121350d52f4f8d76aeb0b7fcc6bef",
|
"sha256:e2d64799c6d9a3735be9e162a5d11061c0b7fbcb1e5fc7446e0993d0f815a93a",
|
||||||
"sha256:fab599d23fa490725e5b5a70bfb6bc87acf5ceb70abe11ad2ef2b2f516961f31",
|
"sha256:e33f8ec5ba7265fe78b30332840b8f454184addfa79f9c27f160f19789aa5ffd",
|
||||||
"sha256:faebdf8679706964f87617ee43b8d0107587d20b526892b538222146a4c32d43"
|
"sha256:fd8388e82b6045807d19addf310d8119d32908e89f76cc8bbf8cf1ec36fce947"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.23.5"
|
"version": "==1.23.6"
|
||||||
},
|
},
|
||||||
"pymupdfb": {
|
"pymupdfb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2b71b5b7987f2ebe9f6893544151ede2de74ec30651eef584039eb5f9c7c02aa",
|
"sha256:009e2cff166059e13bf71f93919e688f46b8fc11d122433574cfb0cc9134690e",
|
||||||
"sha256:80137c37a4b0d5abeb988434c7d7eb3f9087afdd0754f4bf2f8840a788e691ae",
|
"sha256:7132b30e6ad6ff2013344e3a481b2287fe0be3710d80694807dd6e0d8635f085",
|
||||||
"sha256:85cbc308085a4ec794e0da790965985cc5ccb21b2abc09732e072f6eaf10150b",
|
"sha256:7bef75988e6979b10ca804cf9487f817aae43b0fff1c6e315b3b9ee0cf1cc32f",
|
||||||
"sha256:d0095f28b2bcd64ed8a9636dfba193108eeb6c24d0ec71fa3f88cb15aee67a30",
|
"sha256:9925816cbe3e05e920f9be925e5752c2eef42b793885b62075bb0f6a69178598",
|
||||||
"sha256:e26705e1a4ea42926b70c5655f2509d555a4774d1d1382ecc7e76466695209e6",
|
"sha256:9d24ddadc204e895bee5000ddc7507c801643548e59f5a56aad6d32981d17eeb",
|
||||||
"sha256:f269814bafdffd5558d44af3de63eaa531d498de640a79cf6c7072011fd4088f"
|
"sha256:e5af77580aad3d1103aeec57009d156bfca429cecda14a17c573fcbe97bafb30"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.23.5"
|
"version": "==1.23.6"
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -609,58 +609,58 @@
|
|||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0b0b3f2686c3f162123adba3cb8b626ed7e9b8433ab528e36ed270b4f70d1cdb",
|
"sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3",
|
||||||
"sha256:0c1fea8c0abcb070ffe15311853abfda4e55bf7dc1d4889497b3403629f3bf00",
|
"sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884",
|
||||||
"sha256:0e1ce8ebd2e040357dde01a3fb7d30d9b5736b3e54a94002641dfd0aa12ae6ce",
|
"sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74",
|
||||||
"sha256:129415f89744b05741c6f0b04a84525f37fbabe5dc3774f7edf100e7458c48cd",
|
"sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d",
|
||||||
"sha256:13790cb42f917c45c9c850b39b9941539ca8ee7917dacf099cc0b569f3d40da7",
|
"sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc",
|
||||||
"sha256:14cd3bcbb853379fef2cd01e7c64a5d6f1d005406d877ed9509afb7a05ff40a5",
|
"sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca",
|
||||||
"sha256:154a32f3c7b00de3d090bc60ec8006a78149e221f1182e3edcf0376016be9396",
|
"sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d",
|
||||||
"sha256:19c6986cf2fb4bc8e0e846f97f4135a8e753b57d2aaaa87c50f9acbe606bd1db",
|
"sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf",
|
||||||
"sha256:2096d6b018d242a2bcc9e451618166f860bb0304f590d205173d317b69986c95",
|
"sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846",
|
||||||
"sha256:2c9bac865ee06d27a1533471405ad240a6f5d83195eca481f9fc4a71d8b87df8",
|
"sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306",
|
||||||
"sha256:3076740335e4aaadd7deb3fe6dcb96b3015f1613bd190a4e1634e1b99b02ec86",
|
"sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221",
|
||||||
"sha256:3940677d341f2b685a999bffe7078697b5848a40b5f6952794ffcf3af150c301",
|
"sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5",
|
||||||
"sha256:3aa1472bf44f61dd27987cd051f1c893b7d3b17238bff8c23fceaef4f1133868",
|
"sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89",
|
||||||
"sha256:40b1206a0d923e73aa54f0a6bd61419a96b914f1cd19900b6c8226899d9742ad",
|
"sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55",
|
||||||
"sha256:4bb062784f37b2d75fd9b074c8ec360ad5df71f933f927e9e95c50eb8e05323c",
|
"sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72",
|
||||||
"sha256:4e869a8ff7ee7a833b74868a0887e8462445ec462432d8cbeff5e85f475186da",
|
"sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea",
|
||||||
"sha256:4f6ff392b27a743c1ad346d215655503cec64405d3b694228b3454878bf21590",
|
"sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8",
|
||||||
"sha256:505f503763a767556fa4deae5194b2be056b64ecca72ac65224381a0acab7ebe",
|
"sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577",
|
||||||
"sha256:53a766cb0b468223cafdf63e2d37f14a4757476157927b09300c8c5832d88560",
|
"sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df",
|
||||||
"sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b",
|
"sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4",
|
||||||
"sha256:54bcceaf4eebef07dadfde424f5c26b491e4a64e61761dea9459103ecd6ccc95",
|
"sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d",
|
||||||
"sha256:55914d45a631b81a8a2cb1a54f03eea265cf1783241ac55396ec6d735be14883",
|
"sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34",
|
||||||
"sha256:564e9f9e4e6466273dbfab0e0a2e5fe819eec480c57b53a2cdee8e4fdae3ad5f",
|
"sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4",
|
||||||
"sha256:56a7e2bb639df9263bf6418231bc2a92a773f57886d371ddb7a869a24919face",
|
"sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24",
|
||||||
"sha256:58a3aba1bfb32ae7af68da3f277ed91d9f57620cf7ce651db96636790a78b736",
|
"sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6",
|
||||||
"sha256:625b72d77ac8ac23da3b1622e2da88c4aedaee14df47c8432bf8f6495e655de2",
|
"sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965",
|
||||||
"sha256:69fd9e41cf9368afa034e1c81f3570afb96f30fcd2eb1ef29cb4d9371c6eece2",
|
"sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35",
|
||||||
"sha256:6ac28bd6888fe3c81fbe97584eb0b96804bd7032d6100b9701255d9441373ec1",
|
"sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b",
|
||||||
"sha256:7c6c3e9350f9fb16de5b5e5fbf17b578811a52d71bb784cc5ff71acb7de2a7f9",
|
"sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab",
|
||||||
"sha256:7ee7ccf47aa503033b6afd57efbac6b9e05180f492aeed9fcf70752556f95624",
|
"sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22",
|
||||||
"sha256:875de9414393e778b655a3d97d60465eb3fae7c919e88b70cc10b40b9f56042d",
|
"sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4",
|
||||||
"sha256:8db5ba8b7da759b727faebc4289a9e6a51edadc7fc32207a30f7c6203a181592",
|
"sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204",
|
||||||
"sha256:92e512a6af769e4725fa5b25981ba790335d42c5977e94ded07db7d641490a85",
|
"sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855",
|
||||||
"sha256:9886a72c8e6371280cb247c5d32c9c8fa141dc560124348762db8a8b236f8692",
|
"sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d",
|
||||||
"sha256:9e55dff5ec115316dd7a083cdc1a52de63693695aecf72bc53a8e1468ce429e5",
|
"sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab",
|
||||||
"sha256:a42c9fa3abcda0dcfad053e49c4f752eef71ecd8c155221e18b99d4224621176",
|
"sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69",
|
||||||
"sha256:a571bc8ac092a3175a1d994794a8e7a1f2f651e7c744de24a19b4f740fe95034",
|
"sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693",
|
||||||
"sha256:af66001d7b76a3fab0d5e4c1ec9339ac45748bc4a399cbc2baa48c1980d3c1f4",
|
"sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e",
|
||||||
"sha256:b39a6e21110204a8c08d40ff56a73ba542ec60bab701c36ce721e7990df49fb9",
|
"sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8",
|
||||||
"sha256:b560f075c151900587ade06706b0c51d04b3277c111151997ea0813455378ae0",
|
"sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0",
|
||||||
"sha256:c8f1792d20d2f4e875ce7a113f43c3561ad12b34ff796b84002a256f37ce9437",
|
"sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45",
|
||||||
"sha256:cb9a758ad973e795267da334a92dd82bb7555cb36a0960dcabcf724d26299db8",
|
"sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab",
|
||||||
"sha256:ccca778c0737a773a1ad86b68bda52a71ad5950b25e120b6eb1330f0df54c3d0",
|
"sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1",
|
||||||
"sha256:ccd87c25e4c8559e1b918d46b4fa90b37f459c9b4566f1dfbce0eb8122571547",
|
"sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d",
|
||||||
"sha256:d143c5a9dada696bcfdb96ba2de4a47d5a89168e71d05a076e88a01386872f97",
|
"sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda",
|
||||||
"sha256:d80eeb5189d7d4b1af519fc3f148fe7521b9dfce8f4d6a0820e8f5769b005051",
|
"sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b",
|
||||||
"sha256:e04ab55cf49daf1aeb8c622c54d23fa4bec91cb051a43cc24351ba97e1dd09f5",
|
"sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18",
|
||||||
"sha256:f146c61ae128ab43ea3a0955de1af7e1633942c2b2b4985ac51cc292daf33222",
|
"sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac",
|
||||||
"sha256:f776c2c30f0e5f4db45c3ee11a5f2a8d9de68e81eb73ec4237de1e32e04ae81c"
|
"sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.0.22"
|
"version": "==2.0.23"
|
||||||
},
|
},
|
||||||
"sqlalchemy-utils": {
|
"sqlalchemy-utils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@ -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 ###
|
||||||
@ -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 ###
|
||||||
@ -12,5 +12,6 @@ from .category import Category
|
|||||||
from .item import Item
|
from .item import Item
|
||||||
from .brand import Brand
|
from .brand import Brand
|
||||||
from .item_category import item_category
|
from .item_category import item_category
|
||||||
|
from .login_token_dates import LoginTokenDates
|
||||||
|
|
||||||
from .schemas import *
|
from .schemas import *
|
||||||
@ -8,6 +8,7 @@ class LoginToken(db.Model):
|
|||||||
'establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
'establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||||
token = db.Column(db.String(15), nullable=False, unique=True)
|
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')
|
Payment = db.relationship("Payment", backref='LoginToken', lazy='dynamic')
|
||||||
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
|
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
|
||||||
|
|
||||||
|
|||||||
13
backend/models/login_token_dates.py
Normal file
13
backend/models/login_token_dates.py
Normal 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}>"
|
||||||
@ -5,6 +5,7 @@ class ReceiptItem(db.Model):
|
|||||||
receipt = db.Column(db.ForeignKey("receipt.id"),
|
receipt = db.Column(db.ForeignKey("receipt.id"),
|
||||||
primary_key=True, server_onupdate=db.FetchedValue())
|
primary_key=True, server_onupdate=db.FetchedValue())
|
||||||
item = db.Column(db.SmallInteger, primary_key=True, nullable=False)
|
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))
|
amount = db.Column(db.SmallInteger, nullable=False, default=str(1))
|
||||||
price = db.Column(db.SmallInteger, nullable=False)
|
price = db.Column(db.SmallInteger, nullable=False)
|
||||||
|
|
||||||
|
|||||||
@ -10,5 +10,7 @@ class ReceiptItemSchema(ma.SQLAlchemySchema):
|
|||||||
include_fk = True
|
include_fk = True
|
||||||
|
|
||||||
Receipt = ma.Nested(ReceiptSchema)
|
Receipt = ma.Nested(ReceiptSchema)
|
||||||
price = ma.auto_field()
|
item = ma.auto_field()
|
||||||
|
name = ma.auto_field()
|
||||||
amount = ma.auto_field()
|
amount = ma.auto_field()
|
||||||
|
price = ma.auto_field()
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
-i https://pypi.org/simple
|
-i https://pypi.org/simple
|
||||||
alembic==1.12.1; python_version >= '3.7'
|
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'
|
certifi==2023.7.22; python_version >= '3.6'
|
||||||
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
|
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
|
||||||
click==8.1.7; python_version >= '3.7'
|
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'
|
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'
|
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
|
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'
|
iniconfig==2.0.0; python_version >= '3.7'
|
||||||
itsdangerous==2.1.2; python_version >= '3.7'
|
itsdangerous==2.1.2; python_version >= '3.7'
|
||||||
jinja2==3.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'
|
markupsafe==2.1.3; python_version >= '3.7'
|
||||||
marshmallow==3.20.1; python_version >= '3.8'
|
marshmallow==3.20.1; python_version >= '3.8'
|
||||||
marshmallow-sqlalchemy==0.29.0
|
marshmallow-sqlalchemy==0.29.0
|
||||||
@ -30,12 +30,11 @@ packaging==23.2; python_version >= '3.7'
|
|||||||
pluggy==1.3.0; python_version >= '3.8'
|
pluggy==1.3.0; python_version >= '3.8'
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
pymupdf==1.23.5
|
pymupdf==1.23.6
|
||||||
pymupdfb==1.23.5; python_version >= '3.8'
|
|
||||||
pytest==7.4.3
|
pytest==7.4.3
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
requests==2.31.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
|
sqlalchemy-utils==0.41.1
|
||||||
typing-extensions==4.8.0; python_version >= '3.8'
|
typing-extensions==4.8.0; python_version >= '3.8'
|
||||||
urllib3==2.0.7; python_version >= '3.7'
|
urllib3==2.0.7; python_version >= '3.7'
|
||||||
|
|||||||
@ -2,15 +2,15 @@ from flask import abort, request
|
|||||||
from flask.json import jsonify
|
from flask.json import jsonify
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from . import bp
|
from . import bp
|
||||||
|
from .utils import get_report, group_results, sum_entries
|
||||||
from src import LOGGER
|
from src import LOGGER
|
||||||
from models import Establishment
|
from models import Establishment, LoginToken
|
||||||
from src.utils import view_utils, database_utils
|
|
||||||
from src.utils.routes_utils import render_custom_template as render_template
|
from src.utils.routes_utils import render_custom_template as render_template
|
||||||
|
|
||||||
@bp.route('/<int:establishment_id>', methods=['GET'])
|
@bp.route('/<int:establishment_id>', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def get_report_from_user(establishment_id):
|
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:
|
if current_user.is_anonymous:
|
||||||
abort(403)
|
abort(403)
|
||||||
if 'month' in request.args:
|
if 'month' in request.args:
|
||||||
@ -23,14 +23,19 @@ def get_report_from_user(establishment_id):
|
|||||||
if (month > 12 or month < 1):
|
if (month > 12 or month < 1):
|
||||||
abort(400)
|
abort(400)
|
||||||
LOGGER.info("Getting results.")
|
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(f"Results received.")
|
||||||
LOGGER.debug(str(results))
|
LOGGER.debug(str(results))
|
||||||
if 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:
|
else:
|
||||||
result_list = []
|
result_list = []
|
||||||
LOGGER.debug(result_list)
|
# LOGGER.debug(result_list)
|
||||||
if request.content_type == "application/json":
|
if request.content_type == "application/json":
|
||||||
return jsonify(result_list)
|
return jsonify(result_list)
|
||||||
else:
|
else:
|
||||||
|
|||||||
158
backend/src/establishment/overview/utils.py
Normal file
158
backend/src/establishment/overview/utils.py
Normal 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)
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from src.utils.models.query_factories import all_brands, all_items
|
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 import BooleanField, DecimalField, HiddenField, FieldList, Form, FormField, IntegerField, RadioField, StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
from wtforms.validators import DataRequired, Optional, ValidationError
|
||||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||||
|
|
||||||
|
|
||||||
@ -21,11 +21,12 @@ def get_choices():
|
|||||||
class CheckItemsEntryForm(Form):
|
class CheckItemsEntryForm(Form):
|
||||||
itemname = HiddenField('itemname', validators=[DataRequired()])
|
itemname = HiddenField('itemname', validators=[DataRequired()])
|
||||||
price = HiddenField('price', validators=[DataRequired()])
|
price = HiddenField('price', validators=[DataRequired()])
|
||||||
|
amount = HiddenField('amount', validators=[DataRequired()])
|
||||||
requesting = BooleanField("", default=False,
|
requesting = BooleanField("", default=False,
|
||||||
render_kw={"class": "form-check-input"})
|
render_kw={"class": "form-check-input form-requesting"})
|
||||||
new_or_existing = RadioField("",
|
new_or_existing = RadioField("",
|
||||||
choices=get_choices(),
|
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)
|
coerce=int, default=3)
|
||||||
# Fields for new Item
|
# Fields for new Item
|
||||||
new_ean = IntegerField("EAN ID", render_kw={
|
new_ean = IntegerField("EAN ID", render_kw={
|
||||||
@ -51,19 +52,25 @@ class CheckItemsEntryForm(Form):
|
|||||||
"Please choose if it's a new or existing Item.")
|
"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):
|
class CheckItemsForm(FlaskForm):
|
||||||
items = FieldList(FormField(CheckItemsEntryForm))
|
items = FieldList(FormField(CheckItemsEntryForm))
|
||||||
|
custom_items = FieldList(FormField(CheckCustomItemsEntryForm))
|
||||||
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, itemarray):
|
def new(cls, itemarray):
|
||||||
CheckItemsEntry = namedtuple(
|
CheckItemsEntry = namedtuple(
|
||||||
"CheckItemsEntry", ["itemname", "price", "new_brand"])
|
"CheckItemsEntry", ["itemname", "price", "amount", "new_brand"])
|
||||||
CheckItems = namedtuple("CheckItems", ["items"])
|
CheckItems = namedtuple("CheckItems", ["items"])
|
||||||
check_items_entry = []
|
check_items_entry = []
|
||||||
for item in itemarray:
|
for item in itemarray:
|
||||||
check_items_entry.append(CheckItemsEntry(
|
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)
|
check_items = CheckItems(check_items_entry)
|
||||||
form = cls(obj=check_items)
|
form = cls(obj=check_items)
|
||||||
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@ from datetime import date
|
|||||||
from flask import abort, request, url_for
|
from flask import abort, request, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from . import bp
|
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 .utils import insert_existing_item, insert_new_item, insert_item_to_receipt, clear_receipt_items
|
||||||
from src import db, LOGGER
|
from src import db, LOGGER
|
||||||
from models import AmountChange, Item, LoginToken, PriceChange, Receipt, ReceiptItem
|
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
|
from src.utils.routes_utils import render_custom_template as render_template
|
||||||
|
|
||||||
PDFDir = "./"
|
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:
|
if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner:
|
||||||
receipt: PDFReceipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf")
|
receipt: PDFReceipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf")
|
||||||
form: CheckItemsForm = CheckItemsForm.new(receipt.items)
|
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
|
# 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.
|
# times and provide dropdown menu if necessary. If not, provide input field.
|
||||||
# temp_choices = []
|
# temp_choices = []
|
||||||
@ -58,7 +59,15 @@ def confirm_receipt_items(receipt_id: int):
|
|||||||
else:
|
else:
|
||||||
LOGGER.debug("Cold catched")
|
LOGGER.debug("Cold catched")
|
||||||
abort(400)
|
abort(400)
|
||||||
LOGGER.debug("At this point form.items.data will be returned")
|
for itempos, formcustomitem in enumerate(form.custom_items):
|
||||||
# return form.items.data
|
formcustomitemdata = formcustomitem.data
|
||||||
return render_template("receipts/check_items.html", form=form)
|
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)
|
abort(403)
|
||||||
@ -35,7 +35,7 @@ def insert_existing_item(formitemdict: dict[str: str], receipt_date: date = None
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def insert_item_to_receipt(receipt: Receipt, item_dict: dict[str: str], item_index:int=0):
|
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.add(receipt)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from .forms import UploadReceiptForm
|
|||||||
from src import db, LOGGER
|
from src import db, LOGGER
|
||||||
from models.receipt import Receipt
|
from models.receipt import Receipt
|
||||||
from models.login_token import LoginToken
|
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
|
from src.utils.routes_utils import render_custom_template as render_template
|
||||||
|
|
||||||
PDFDir = "./"
|
PDFDir = "./"
|
||||||
|
|||||||
@ -34,70 +34,6 @@ def insert_bought_items(token: str, dates: list[dict[str: any]]):
|
|||||||
del (dates[date_index])
|
del (dates[date_index])
|
||||||
return {'token': token, 'dates': dates} if dates else {}
|
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):
|
def generate_token(length=15, allowed_chars=ascii_letters + digits):
|
||||||
new_token = "".join((rndchoice(allowed_chars) for i in range(length)))
|
new_token = "".join((rndchoice(allowed_chars) for i in range(length)))
|
||||||
if not LoginToken.query.filter_by(token=new_token).first():
|
if not LoginToken.query.filter_by(token=new_token).first():
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import fitz
|
import fitz
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from re import search
|
from re import search
|
||||||
|
|
||||||
class PDFReceipt:
|
class PDFReceipt:
|
||||||
@ -11,8 +11,14 @@ class PDFReceipt:
|
|||||||
Currently supported: 'edeka'
|
Currently supported: 'edeka'
|
||||||
"""
|
"""
|
||||||
def __init__(self, bPDFFile, parser: str = "edeka") -> None:
|
def __init__(self, bPDFFile, parser: str = "edeka") -> None:
|
||||||
self.text = PDFReceipt._getTextFromPDF(bPDFFile)
|
try:
|
||||||
self.id, self.date, self.items = PDFReceipt._getInfosFromText(self.text, parser)
|
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):
|
def _getTextFromPDF(file):
|
||||||
with fitz.open(file, filetype="pdf") as doc:
|
with fitz.open(file, filetype="pdf") as doc:
|
||||||
|
|||||||
@ -4,31 +4,6 @@ from models.amount_change import AmountChange
|
|||||||
from models.bought import Bought
|
from models.bought import Bought
|
||||||
from models.price_change import PriceChange
|
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():
|
def selectable_price_per_amount_view():
|
||||||
p = db.aliased(PriceChange, name="p")
|
p = db.aliased(PriceChange, name="p")
|
||||||
a = db.aliased(AmountChange, name="a")
|
a = db.aliased(AmountChange, name="a")
|
||||||
|
|||||||
154
backend/web/static/receipts/check_items.js
Normal file
154
backend/web/static/receipts/check_items.js
Normal 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);
|
||||||
|
});
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
|
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
|
<h3>{{ user.email }}: {{ (user.sum | int)/100 }} €</h3>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{% if user.id %}
|
{% if user.id %}
|
||||||
|
|||||||
@ -5,70 +5,55 @@
|
|||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
<form action="" method="post" novalidate enctype="multipart/form-data">
|
<form action="" method="post" novalidate enctype="multipart/form-data">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{% for item in form.items %}
|
<div id="items-container">
|
||||||
<h4>{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})</h4>
|
{% for item in form.items %}
|
||||||
{{ render_field(item.itemname) }}
|
<div id="{{ item.id }}" class="form-item" data-index="{{ loop.index-1 }}">
|
||||||
{{ render_field(item.price) }}
|
<h4>{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})</h4>
|
||||||
<div class="{{ item.new_or_existing.id }}" style="display: none;">
|
{{ render_field(item.itemname) }}
|
||||||
{{ render_field(item.new_or_existing) }}
|
{{ render_field(item.price) }}
|
||||||
|
<div id="{{ item.new_or_existing.id }}" class="new_or_existing-subform" style="display: none;">
|
||||||
|
{{ render_field(item.new_or_existing) }}
|
||||||
|
</div>
|
||||||
|
<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 id="{{ item.id }}_existing" class="existing-subform" style="display: none;">
|
||||||
|
{{ render_field(item.existing_item) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ item.id }}_new" style="display: none;">
|
<div id="custom_items-container">
|
||||||
{{ render_field(item.new_ean) }}
|
{% for item in form.custom_items %}
|
||||||
{{ render_field(item.new_description) }}
|
<div id="{{ item.id }}" class="form-custom_item" data-index="{{ loop.index-1 }}">
|
||||||
{{ render_field(item.new_amount_change) }}
|
{{ render_field(item.itemname) }}
|
||||||
{{ render_field(item.new_brand) }}
|
{{ render_field(item.price) }}
|
||||||
|
{{ render_field(item.amount) }}
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="{{ item.id }}_existing" style="display: none;">
|
<button type="button" id="append" class="btn btn-success">Append</button>
|
||||||
{{ render_field(item.existing_item) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
{% endfor %}
|
|
||||||
{{ form.submit() }}
|
{{ form.submit() }}
|
||||||
</form>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% for item in form.items %}
|
<script src="{{ url_for('static', filename='receipts/check_items.js') }}"></script>
|
||||||
<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 %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user