Compare commits
	
		
			627 Commits
		
	
	
		
			v0.2.0
			...
			7c512b1c15
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7c512b1c15 | |||
| dc432da398 | |||
| c8504bcbf5 | |||
| c865141583 | |||
| 8c1ecd6eac | |||
| e8e4100677 | |||
| 6a8773c531 | |||
| 30e0c7682c | |||
| eb5a7bef7e | |||
| 8a174d8847 | |||
| 7459afd63a | |||
| a9afc385e9 | |||
| a8be739ec7 | |||
| 0130bc58a9 | |||
| 821059fa80 | |||
| 5b4f57d0b3 | |||
| 4bfac2d545 | |||
| f105f0cf7b | |||
| 5e320729d7 | |||
| 7515032082 | |||
| 361b18e411 | |||
| 7d084e570e | |||
| cb397910f8 | |||
| 5f8b0dec98 | |||
| 8398d1e8bb | |||
| 562801692a | |||
| faee1e61c6 | |||
| 57a4177037 | |||
| fa1dedf207 | |||
| 7ed13dc0af | |||
| 52807c5322 | |||
| 231a71feea | |||
| 4902eecae0 | |||
| 889e4c058e | |||
| 7262a6cb42 | |||
| c4ff4ecb3d | |||
| 2859f628ea | |||
| e0355b2af1 | |||
| b4d390c33a | |||
| a4ab8a761c | |||
| 907ce6d06e | |||
| 7e1388735e | |||
| 6f773dd837 | |||
| 87fa5aa6bc | |||
| 35e05b3708 | |||
| 7ccc96bda0 | |||
| 283758ebe9 | |||
| b673c7aeaf | |||
| 0ad2ac53dd | |||
| 7e90ec5a8f | |||
| 7755365467 | |||
| 979eea606a | |||
| 5a9e08f2c4 | |||
| 68c810d492 | |||
| 5f88260507 | |||
| 779d89f8c4 | |||
| 5d4bf4361b | |||
| 10170d613d | |||
| c885c08c37 | |||
| e2a4340f2a | |||
| 9728ff30e0 | |||
| a4644ede5f | |||
| 8f477dd6f1 | |||
| 44ac53f15c | |||
| 5edb5465c5 | |||
| 067afdb165 | |||
| 37a4c26f86 | |||
| 89e43830b4 | |||
| 671dbfb692 | |||
| 2014344d25 | |||
| f9c39709c8 | |||
| b394c58ec6 | |||
| 0af3e2785b | |||
| 7066f75e72 | |||
| 619540da49 | |||
| 567004f7d9 | |||
| 761d5a5824 | |||
| fa3cdace7f | |||
| 656762850c | |||
| e2325f08d0 | |||
| 855356084e | |||
| 7aaeb32a3d | |||
| b376cf1580 | |||
| ccbdc779ac | |||
| 61ee08fda2 | |||
| d8afadda02 | |||
| c8e1270d8f | |||
| 2a78799404 | |||
| 863d7a9368 | |||
| 6fd37b21d9 | |||
| bbf3ee3320 | |||
| b60cc7902d | |||
| 623313b58a | |||
| d0d2d77a2e | |||
| 494faeffea | |||
| 871a5fd1d8 | |||
| e615ad2690 | |||
| da92a0b42c | |||
| 678d0aa773 | |||
| 9248ba7e3b | |||
| 446087b212 | |||
| a42e7d13a2 | |||
| a82f5091f1 | |||
| 3455827c09 | |||
| 5dccf99a55 | |||
| 8818b46e01 | |||
| 2f3ad99467 | |||
| 592910187b | |||
| cb7a0d377f | |||
| 79175285f8 | |||
| fef474977c | |||
| fa1a55cd3d | |||
| 2253ec7e6d | |||
| 32aa532548 | |||
| 56138f7de3 | |||
| 21ef944259 | |||
| 760f1c2877 | |||
| e377eac407 | |||
| 77787eee9f | |||
| 03265a1232 | |||
| 079dc1ab6d | |||
| d4fe91ec4a | |||
| acc5b4d6ea | |||
| 19a93cb4c3 | |||
| 116089d1d2 | |||
| 50dd6078c7 | |||
| 9a4531b26c | |||
| b1af1d7425 | |||
| 8f909965a9 | |||
| e26af6f3fc | |||
| 02fffc3400 | |||
| d7d6929bf2 | |||
| e4cc61552e | |||
| d18dd7d4d2 | |||
| 3251660092 | |||
| c1235608d8 | |||
| 25c45b16ae | |||
| 78f570b81b | |||
| 5db13393cc | |||
| 1e286fbeba | |||
| d4b3fe67b9 | |||
| 5d0757c845 | |||
| b69a519904 | |||
| 122b7b059c | |||
| 4977847dd8 | |||
| b9b197ea27 | |||
| 884e37fe1b | |||
| cc6a73211e | |||
| 2299b86d0f | |||
| 6d293a1aac | |||
| a2311aee24 | |||
| 5571c0d01f | |||
| 98e1bad413 | |||
| 7ff52d99e6 | |||
| cc440a4110 | |||
| f5149a0c37 | |||
| ca928636fd | |||
| 4a8297d594 | |||
| 915e4408e1 | |||
| fd9eac06f6 | |||
| 403942dfc0 | |||
| 35dc513760 | |||
| 01861f0b6a | |||
| 8c10f1e96a | |||
| 5f7fc0b8e8 | |||
| 700c179774 | |||
| cabe02f7d0 | |||
| 5ceb9f2e83 | |||
| fe1c7669b6 | |||
| 4eac10981f | |||
| c869bccc04 | |||
| 61c111db69 | |||
| 34f63c1cdf | |||
| a643d9e811 | |||
| 2239ddfad1 | |||
| 12fbe36b9a | |||
| 46e34bb89a | |||
| c9453d3023 | |||
| fc766724c4 | |||
| 38c394c0af | |||
| 67e2b06d37 | |||
| be10a8d99e | |||
| fbeec600b7 | |||
| 1a54592d4c | |||
| 94a527caf2 | |||
| 0a1bbbdd47 | |||
| 82b63e4bd4 | |||
| e1d1aff0c1 | |||
| 2e5f9ee01f | |||
| f901a0020f | |||
| fc2be75c3b | |||
| 96c131940b | |||
| b9435a255b | |||
| 56045f0faf | |||
| 08d1e60238 | |||
| d88b3ac770 | |||
| 40e329d37f | |||
| 23a0721d8d | |||
| 2b2c665eb6 | |||
| 954173a2c2 | |||
| 91e6dc6668 | |||
| e9d8a8fcd8 | |||
| 4c84686395 | |||
| 61fd1849ed | |||
| a67158f8f6 | |||
| 5c6bfd8b49 | |||
| d9ecf51c6d | |||
| 5d31eb9172 | |||
| fadce244c5 | |||
| cbe7c6ca6d | |||
| b03938fb2e | |||
| 8061a23fdc | |||
| cd8a480cd0 | |||
| b8b87714eb | |||
| bf2f96621d | |||
| 2d771f04be | |||
| 3a12472d4b | |||
| d5a686a5d8 | |||
| 690f89e29a | |||
| 82a6a53dc4 | |||
| cdd31b1047 | |||
| 5bad949cfa | |||
| 3826646d06 | |||
| 74071e8997 | |||
| 3ce34803f3 | |||
| 232f73172f | |||
| ff1bb7142b | |||
| 7155bf635a | |||
| c306ff8009 | |||
| b344abce06 | |||
| b3c666c872 | |||
| 6a671cac84 | |||
| fe87c3a7de | |||
| 2013f8cbd9 | |||
| 2325842471 | |||
| c80e58b049 | |||
| be0ae5eba4 | |||
| 2b84f64554 | |||
| 0a658a76e8 | |||
| 50dc79d865 | |||
| 8e5377a416 | |||
| 4299fd6fbd | |||
| 1d6a53f7cd | |||
| bb2993b0c0 | |||
| f6946c1165 | |||
| 8e219d8006 | |||
| 53565eb9e6 | |||
| 965e78d8ad | |||
| 74b81d3e23 | |||
| a0fba6387f | |||
| d28bdf2064 | |||
| edf0c00e34 | |||
| 107d161379 | |||
| f2c184f769 | |||
| b45986ecfc | |||
| a2c2452ec5 | |||
| 5194258b48 | |||
| 3fe7eb41ac | |||
| 7fb9e2f0a1 | |||
| 1d443f7b76 | |||
| 6ad4fba9cd | |||
| 3dda6531b5 | |||
| 4d11517e21 | |||
| 308e4ac69d | |||
| de09e1498b | |||
| c26c4686c5 | |||
| c95f4fcc47 | |||
| 71af74fc8a | |||
| 56e972c371 | |||
| 7feb6da062 | |||
| af71874f9d | |||
| 3fa8818a27 | |||
| be46d8aa14 | |||
| 20f55058ac | |||
| e9d1a53e03 | |||
| 38141759fd | |||
| 7fb3e3bc2c | |||
| 05ac5158f8 | |||
| ec257a4b57 | |||
| 5ebb89a6d5 | |||
| 900d60d1ae | |||
| bc792c145f | |||
| 4432484acd | |||
| 7ad3f9e0cb | |||
| 060a52f7a2 | |||
| c17430d211 | |||
| 8fd99bb617 | |||
| ce388eb6c8 | |||
| 1850f9787e | |||
| c6d55fad1c | |||
| 0c647d8f21 | |||
| 5d1f87582e | |||
| ef086b3f81 | |||
| b4be1db712 | |||
| 5d44ebdfd8 | |||
| 9859604c81 | |||
| d31e495f6b | |||
| 7c4102be44 | |||
| 1fd50e23d9 | |||
| 9635448f18 | |||
| e7f1ca332e | |||
| 3d2e40865e | |||
| 5132141c68 | |||
| e37f6792c9 | |||
| e6b1136a14 | |||
| d7bc01ccb4 | |||
| 27beff3f8f | |||
| c6c545b99f | |||
| 6d5a2fae6a | |||
| 8819eabcd0 | |||
| 3582d960ca | |||
| 02e10a301a | |||
| f0187434d2 | |||
| 34af52e3c3 | |||
| 965df82c1c | |||
| df53f06094 | |||
| 140d3c6010 | |||
| a65dccac92 | |||
| 740e1cfac1 | |||
| b62f31d385 | |||
| 1c740b9bbc | |||
| 380256eda7 | |||
| 74b695c089 | |||
| 6d1e705e4b | |||
| 8abe20dba5 | |||
| ed7a8ac0fd | |||
| 74eee034d0 | |||
| d19d23fe37 | |||
| 4ce577d7d8 | |||
| a340fad109 | |||
| 555ad388bc | |||
| 2f27ad5bef | |||
| c6487bf9d4 | |||
| ff3dd28cd7 | |||
| a14ffa93ed | |||
| 672fcbcbdf | |||
| cb4258dd6d | |||
| 6fc21f82af | |||
| 13e3ef5875 | |||
| 21b3320e66 | |||
| 5c47e63ae3 | |||
| f59378002e | |||
| 531e90e8ad | |||
| 8fc33131dd | |||
| 62716eb545 | |||
| 14d5d1e8d6 | |||
| 4306ed739f | |||
| 1f87bc00e8 | |||
| ff9ff4bdcf | |||
| 578233d66d | |||
| 5e7f790f87 | |||
| d64f354ee0 | |||
| ba3d8c6d4e | |||
| 4f7f87b10d | |||
| 4273f99644 | |||
| ffe834bedd | |||
| e448e009c9 | |||
| b6802c51bb | |||
| 2515c1ea1f | |||
| 0ef6409f75 | |||
| ed18b81ad8 | |||
| b46cec6fab | |||
| 6c122666a0 | |||
| 7ddc9ececf | |||
| 4eebbd9692 | |||
| 338b49c965 | |||
| f438f97571 | |||
| 9b273115a0 | |||
| 58d1add810 | |||
| c189615ca4 | |||
| 5687852dfb | |||
| d74c62dbb7 | |||
| 987e98ebc0 | |||
| 7083f22577 | |||
| 7b10eb68bc | |||
| f277010991 | |||
| 729a7fd107 | |||
| c8230c949d | |||
| 3c98960efe | |||
| c5d0d91a7d | |||
| fb06e9db44 | |||
| d47e2e231b | |||
| cb89f34455 | |||
| 11ab4a4ba6 | |||
| 5dc8387ad9 | |||
| 26b70bb625 | |||
| f30a96d7e9 | |||
| a1627b7fbf | |||
| 7c3b8c8f44 | |||
| b19f4fa939 | |||
| 41c3e06ce4 | |||
| 8a3df7a689 | |||
| 196a115c99 | |||
| 005f9083aa | |||
| 12dbae56c4 | |||
| a98723c57b | |||
| d5bd3b8383 | |||
| 617dd29f23 | |||
| b0a4a735f3 | |||
| 41770e38b8 | |||
| d8a6614543 | |||
| 8d76b5130e | |||
| 43fc4b9b8d | |||
| 3ed8d7f1d2 | |||
| ea7c194d7e | |||
| 041a905fc0 | |||
| 10d1be8bd1 | |||
| 6e1d35eda4 | |||
| 52b5151fe0 | |||
| f9fc033de6 | |||
| 116d00a557 | |||
| 329e3d5362 | |||
| 47e8944f06 | |||
| e7c43ae390 | |||
| b8b51b34d3 | |||
| d083036719 | |||
| 7fe81c710b | |||
| 9993f65627 | |||
| fe01d5418d | |||
| 2f7b9932a0 | |||
| 1eed16b732 | |||
| ede1160943 | |||
| 3814f0cb18 | |||
| 24315b8203 | |||
| 3c200d0dc6 | |||
| 9f1e724875 | |||
| f838e7f893 | |||
| edb893ecd3 | |||
| 436a4c367f | |||
| 1813ce0cfa | |||
| 7683347997 | |||
| 46ffc7a73d | |||
| e0a807d625 | |||
| dffcf6d2ce | |||
| 84d239e4b1 | |||
| fcefc64117 | |||
| 81fbb380b4 | |||
| d7ac8a3dcf | |||
| bcd3418e2c | |||
| ef9e5cb5b3 | |||
| e797cfeb8c | |||
| 22bae7f766 | |||
| aa669e9f53 | |||
| 898a1af7b5 | |||
| f762bcf48f | |||
| 5d4effa360 | |||
| dd05478bf3 | |||
| 9450915404 | |||
| 8d126e183f | |||
| bfb08cf5fc | |||
| a7bcf4b5c1 | |||
| cd49ca44b1 | |||
| 734362396f | |||
| 88147bea66 | |||
| cca43c68a6 | |||
| 480e2d2d8f | |||
| be100ce7ec | |||
| eca91d32ed | |||
| 1f95212494 | |||
| 0173104c84 | |||
| 6e33fa775d | |||
| e244ff70e6 | |||
| ace782a26b | |||
| 90289a0db2 | |||
| 7e7e1a2844 | |||
| ddd028736c | |||
| e1d35a64da | |||
| 39807ef480 | |||
| 39723b1299 | |||
| 8cd004bede | |||
| 4f112dd386 | |||
| b806b1ed1f | |||
| 1d0a79e33c | |||
| d4a690ebbc | |||
| 68687897f3 | |||
| a7250fd9bf | |||
| eabe80b790 | |||
| fe77f87110 | |||
| 32c27d7c07 | |||
| 14b871b57a | |||
| 9d5fce2752 | |||
| d333151731 | |||
| b2e500a714 | |||
| b705795b44 | |||
| 250f4ff1ae | |||
| 6bed180790 | |||
| 10fbc3f638 | |||
| f65dc6fc42 | |||
| 9833bac6e4 | |||
| 7d412b20d7 | |||
| 9bfcd3c50c | |||
| 55c2ce6695 | |||
| 493677e0aa | |||
| 710c26d016 | |||
| 24415018b7 | |||
| c50b9a2000 | |||
| af9bd14eed | |||
| 9e1ff16e96 | |||
| f7c1fd77f2 | |||
| 641315537d | |||
| a895bd8560 | |||
| ca86a08f3e | |||
| e118422441 | |||
| b3777cffbf | |||
| 39c9c17007 | |||
| 3ab4eacf9f | |||
| cff3d1b6bd | |||
| f41db78831 | |||
| 73f7d14e7b | |||
| f6ed6b10a7 | |||
| b5aaee4d15 | |||
| c849d6b3d4 | |||
| a9908a7df4 | |||
| 063c769158 | |||
| f8e9871300 | |||
| 78a62a9575 | |||
| 85fde6219e | |||
| 4eb9346d8d | |||
| 11966a52ba | |||
| 8cf81b5459 | |||
| cc958a39b3 | |||
| 9065686cc5 | |||
| 9a41cb10a1 | |||
| 6957e52d0d | |||
| 9cd9e90be0 | |||
| 2839dc60b4 | |||
| f3548a2327 | |||
| 79883d6940 | |||
| b2bc993416 | |||
| 453b3f0da5 | |||
| 63ae3f0746 | |||
| da4cc6489f | |||
| 1102a3a4f3 | |||
| 1402a12f04 | |||
| f049b5d7ee | |||
| 14ed4ca354 | |||
| 535ff96ab3 | |||
| 57482f81fc | |||
| a31ce3c400 | |||
| 319f0aed90 | |||
| 826dcf0f86 | |||
| b2411aee74 | |||
| 731acdced0 | |||
| 35b3bca1e6 | |||
| 3c413497ae | |||
| 1b5e516413 | |||
| 20cb5cecc4 | |||
| 08dc24605d | |||
| bb7e9e94ee | |||
| 2680a1c872 | |||
| 20a7ce591c | |||
| 474e844ed9 | |||
| b34955f2fb | |||
| 2bd0f0f14d | |||
| 8b77d9ff93 | |||
| a9c7360020 | |||
| d02c87602b | |||
| 9f966643b5 | |||
| 5746e2a3d6 | |||
| d5c2231794 | |||
| fc8e257a10 | |||
| 2e9bf382fb | |||
| de48c848da | |||
| 9cdcc828a7 | |||
| b28d446d07 | |||
| 274a38a588 | |||
| fff89a9957 | |||
| 5613657c8f | |||
| 26bb16dd40 | |||
| f0d39bb27b | |||
| 4c17310ebf | |||
| fd36672877 | |||
| d67c57056b | |||
| 59c55ef574 | |||
| 329027969a | |||
| 9f7a8c9540 | |||
| 384bb2c46d | |||
| cabfe268ce | |||
| 26df71014b | |||
| 3126ee8153 | |||
| cb622f4bad | |||
| 515d39e61c | |||
| 952061c4bb | |||
| 788225826d | |||
| c52081e528 | |||
| 1f235acdf9 | |||
| 0f6c23e1f3 | |||
| 488e72679e | |||
| 6d43b14862 | |||
| 685213cdbb | |||
| 05fde3a742 | |||
| 9383f5484f | |||
| 88314e1e45 | |||
| 83b5761bca | |||
| f25c993b75 | |||
| 6d02f8033d | |||
| 2c367703e4 | |||
| 284b5be128 | |||
| a672a13789 | |||
| 9af9afd14d | |||
| d98e9f8f05 | |||
| 652bddc07a | |||
| 5a6e4f5b5e | |||
| f878ba5535 | |||
| e7c36ba13a | |||
| 4cfe7c7c59 | |||
| b0b30a8ae6 | |||
| 2e3633b205 | |||
| d68aa91c33 | |||
| 3f63fb0bda | |||
| d5af5de3c1 | |||
| d9c08568cf | |||
| a4c89f1494 | |||
| a73e3204b9 | |||
| 330a71ebf2 | |||
| 36b0bb3a0e | |||
| 2ab60b2224 | |||
| 36f55900c7 | |||
| d99f592cff | |||
| e24ed61b99 | |||
| 354f1ff3d8 | |||
| d8e0e30c41 | |||
| d58859bcf3 | |||
| 40e64c4d2e | |||
| 2aacb67988 | |||
| a839c5a41a | |||
| 356d10eb6e | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -38,3 +38,4 @@ excludes
 | 
				
			|||||||
*.mo
 | 
					*.mo
 | 
				
			||||||
zh_Hans
 | 
					zh_Hans
 | 
				
			||||||
test_temp.py
 | 
					test_temp.py
 | 
				
			||||||
 | 
					dummy.js
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								MANIFEST.in
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								MANIFEST.in
									
									
									
									
									
								
							@@ -15,6 +15,7 @@
 | 
				
			|||||||
#  See the License for the specific language governing permissions and
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exclude src/accounting/static/js/dummy.js
 | 
				
			||||||
include src/accounting/translations/*
 | 
					include src/accounting/translations/*
 | 
				
			||||||
include src/accounting/translations/*/LC_MESSAGES/*
 | 
					include src/accounting/translations/*/LC_MESSAGES/*
 | 
				
			||||||
include docs/*
 | 
					include docs/*
 | 
				
			||||||
@@ -22,7 +23,9 @@ include docs/source/*
 | 
				
			|||||||
include docs/source/_static/*
 | 
					include docs/source/_static/*
 | 
				
			||||||
include docs/source/_templates/*
 | 
					include docs/source/_templates/*
 | 
				
			||||||
include tests/*
 | 
					include tests/*
 | 
				
			||||||
include tests/testsite/*
 | 
					exclude tests/test_temp.py
 | 
				
			||||||
include tests/testsite/templates/*
 | 
					include tests/test_site/*
 | 
				
			||||||
include tests/testsite/translations/*
 | 
					include tests/test_site/static/*
 | 
				
			||||||
include tests/testsite/translations/*/LC_MESSAGES/*
 | 
					include tests/test_site/templates/*
 | 
				
			||||||
 | 
					include tests/test_site/translations/*
 | 
				
			||||||
 | 
					include tests/test_site/translations/*/LC_MESSAGES/*
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,10 +28,10 @@ accounting.account.forms module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.account.query module
 | 
					accounting.account.queries module
 | 
				
			||||||
-------------------------------
 | 
					---------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. automodule:: accounting.account.query
 | 
					.. automodule:: accounting.account.queries
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,10 +20,10 @@ accounting.base\_account.converters module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.base\_account.query module
 | 
					accounting.base\_account.queries module
 | 
				
			||||||
-------------------------------------
 | 
					---------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. automodule:: accounting.base_account.query
 | 
					.. automodule:: accounting.base_account.queries
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,10 +28,10 @@ accounting.currency.forms module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.currency.query module
 | 
					accounting.currency.queries module
 | 
				
			||||||
--------------------------------
 | 
					----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. automodule:: accounting.currency.query
 | 
					.. automodule:: accounting.currency.queries
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										45
									
								
								docs/source/accounting.journal_entry.forms.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								docs/source/accounting.journal_entry.forms.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					accounting.journal\_entry.forms package
 | 
				
			||||||
 | 
					=======================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.forms.currency module
 | 
				
			||||||
 | 
					-----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.forms.currency
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.forms.journal\_entry module
 | 
				
			||||||
 | 
					-----------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.forms.journal_entry
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.forms.line\_item module
 | 
				
			||||||
 | 
					-------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.forms.line_item
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.forms.reorder module
 | 
				
			||||||
 | 
					----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.forms.reorder
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.forms
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										46
									
								
								docs/source/accounting.journal_entry.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								docs/source/accounting.journal_entry.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					accounting.journal\_entry package
 | 
				
			||||||
 | 
					=================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Subpackages
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					   :maxdepth: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   accounting.journal_entry.forms
 | 
				
			||||||
 | 
					   accounting.journal_entry.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.converters module
 | 
				
			||||||
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.converters
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.template\_filters module
 | 
				
			||||||
 | 
					--------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.template_filters
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.views module
 | 
				
			||||||
 | 
					--------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.views
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										53
									
								
								docs/source/accounting.journal_entry.utils.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								docs/source/accounting.journal_entry.utils.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					accounting.journal\_entry.utils package
 | 
				
			||||||
 | 
					=======================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.utils.account\_option module
 | 
				
			||||||
 | 
					------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils.account_option
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.utils.description\_editor module
 | 
				
			||||||
 | 
					----------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils.description_editor
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.utils.offset\_alias module
 | 
				
			||||||
 | 
					----------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils.offset_alias
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.utils.operators module
 | 
				
			||||||
 | 
					------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils.operators
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.journal\_entry.utils.original\_line\_items module
 | 
				
			||||||
 | 
					------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils.original_line_items
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.journal_entry.utils
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										29
									
								
								docs/source/accounting.option.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docs/source/accounting.option.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					accounting.option package
 | 
				
			||||||
 | 
					=========================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.option.forms module
 | 
				
			||||||
 | 
					------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option.forms
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.option.views module
 | 
				
			||||||
 | 
					------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option.views
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.option
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										69
									
								
								docs/source/accounting.report.period.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								docs/source/accounting.report.period.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					accounting.report.period package
 | 
				
			||||||
 | 
					================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.chooser module
 | 
				
			||||||
 | 
					---------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.chooser
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.description module
 | 
				
			||||||
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.description
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.month\_end module
 | 
				
			||||||
 | 
					------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.month_end
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.parser module
 | 
				
			||||||
 | 
					--------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.parser
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.period module
 | 
				
			||||||
 | 
					--------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.period
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.shortcuts module
 | 
				
			||||||
 | 
					-----------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.shortcuts
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.period.specification module
 | 
				
			||||||
 | 
					---------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period.specification
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.period
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										69
									
								
								docs/source/accounting.report.reports.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								docs/source/accounting.report.reports.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					accounting.report.reports package
 | 
				
			||||||
 | 
					=================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.balance\_sheet module
 | 
				
			||||||
 | 
					-----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.balance_sheet
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.income\_expenses module
 | 
				
			||||||
 | 
					-------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.income_expenses
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.income\_statement module
 | 
				
			||||||
 | 
					--------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.income_statement
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.journal module
 | 
				
			||||||
 | 
					----------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.journal
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.ledger module
 | 
				
			||||||
 | 
					---------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.ledger
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.search module
 | 
				
			||||||
 | 
					---------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.search
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.reports.trial\_balance module
 | 
				
			||||||
 | 
					-----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports.trial_balance
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.reports
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										47
									
								
								docs/source/accounting.report.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								docs/source/accounting.report.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					accounting.report package
 | 
				
			||||||
 | 
					=========================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Subpackages
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					   :maxdepth: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   accounting.report.period
 | 
				
			||||||
 | 
					   accounting.report.reports
 | 
				
			||||||
 | 
					   accounting.report.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.converters module
 | 
				
			||||||
 | 
					-----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.converters
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.template\_filters module
 | 
				
			||||||
 | 
					------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.template_filters
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.views module
 | 
				
			||||||
 | 
					------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.views
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
							
								
								
									
										69
									
								
								docs/source/accounting.report.utils.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								docs/source/accounting.report.utils.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					accounting.report.utils package
 | 
				
			||||||
 | 
					===============================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Submodules
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.base\_page\_params module
 | 
				
			||||||
 | 
					-------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.base_page_params
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.base\_report module
 | 
				
			||||||
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.base_report
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.csv\_export module
 | 
				
			||||||
 | 
					------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.csv_export
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.option\_link module
 | 
				
			||||||
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.option_link
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.report\_chooser module
 | 
				
			||||||
 | 
					----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.report_chooser
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.report\_type module
 | 
				
			||||||
 | 
					-------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.report_type
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.report.utils.urls module
 | 
				
			||||||
 | 
					-----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils.urls
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Module contents
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.report.utils
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
@@ -10,15 +10,18 @@ Subpackages
 | 
				
			|||||||
   accounting.account
 | 
					   accounting.account
 | 
				
			||||||
   accounting.base_account
 | 
					   accounting.base_account
 | 
				
			||||||
   accounting.currency
 | 
					   accounting.currency
 | 
				
			||||||
 | 
					   accounting.journal_entry
 | 
				
			||||||
 | 
					   accounting.option
 | 
				
			||||||
 | 
					   accounting.report
 | 
				
			||||||
   accounting.utils
 | 
					   accounting.utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Submodules
 | 
					Submodules
 | 
				
			||||||
----------
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.database module
 | 
					accounting.forms module
 | 
				
			||||||
--------------------------
 | 
					-----------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. automodule:: accounting.database
 | 
					.. automodule:: accounting.forms
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
@@ -39,6 +42,22 @@ accounting.models module
 | 
				
			|||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.template\_filters module
 | 
				
			||||||
 | 
					-----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.template_filters
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.template\_globals module
 | 
				
			||||||
 | 
					-----------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.template_globals
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Module contents
 | 
					Module contents
 | 
				
			||||||
---------------
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,50 @@ accounting.utils package
 | 
				
			|||||||
Submodules
 | 
					Submodules
 | 
				
			||||||
----------
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
accounting.utils.next\_url module
 | 
					accounting.utils.cast module
 | 
				
			||||||
 | 
					----------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.cast
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.current\_account module
 | 
				
			||||||
 | 
					----------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.current_account
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.flash\_errors module
 | 
				
			||||||
 | 
					-------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.flash_errors
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.journal\_entry\_types module
 | 
				
			||||||
 | 
					---------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.journal_entry_types
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.next\_uri module
 | 
				
			||||||
---------------------------------
 | 
					---------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. automodule:: accounting.utils.next_url
 | 
					.. automodule:: accounting.utils.next_uri
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :undoc-members:
 | 
				
			||||||
 | 
					   :show-inheritance:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					accounting.utils.options module
 | 
				
			||||||
 | 
					-------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: accounting.utils.options
 | 
				
			||||||
   :members:
 | 
					   :members:
 | 
				
			||||||
   :undoc-members:
 | 
					   :undoc-members:
 | 
				
			||||||
   :show-inheritance:
 | 
					   :show-inheritance:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('../../src/'))
 | 
				
			|||||||
project = 'Mia! Accounting Flask'
 | 
					project = 'Mia! Accounting Flask'
 | 
				
			||||||
copyright = '2023, imacat'
 | 
					copyright = '2023, imacat'
 | 
				
			||||||
author = 'imacat'
 | 
					author = 'imacat'
 | 
				
			||||||
release = '0.0.0'
 | 
					release = '0.9.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- General configuration ---------------------------------------------------
 | 
					# -- General configuration ---------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
				
			||||||
@@ -28,5 +28,5 @@ exclude_patterns = []
 | 
				
			|||||||
# -- Options for HTML output -------------------------------------------------
 | 
					# -- Options for HTML output -------------------------------------------------
 | 
				
			||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
					# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html_theme = 'nature'
 | 
					html_theme = 'sphinx_rtd_theme'
 | 
				
			||||||
html_static_path = ['_static']
 | 
					html_static_path = ['_static']
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[metadata]
 | 
					[metadata]
 | 
				
			||||||
name = mia-accounting-flask
 | 
					name = mia-accounting-flask
 | 
				
			||||||
version = 0.2.0
 | 
					version = 0.9.0
 | 
				
			||||||
author = imacat
 | 
					author = imacat
 | 
				
			||||||
author_email = imacat@mail.imacat.idv.tw
 | 
					author_email = imacat@mail.imacat.idv.tw
 | 
				
			||||||
description = The Mia! Accounting Flask project.
 | 
					description = The Mia! Accounting Flask project.
 | 
				
			||||||
@@ -53,3 +53,4 @@ accounting =
 | 
				
			|||||||
  static/**
 | 
					  static/**
 | 
				
			||||||
  templates/**
 | 
					  templates/**
 | 
				
			||||||
  translations/*/LC_MESSAGES/*.mo
 | 
					  translations/*/LC_MESSAGES/*.mo
 | 
				
			||||||
 | 
					  data/**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,32 +17,32 @@
 | 
				
			|||||||
"""The accounting application.
 | 
					"""The accounting application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import typing as t
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from flask import Flask, Blueprint
 | 
					from flask import Flask, Blueprint
 | 
				
			||||||
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.utils.user import AbstractUserUtils
 | 
					from accounting.utils.user import UserUtilityInterface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					db: SQLAlchemy = SQLAlchemy()
 | 
				
			||||||
 | 
					"""The database instance."""
 | 
				
			||||||
 | 
					data_dir: Path = Path(__file__).parent / "data"
 | 
				
			||||||
 | 
					"""The data directory."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def init_app(app: Flask, user_utils: AbstractUserUtils,
 | 
					def init_app(app: Flask, user_utils: UserUtilityInterface,
 | 
				
			||||||
             url_prefix: str = "/accounting",
 | 
					             url_prefix: str = "/accounting") -> None:
 | 
				
			||||||
             can_view_func: t.Callable[[], bool] | None = None,
 | 
					 | 
				
			||||||
             can_edit_func: t.Callable[[], bool] | None = None) -> None:
 | 
					 | 
				
			||||||
    """Initialize the application.
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param app: The Flask application.
 | 
					    :param app: The Flask application.
 | 
				
			||||||
    :param user_utils: The user utilities.
 | 
					    :param user_utils: The user utilities.
 | 
				
			||||||
    :param url_prefix: The URL prefix of the accounting application.
 | 
					    :param url_prefix: The URL prefix of the accounting application.
 | 
				
			||||||
    :param can_view_func: A callback that returns whether the current user can
 | 
					 | 
				
			||||||
        view the accounting data.
 | 
					 | 
				
			||||||
    :param can_edit_func: A callback that returns whether the current user can
 | 
					 | 
				
			||||||
        edit the accounting data.
 | 
					 | 
				
			||||||
    :return: None.
 | 
					    :return: None.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    # The database instance must be set before loading everything
 | 
					    # The database instance must be set before loading everything
 | 
				
			||||||
    # in the application.
 | 
					    # in the application.
 | 
				
			||||||
    from .database import set_db
 | 
					    global db
 | 
				
			||||||
    set_db(app.extensions["sqlalchemy"])
 | 
					    db = app.extensions["sqlalchemy"]
 | 
				
			||||||
    from .utils.user import init_user_utils
 | 
					    from .utils.user import init_user_utils
 | 
				
			||||||
    init_user_utils(user_utils)
 | 
					    init_user_utils(user_utils)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,11 +51,25 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
 | 
				
			|||||||
                              template_folder="templates",
 | 
					                              template_folder="templates",
 | 
				
			||||||
                              static_folder="static")
 | 
					                              static_folder="static")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .template_filters import format_amount, format_date, default
 | 
				
			||||||
 | 
					    bp.add_app_template_filter(format_amount, "accounting_format_amount")
 | 
				
			||||||
 | 
					    bp.add_app_template_filter(format_date, "accounting_format_date")
 | 
				
			||||||
 | 
					    bp.add_app_template_filter(default, "accounting_default")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .template_globals import currency_options, default_currency_code
 | 
				
			||||||
 | 
					    bp.add_app_template_global(currency_options,
 | 
				
			||||||
 | 
					                               "accounting_currency_options")
 | 
				
			||||||
 | 
					    bp.add_app_template_global(default_currency_code,
 | 
				
			||||||
 | 
					                               "accounting_default_currency_code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from . import locale
 | 
					    from . import locale
 | 
				
			||||||
    locale.init_app(app, bp)
 | 
					    locale.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .utils import permission
 | 
					    from .utils import permission
 | 
				
			||||||
    permission.init_app(bp, can_view_func, can_edit_func)
 | 
					    permission.init_app(bp, user_utils)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .utils import next_uri
 | 
				
			||||||
 | 
					    next_uri.init_app(bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from . import base_account
 | 
					    from . import base_account
 | 
				
			||||||
    base_account.init_app(app, bp)
 | 
					    base_account.init_app(app, bp)
 | 
				
			||||||
@@ -66,7 +80,13 @@ def init_app(app: Flask, user_utils: AbstractUserUtils,
 | 
				
			|||||||
    from . import currency
 | 
					    from . import currency
 | 
				
			||||||
    currency.init_app(app, bp)
 | 
					    currency.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from .utils import next_url
 | 
					    from . import journal_entry
 | 
				
			||||||
    next_url.init_app(bp)
 | 
					    journal_entry.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from . import report
 | 
				
			||||||
 | 
					    report.init_app(app, bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from . import option
 | 
				
			||||||
 | 
					    option.init_app(bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.register_blueprint(bp)
 | 
					    app.register_blueprint(bp)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,19 +18,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
from secrets import randbelow
 | 
					from secrets import randbelow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import click
 | 
					import click
 | 
				
			||||||
from flask.cli import with_appcontext
 | 
					from flask.cli import with_appcontext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import BaseAccount, Account, AccountL10n
 | 
					from accounting.models import BaseAccount, Account, AccountL10n
 | 
				
			||||||
from accounting.utils.user import has_user, get_user_pk
 | 
					from accounting.utils.user import has_user, get_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
AccountData = tuple[int, str, int, str, str, str, bool]
 | 
					AccountData = tuple[int, str, int, str, str, str, bool]
 | 
				
			||||||
"""The format of the account data, as a list of (ID, base account code, number,
 | 
					"""The format of the account data, as a list of (ID, base account code, number,
 | 
				
			||||||
English, Traditional Chinese, Simplified Chinese, is-offset-needed) tuples."""
 | 
					English, Traditional Chinese, Simplified Chinese, is-need-offset) tuples."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def __validate_username(ctx: click.core.Context, param: click.core.Option,
 | 
					def __validate_username(ctx: click.core.Context, param: click.core.Option,
 | 
				
			||||||
@@ -93,14 +92,37 @@ def init_accounts_command(username: str) -> None:
 | 
				
			|||||||
    data: list[AccountData] = []
 | 
					    data: list[AccountData] = []
 | 
				
			||||||
    for base in bases_to_add:
 | 
					    for base in bases_to_add:
 | 
				
			||||||
        l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
 | 
					        l10n: dict[str, str] = {x.locale: x.title for x in base.l10n}
 | 
				
			||||||
        is_offset_needed: bool = True if re.match("^[12]1[34]", base.code) \
 | 
					        is_need_offset: bool = __is_need_offset(base.code)
 | 
				
			||||||
            else False
 | 
					 | 
				
			||||||
        data.append((get_new_id(), base.code, 1, base.title_l10n,
 | 
					        data.append((get_new_id(), base.code, 1, base.title_l10n,
 | 
				
			||||||
                     l10n["zh_Hant"], l10n["zh_Hans"], is_offset_needed))
 | 
					                     l10n["zh_Hant"], l10n["zh_Hans"], is_need_offset))
 | 
				
			||||||
    __add_accounting_accounts(data, creator_pk)
 | 
					    __add_accounting_accounts(data, creator_pk)
 | 
				
			||||||
    click.echo(F"{len(data)} added.  Accounting accounts initialized.")
 | 
					    click.echo(F"{len(data)} added.  Accounting accounts initialized.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __is_need_offset(base_code: str) -> bool:
 | 
				
			||||||
 | 
					    """Checks that whether journal entry line items in the account need offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param base_code: The code of the base account.
 | 
				
			||||||
 | 
					    :return: True if journal entry line items in the account need offset, or
 | 
				
			||||||
 | 
					        False otherwise.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # Assets
 | 
				
			||||||
 | 
					    if base_code[0] == "1":
 | 
				
			||||||
 | 
					        if base_code[:3] in {"113", "114", "118", "184"}:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        if base_code in {"1411", "1421", "1431", "1441", "1511", "1521",
 | 
				
			||||||
 | 
					                         "1581", "1611", "1851", ""}:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    # Liabilities
 | 
				
			||||||
 | 
					    if base_code[0] == "2":
 | 
				
			||||||
 | 
					        if base_code in {"2111", "2114", "2284", "2293"}:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    # Only assets and liabilities need offset
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
 | 
					def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
 | 
				
			||||||
        -> None:
 | 
					        -> None:
 | 
				
			||||||
    """Adds the accounts.
 | 
					    """Adds the accounts.
 | 
				
			||||||
@@ -113,7 +135,7 @@ def __add_accounting_accounts(data: list[AccountData], creator_pk: int)\
 | 
				
			|||||||
                                       base_code=x[1],
 | 
					                                       base_code=x[1],
 | 
				
			||||||
                                       no=x[2],
 | 
					                                       no=x[2],
 | 
				
			||||||
                                       title_l10n=x[3],
 | 
					                                       title_l10n=x[3],
 | 
				
			||||||
                                       is_offset_needed=x[6],
 | 
					                                       is_need_offset=x[6],
 | 
				
			||||||
                                       created_by_id=creator_pk,
 | 
					                                       created_by_id=creator_pk,
 | 
				
			||||||
                                       updated_by_id=creator_pk)
 | 
					                                       updated_by_id=creator_pk)
 | 
				
			||||||
                               for x in data]
 | 
					                               for x in data]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ from flask_wtf import FlaskForm
 | 
				
			|||||||
from wtforms import StringField, BooleanField
 | 
					from wtforms import StringField, BooleanField
 | 
				
			||||||
from wtforms.validators import DataRequired, ValidationError
 | 
					from wtforms.validators import DataRequired, ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import BaseAccount, Account
 | 
					from accounting.models import BaseAccount, Account
 | 
				
			||||||
from accounting.utils.random_id import new_id
 | 
					from accounting.utils.random_id import new_id
 | 
				
			||||||
@@ -53,6 +53,20 @@ class BaseAccountAvailable:
 | 
				
			|||||||
                "The base account is not available."))
 | 
					                "The base account is not available."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NoOffsetNominalAccount:
 | 
				
			||||||
 | 
					    """The validator to check nominal account is not to be offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: BooleanField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, AccountForm)
 | 
				
			||||||
 | 
					        if not field.data:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if form.base_code.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if form.base_code.data[0] not in {"1", "2", "3"}:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "A nominal account does not need offset."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountForm(FlaskForm):
 | 
					class AccountForm(FlaskForm):
 | 
				
			||||||
    """The form to create or edit an account."""
 | 
					    """The form to create or edit an account."""
 | 
				
			||||||
    base_code = StringField(
 | 
					    base_code = StringField(
 | 
				
			||||||
@@ -66,8 +80,9 @@ class AccountForm(FlaskForm):
 | 
				
			|||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[DataRequired(lazy_gettext("Please fill in the title"))])
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the title"))])
 | 
				
			||||||
    """The title."""
 | 
					    """The title."""
 | 
				
			||||||
    is_offset_needed = BooleanField()
 | 
					    is_need_offset = BooleanField(
 | 
				
			||||||
    """Whether the the entries of this account need offsets."""
 | 
					        validators=[NoOffsetNominalAccount()])
 | 
				
			||||||
 | 
					    """Whether the the journal entry line items of this account need offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def populate_obj(self, obj: Account) -> None:
 | 
					    def populate_obj(self, obj: Account) -> None:
 | 
				
			||||||
        """Populates the form data into an account object.
 | 
					        """Populates the form data into an account object.
 | 
				
			||||||
@@ -76,29 +91,30 @@ class AccountForm(FlaskForm):
 | 
				
			|||||||
        :return: None.
 | 
					        :return: None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        is_new: bool = obj.id is None
 | 
					        is_new: bool = obj.id is None
 | 
				
			||||||
        prev_base_code: str | None = obj.base_code
 | 
					 | 
				
			||||||
        if is_new:
 | 
					        if is_new:
 | 
				
			||||||
            obj.id = new_id(Account)
 | 
					            obj.id = new_id(Account)
 | 
				
			||||||
        obj.base_code = self.base_code.data
 | 
					        if obj.base_code != self.base_code.data:
 | 
				
			||||||
        if prev_base_code != self.base_code.data:
 | 
					            if obj.base_code is not None:
 | 
				
			||||||
            max_no: int = db.session.scalars(
 | 
					                sort_accounts_in(obj.base_code, obj.id)
 | 
				
			||||||
                sa.select(sa.func.max(Account.no))
 | 
					            sort_accounts_in(self.base_code.data, obj.id)
 | 
				
			||||||
                .filter(Account.base_code == self.base_code.data)).one()
 | 
					            count: int = Account.query\
 | 
				
			||||||
            obj.no = 1 if max_no is None else max_no + 1
 | 
					                .filter(Account.base_code == self.base_code.data).count()
 | 
				
			||||||
 | 
					            obj.base_code = self.base_code.data
 | 
				
			||||||
 | 
					            obj.no = count + 1
 | 
				
			||||||
        obj.title = self.title.data
 | 
					        obj.title = self.title.data
 | 
				
			||||||
        obj.is_offset_needed = self.is_offset_needed.data
 | 
					        if self.base_code.data[0] in {"1", "2", "3"}:
 | 
				
			||||||
 | 
					            obj.is_need_offset = self.is_need_offset.data
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            obj.is_need_offset = False
 | 
				
			||||||
        if is_new:
 | 
					        if is_new:
 | 
				
			||||||
            current_user_pk: int = get_current_user_pk()
 | 
					            current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
            obj.created_by_id = current_user_pk
 | 
					            obj.created_by_id = current_user_pk
 | 
				
			||||||
            obj.updated_by_id = current_user_pk
 | 
					            obj.updated_by_id = current_user_pk
 | 
				
			||||||
        if prev_base_code is not None \
 | 
					 | 
				
			||||||
                and prev_base_code != self.base_code.data:
 | 
					 | 
				
			||||||
            setattr(self, "__post_update",
 | 
					 | 
				
			||||||
                    lambda: sort_accounts_in(prev_base_code, obj.id))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post_update(self, obj) -> None:
 | 
					    def post_update(self, obj: Account) -> None:
 | 
				
			||||||
        """The post-processing after the update.
 | 
					        """The post-processing after the update.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The account object.
 | 
				
			||||||
        :return: None
 | 
					        :return: None
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_user_pk: int = get_current_user_pk()
 | 
					        current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
#  See the License for the specific language governing permissions and
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
"""The account query.
 | 
					"""The queries for the account management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
@@ -40,15 +40,15 @@ def get_account_query() -> list[Account]:
 | 
				
			|||||||
    conditions: list[sa.BinaryExpression] = []
 | 
					    conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
    for k in keywords:
 | 
					    for k in keywords:
 | 
				
			||||||
        l10n: list[AccountL10n] = AccountL10n.query\
 | 
					        l10n: list[AccountL10n] = AccountL10n.query\
 | 
				
			||||||
            .filter(AccountL10n.title.contains(k)).all()
 | 
					            .filter(AccountL10n.title.icontains(k)).all()
 | 
				
			||||||
        l10n_matches: set[str] = {x.account_id for x in l10n}
 | 
					        l10n_matches: set[str] = {x.account_id for x in l10n}
 | 
				
			||||||
        sub_conditions: list[sa.BinaryExpression] \
 | 
					        sub_conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
            = [Account.base_code.contains(k),
 | 
					            = [Account.base_code.contains(k),
 | 
				
			||||||
               Account.title_l10n.contains(k),
 | 
					               Account.title_l10n.icontains(k),
 | 
				
			||||||
               code.contains(k),
 | 
					               code.contains(k),
 | 
				
			||||||
               Account.id.in_(l10n_matches)]
 | 
					               Account.id.in_(l10n_matches)]
 | 
				
			||||||
        if k in gettext("Offset needed"):
 | 
					        if k in gettext("Needs Offset"):
 | 
				
			||||||
            sub_conditions.append(Account.is_offset_needed)
 | 
					            sub_conditions.append(Account.is_need_offset)
 | 
				
			||||||
        conditions.append(sa.or_(*sub_conditions))
 | 
					        conditions.append(sa.or_(*sub_conditions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Account.query.filter(*conditions)\
 | 
					    return Account.query.filter(*conditions)\
 | 
				
			||||||
@@ -19,17 +19,22 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
from urllib.parse import parse_qsl, urlencode
 | 
					from urllib.parse import parse_qsl, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import Blueprint, render_template, session, redirect, flash, \
 | 
					from flask import Blueprint, render_template, session, redirect, flash, \
 | 
				
			||||||
    url_for, request
 | 
					    url_for, request
 | 
				
			||||||
from werkzeug.datastructures import ImmutableMultiDict
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Account, BaseAccount
 | 
					from accounting.models import Account, BaseAccount
 | 
				
			||||||
from accounting.utils.next_url import inherit_next, or_next
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next, or_next
 | 
				
			||||||
from accounting.utils.pagination import Pagination
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
from accounting.utils.permission import can_view, has_permission, can_edit
 | 
					from accounting.utils.permission import can_view, has_permission, can_edit
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
from .forms import AccountForm, sort_accounts_in, AccountReorderForm
 | 
					from .forms import AccountForm, sort_accounts_in, AccountReorderForm
 | 
				
			||||||
 | 
					from .queries import get_account_query
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp: Blueprint = Blueprint("account", __name__)
 | 
					bp: Blueprint = Blueprint("account", __name__)
 | 
				
			||||||
"""The view blueprint for the account management."""
 | 
					"""The view blueprint for the account management."""
 | 
				
			||||||
@@ -42,7 +47,6 @@ def list_accounts() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The account list.
 | 
					    :return: The account list.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from .query import get_account_query
 | 
					 | 
				
			||||||
    accounts: list[BaseAccount] = get_account_query()
 | 
					    accounts: list[BaseAccount] = get_account_query()
 | 
				
			||||||
    pagination: Pagination = Pagination[BaseAccount](accounts)
 | 
					    pagination: Pagination = Pagination[BaseAccount](accounts)
 | 
				
			||||||
    return render_template("accounting/account/list.html",
 | 
					    return render_template("accounting/account/list.html",
 | 
				
			||||||
@@ -76,18 +80,15 @@ def add_account() -> redirect:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    form = AccountForm(request.form)
 | 
					    form = AccountForm(request.form)
 | 
				
			||||||
    if not form.validate():
 | 
					    if not form.validate():
 | 
				
			||||||
        for key in form.errors:
 | 
					        flash_form_errors(form)
 | 
				
			||||||
            for error in form.errors[key]:
 | 
					 | 
				
			||||||
                flash(error, "error")
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.account.create")))
 | 
					        return redirect(inherit_next(url_for("accounting.account.create")))
 | 
				
			||||||
    account: Account = Account()
 | 
					    account: Account = Account()
 | 
				
			||||||
    form.populate_obj(account)
 | 
					    form.populate_obj(account)
 | 
				
			||||||
    db.session.add(account)
 | 
					    db.session.add(account)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The account is added successfully"), "success")
 | 
					    flash(s(lazy_gettext("The account is added successfully")), "success")
 | 
				
			||||||
    return redirect(inherit_next(url_for("accounting.account.detail",
 | 
					    return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
                                         account=account)))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.get("/<account:account>", endpoint="detail")
 | 
					@bp.get("/<account:account>", endpoint="detail")
 | 
				
			||||||
@@ -131,23 +132,20 @@ def update_account(account: Account) -> redirect:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    form = AccountForm(request.form)
 | 
					    form = AccountForm(request.form)
 | 
				
			||||||
    if not form.validate():
 | 
					    if not form.validate():
 | 
				
			||||||
        for key in form.errors:
 | 
					        flash_form_errors(form)
 | 
				
			||||||
            for error in form.errors[key]:
 | 
					 | 
				
			||||||
                flash(error, "error")
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.account.edit",
 | 
					        return redirect(inherit_next(url_for("accounting.account.edit",
 | 
				
			||||||
                                             account=account)))
 | 
					                                             account=account)))
 | 
				
			||||||
    with db.session.no_autoflush:
 | 
					    with db.session.no_autoflush:
 | 
				
			||||||
        form.populate_obj(account)
 | 
					        form.populate_obj(account)
 | 
				
			||||||
    if not account.is_modified:
 | 
					    if not account.is_modified:
 | 
				
			||||||
        flash(lazy_gettext("The account was not modified."), "success")
 | 
					        flash(s(lazy_gettext("The account was not modified.")), "success")
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.account.detail",
 | 
					        return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
                                             account=account)))
 | 
					    account.updated_by_id = get_current_user_pk()
 | 
				
			||||||
    form.post_update(account)
 | 
					    account.updated_at = sa.func.now()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The account is updated successfully."), "success")
 | 
					    flash(s(lazy_gettext("The account is updated successfully.")), "success")
 | 
				
			||||||
    return redirect(inherit_next(url_for("accounting.account.detail",
 | 
					    return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
                                         account=account)))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.post("/<account:account>/delete", endpoint="delete")
 | 
					@bp.post("/<account:account>/delete", endpoint="delete")
 | 
				
			||||||
@@ -159,11 +157,14 @@ def delete_account(account: Account) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the account list on success, or the account
 | 
					    :return: The redirection to the account list on success, or the account
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not account.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The account cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(account)))
 | 
				
			||||||
    account.delete()
 | 
					    account.delete()
 | 
				
			||||||
    sort_accounts_in(account.base_code, account.id)
 | 
					    sort_accounts_in(account.base_code, account.id)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The account is deleted successfully."), "success")
 | 
					    flash(s(lazy_gettext("The account is deleted successfully.")), "success")
 | 
				
			||||||
    return redirect(or_next(url_for("accounting.account.list")))
 | 
					    return redirect(or_next(__get_list_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.get("/bases/<baseAccount:base>", endpoint="order")
 | 
					@bp.get("/bases/<baseAccount:base>", endpoint="order")
 | 
				
			||||||
@@ -189,8 +190,25 @@ def sort_accounts(base: BaseAccount) -> redirect:
 | 
				
			|||||||
    form: AccountReorderForm = AccountReorderForm(base)
 | 
					    form: AccountReorderForm = AccountReorderForm(base)
 | 
				
			||||||
    form.save_order()
 | 
					    form.save_order()
 | 
				
			||||||
    if not form.is_modified:
 | 
					    if not form.is_modified:
 | 
				
			||||||
        flash(lazy_gettext("The order was not modified."), "success")
 | 
					        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
				
			||||||
        return redirect(or_next(url_for("accounting.account.list")))
 | 
					        return redirect(or_next(__get_list_uri()))
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The order is updated successfully."), "success")
 | 
					    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
				
			||||||
    return redirect(or_next(url_for("accounting.account.list")))
 | 
					    return redirect(or_next(__get_list_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_detail_uri(account: Account) -> str:
 | 
				
			||||||
 | 
					    """Returns the detail URI of an account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :return: The detail URI of the account.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.account.detail", account=account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_list_uri() -> str:
 | 
				
			||||||
 | 
					    """Returns the account list URI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The account list URI.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.account.list")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,16 +17,15 @@
 | 
				
			|||||||
"""The console commands for the base account management.
 | 
					"""The console commands for the base account management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import click
 | 
					import click
 | 
				
			||||||
from flask.cli import with_appcontext
 | 
					from flask.cli import with_appcontext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import data_dir
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import BaseAccount, BaseAccountL10n
 | 
					from accounting.models import BaseAccount, BaseAccountL10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BaseAccountData = tuple[int, str, str, str]
 | 
					 | 
				
			||||||
"""The format of the base account data, as a list of (code, English,
 | 
					 | 
				
			||||||
Traditional Chinese, Simplified Chinese) tuples."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@click.command("accounting-init-base")
 | 
					@click.command("accounting-init-base")
 | 
				
			||||||
@with_appcontext
 | 
					@with_appcontext
 | 
				
			||||||
@@ -36,674 +35,17 @@ def init_base_accounts_command() -> None:
 | 
				
			|||||||
        click.echo("Base accounts already exist.")
 | 
					        click.echo("Base accounts already exist.")
 | 
				
			||||||
        raise click.Abort
 | 
					        raise click.Abort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.session.bulk_save_objects(
 | 
					    with open(data_dir / "base_accounts.csv") as fp:
 | 
				
			||||||
        [BaseAccount(code=str(x[0]), title_l10n=x[1]) for x in DATA])
 | 
					        data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
 | 
				
			||||||
    db.session.bulk_save_objects(
 | 
					    account_data: list[dict[str, str]] = [{"code": x["code"],
 | 
				
			||||||
        [BaseAccountL10n(account_code=x[0], locale=y[0], title=y[1])
 | 
					                                           "title_l10n": x["title"]}
 | 
				
			||||||
         for x in DATA for y in (("zh_Hant", x[2]), ("zh_Hans", x[3]))])
 | 
					                                          for x in data]
 | 
				
			||||||
 | 
					    locales: list[str] = [x[5:] for x in data[0] if x.startswith("l10n-")]
 | 
				
			||||||
 | 
					    l10n_data: list[dict[str, str]] = [{"account_code": x["code"],
 | 
				
			||||||
 | 
					                                        "locale": y,
 | 
				
			||||||
 | 
					                                        "title": x[f"l10n-{y}"]}
 | 
				
			||||||
 | 
					                                       for x in data for y in locales]
 | 
				
			||||||
 | 
					    db.session.bulk_insert_mappings(BaseAccount, account_data)
 | 
				
			||||||
 | 
					    db.session.bulk_insert_mappings(BaseAccountL10n, l10n_data)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    click.echo("Base accounts initialized.")
 | 
					    click.echo("Base accounts initialized.")
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DATA: list[BaseAccountData] = [
 | 
					 | 
				
			||||||
    (1, "assets", "資產", "资产"),
 | 
					 | 
				
			||||||
    (2, "liabilities", "負債", "负债"),
 | 
					 | 
				
			||||||
    (3, "owners’ equity", "業主權益", "业主权益"),
 | 
					 | 
				
			||||||
    (4, "operating revenue", "營業收入", "营业收入"),
 | 
					 | 
				
			||||||
    (5, "operating costs", "營業成本", "营业成本"),
 | 
					 | 
				
			||||||
    (6, "operating expenses", "營業費用", "营业费用"),
 | 
					 | 
				
			||||||
    (7, "non-operating revenue and expenses, other income (expense)",
 | 
					 | 
				
			||||||
     "營業外收入及費用", "营业外收入及费用"),
 | 
					 | 
				
			||||||
    (8, "income tax expense (or benefit)", "所得稅費用(或利益)",
 | 
					 | 
				
			||||||
     "所得税费用(或利益)"),
 | 
					 | 
				
			||||||
    (9, "nonrecurring gain or loss", "非經常營業損益", "非经常营业损益"),
 | 
					 | 
				
			||||||
    (11, "current assets", "流動資產", "流动资产"),
 | 
					 | 
				
			||||||
    (12, "current assets", "流動資產", "流动资产"),
 | 
					 | 
				
			||||||
    (13, "funds and long-term investments", "基金及長期投資", "基金及长期投资"),
 | 
					 | 
				
			||||||
    (14, "property , plant, and equipment", "固定資產", "固定资产"),
 | 
					 | 
				
			||||||
    (15, "property , plant, and equipment", "固定資產", "固定资产"),
 | 
					 | 
				
			||||||
    (16, "depletable assets", "遞耗資產", "递耗资产"),
 | 
					 | 
				
			||||||
    (17, "intangible assets", "無形資產", "无形资产"),
 | 
					 | 
				
			||||||
    (18, "other assets", "其他資產", "其他资产"),
 | 
					 | 
				
			||||||
    (21, "current liabilities", "流動負債", "流动负债"),
 | 
					 | 
				
			||||||
    (22, "current liabilities", "流動負債", "流动负债"),
 | 
					 | 
				
			||||||
    (23, "long-term liabilities", "長期負債", "长期负债"),
 | 
					 | 
				
			||||||
    (28, "other liabilities", "其他負債", "其他负债"),
 | 
					 | 
				
			||||||
    (31, "capital", "資本", "资本"),
 | 
					 | 
				
			||||||
    (32, "additional paid-in capital", "資本公積", "资本公积"),
 | 
					 | 
				
			||||||
    (33, "retained earnings (accumulated deficit)", "保留盈餘(或累積虧損)",
 | 
					 | 
				
			||||||
     "保留盈余(或累积亏损)"),
 | 
					 | 
				
			||||||
    (34, "equity adjustments", "權益調整", "权益调整"),
 | 
					 | 
				
			||||||
    (35, "treasury stock", "庫藏股", "库藏股"),
 | 
					 | 
				
			||||||
    (36, "minority interest", "少數股權", "少数股权"),
 | 
					 | 
				
			||||||
    (41, "sales revenue", "銷貨收入", "销货收入"),
 | 
					 | 
				
			||||||
    (46, "service revenue", "勞務收入", "劳务收入"),
 | 
					 | 
				
			||||||
    (47, "agency revenue", "業務收入", "业务收入"),
 | 
					 | 
				
			||||||
    (48, "other operating revenue", "其他營業收入", "其他营业收入"),
 | 
					 | 
				
			||||||
    (51, "cost of goods sold", "銷貨成本", "销货成本"),
 | 
					 | 
				
			||||||
    (56, "service costs", "勞務成本", "劳务成本"),
 | 
					 | 
				
			||||||
    (57, "agency costs", "業務成本", "业务成本"),
 | 
					 | 
				
			||||||
    (58, "other operating costs", "其他營業成本", "其他营业成本"),
 | 
					 | 
				
			||||||
    (61, "selling expenses", "推銷費用", "推销费用"),
 | 
					 | 
				
			||||||
    (62, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
 | 
					 | 
				
			||||||
    (63, "research and development expenses", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (71, "non-operating revenue", "營業外收入", "营业外收入"),
 | 
					 | 
				
			||||||
    (72, "non-operating revenue", "營業外收入", "营业外收入"),
 | 
					 | 
				
			||||||
    (73, "non-operating revenue", "營業外收入", "营业外收入"),
 | 
					 | 
				
			||||||
    (74, "non-operating revenue", "營業外收入", "营业外收入"),
 | 
					 | 
				
			||||||
    (75, "non-operating expenses", "營業外費用", "营业外费用"),
 | 
					 | 
				
			||||||
    (76, "non-operating expenses", "營業外費用", "营业外费用"),
 | 
					 | 
				
			||||||
    (77, "non-operating expenses", "營業外費用", "营业外费用"),
 | 
					 | 
				
			||||||
    (78, "non-operating expenses", "營業外費用", "营业外费用"),
 | 
					 | 
				
			||||||
    (81, "income tax expense (or benefit)", "所得稅費用(或利益)",
 | 
					 | 
				
			||||||
     "所得税费用(或利益)"),
 | 
					 | 
				
			||||||
    (91, "gain (loss) from discontinued operations", "停業部門損益",
 | 
					 | 
				
			||||||
     "停业部门损益"),
 | 
					 | 
				
			||||||
    (92, "extraordinary gain or loss", "非常損益", "非常损益"),
 | 
					 | 
				
			||||||
    (93, "cumulative effect of changes in accounting principles",
 | 
					 | 
				
			||||||
     "會計原則變動累積影響數", "会计原则变动累积影响数"),
 | 
					 | 
				
			||||||
    (94, "minority interest income", "少數股權淨利", "少数股权净利"),
 | 
					 | 
				
			||||||
    (111, "cash and cash equivalents", "現金及約當現金", "现金及约当现金"),
 | 
					 | 
				
			||||||
    (112, "short-term investments", "短期投資", "短期投资"),
 | 
					 | 
				
			||||||
    (113, "notes receivable", "應收票據", "应收票据"),
 | 
					 | 
				
			||||||
    (114, "accounts receivable", "應收帳款", "应收帐款"),
 | 
					 | 
				
			||||||
    (118, "other receivables", "其他應收款", "其他应收款"),
 | 
					 | 
				
			||||||
    (121, "inventories", "存貨", "存货"),
 | 
					 | 
				
			||||||
    (122, "inventories", "存貨", "存货"),
 | 
					 | 
				
			||||||
    (125, "prepaid expenses", "預付費用", "预付费用"),
 | 
					 | 
				
			||||||
    (126, "prepayments", "預付款項", "预付款项"),
 | 
					 | 
				
			||||||
    (128, "other current assets", "其他流動資產", "其他流动资产"),
 | 
					 | 
				
			||||||
    (129, "other current assets", "其他流動資產", "其他流动资产"),
 | 
					 | 
				
			||||||
    (131, "funds", "基金", "基金"),
 | 
					 | 
				
			||||||
    (132, "long-term investments", "長期投資", "长期投资"),
 | 
					 | 
				
			||||||
    (141, "land", "土地", "土地"),
 | 
					 | 
				
			||||||
    (142, "land improvements", "土地改良物", "土地改良物"),
 | 
					 | 
				
			||||||
    (143, "buildings", "房屋及建物", "房屋及建物"),
 | 
					 | 
				
			||||||
    (144, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
 | 
					 | 
				
			||||||
    (145, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
 | 
					 | 
				
			||||||
    (146, "machinery and equipment", "機(器)具及設備", "机(器)具及设备"),
 | 
					 | 
				
			||||||
    (151, "leased assets", "租賃資產", "租赁资产"),
 | 
					 | 
				
			||||||
    (152, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
 | 
					 | 
				
			||||||
    (156, "construction in progress and prepayments for equipment",
 | 
					 | 
				
			||||||
     "未完工程及預付購置設備款", "未完工程及预付购置设备款"),
 | 
					 | 
				
			||||||
    (158, "miscellaneous property, plant, and equipment", "雜項固定資產",
 | 
					 | 
				
			||||||
     "杂项固定资产"),
 | 
					 | 
				
			||||||
    (161, "depletable assets", "遞耗資產", "递耗资产"),
 | 
					 | 
				
			||||||
    (171, "trademarks", "商標權", "商标权"),
 | 
					 | 
				
			||||||
    (172, "patents", "專利權", "专利权"),
 | 
					 | 
				
			||||||
    (173, "franchise", "特許權", "特许权"),
 | 
					 | 
				
			||||||
    (174, "copyright", "著作權", "著作权"),
 | 
					 | 
				
			||||||
    (175, "computer software", "電腦軟體", "电脑软体"),
 | 
					 | 
				
			||||||
    (176, "goodwill", "商譽", "商誉"),
 | 
					 | 
				
			||||||
    (177, "organization costs", "開辦費", "开办费"),
 | 
					 | 
				
			||||||
    (178, "other intangibles", "其他無形資產", "其他无形资产"),
 | 
					 | 
				
			||||||
    (181, "deferred assets", "遞延資產", "递延资产"),
 | 
					 | 
				
			||||||
    (182, "idle assets", "閒置資產", "闲置资产"),
 | 
					 | 
				
			||||||
    (184, "long-term notes , accounts and overdue receivables",
 | 
					 | 
				
			||||||
     "長期應收票據及款項與催收帳款", "长期应收票据及款项与催收帐款"),
 | 
					 | 
				
			||||||
    (185, "assets leased to others", "出租資產", "出租资产"),
 | 
					 | 
				
			||||||
    (186, "refundable deposit", "存出保證金", "存出保证金"),
 | 
					 | 
				
			||||||
    (188, "miscellaneous assets", "雜項資產", "杂项资产"),
 | 
					 | 
				
			||||||
    (211, "short-term borrowings (debt)", "短期借款", "短期借款"),
 | 
					 | 
				
			||||||
    (212, "short-term notes and bills payable", "應付短期票券", "应付短期票券"),
 | 
					 | 
				
			||||||
    (213, "notes payable", "應付票據", "应付票据"),
 | 
					 | 
				
			||||||
    (214, "accounts pay able", "應付帳款", "应付帐款"),
 | 
					 | 
				
			||||||
    (216, "income taxes payable", "應付所得稅", "应付所得税"),
 | 
					 | 
				
			||||||
    (217, "accrued expenses", "應付費用", "应付费用"),
 | 
					 | 
				
			||||||
    (218, "other payables", "其他應付款", "其他应付款"),
 | 
					 | 
				
			||||||
    (219, "other payables", "其他應付款", "其他应付款"),
 | 
					 | 
				
			||||||
    (226, "advance receipts", "預收款項", "预收款项"),
 | 
					 | 
				
			||||||
    (227, "long-term liabilities -current portion",
 | 
					 | 
				
			||||||
     "一年或一營業週期內到期長期負債", "一年或一营业周期内到期长期负债"),
 | 
					 | 
				
			||||||
    (228, "other current liabilities", "其他流動負債",
 | 
					 | 
				
			||||||
     "其他流动负债"),
 | 
					 | 
				
			||||||
    (229, "other current liabilities", "其他流動負債",
 | 
					 | 
				
			||||||
     "其他流动负债"),
 | 
					 | 
				
			||||||
    (231, "corporate bonds payable", "應付公司債", "应付公司债"),
 | 
					 | 
				
			||||||
    (232, "long-term loans payable", "長期借款", "长期借款"),
 | 
					 | 
				
			||||||
    (233, "long-term notes and accounts payable", "長期應付票據及款項",
 | 
					 | 
				
			||||||
     "长期应付票据及款项"),
 | 
					 | 
				
			||||||
    (234, "accrued liabilities for land value increment tax",
 | 
					 | 
				
			||||||
     "估計應付土地增值稅", "估计应付土地增值税"),
 | 
					 | 
				
			||||||
    (235, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"),
 | 
					 | 
				
			||||||
    (238, "other long-term liabilities", "其他長期負債", "其他长期负债"),
 | 
					 | 
				
			||||||
    (281, "deferred liabilities", "遞延負債", "递延负债"),
 | 
					 | 
				
			||||||
    (286, "deposits received", "存入保證金", "存入保证金"),
 | 
					 | 
				
			||||||
    (288, "miscellaneous liabilities", "雜項負債", "杂项负债"),
 | 
					 | 
				
			||||||
    (311, "capital", "資本(或股本)", "资本(或股本)"),
 | 
					 | 
				
			||||||
    (321, "paid-in capital in excess of par", "股票溢價", "股票溢价"),
 | 
					 | 
				
			||||||
    (323, "capital surplus from assets revaluation", "資產重估增值準備",
 | 
					 | 
				
			||||||
     "资产重估增值准备"),
 | 
					 | 
				
			||||||
    (324, "capital surplus from gain on disposal of assets", "處分資產溢價公積",
 | 
					 | 
				
			||||||
     "处分资产溢价公积"),
 | 
					 | 
				
			||||||
    (325, "capital surplus from business combination", "合併公積", "合并公积"),
 | 
					 | 
				
			||||||
    (326, "donated surplus", "受贈公積", "受赠公积"),
 | 
					 | 
				
			||||||
    (328, "other additional paid-in capital", "其他資本公積", "其他资本公积"),
 | 
					 | 
				
			||||||
    (331, "legal reserve", "法定盈餘公積", "法定盈余公积"),
 | 
					 | 
				
			||||||
    (332, "special reserve", "特別盈餘公積", "特别盈余公积"),
 | 
					 | 
				
			||||||
    (335, "retained earnings-unappropriated (or accumulated deficit)",
 | 
					 | 
				
			||||||
     "未分配盈餘(或累積虧損)", "未分配盈余(或累积亏损)"),
 | 
					 | 
				
			||||||
    (341,
 | 
					 | 
				
			||||||
     "unrealized loss on market value decline of long-term equity investments",
 | 
					 | 
				
			||||||
     "長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
 | 
					 | 
				
			||||||
    (342, "cumulative translation adjustment", "累積換算調整數", "累积换算调整数"),
 | 
					 | 
				
			||||||
    (343, "net loss not recognized as pension cost", "未認列為退休金成本之淨損失",
 | 
					 | 
				
			||||||
     "未认列为退休金成本之净损失"),
 | 
					 | 
				
			||||||
    (351, "treasury stock", "庫藏股", "库藏股"),
 | 
					 | 
				
			||||||
    (361, "minority interest", "少數股權", "少数股权"),
 | 
					 | 
				
			||||||
    (411, "sales revenue", "銷貨收入", "销货收入"),
 | 
					 | 
				
			||||||
    (417, "sales return", "銷貨退回", "销货退回"),
 | 
					 | 
				
			||||||
    (419, "sales allowances", "銷貨折讓", "销货折让"),
 | 
					 | 
				
			||||||
    (461, "service revenue", "勞務收入", "劳务收入"),
 | 
					 | 
				
			||||||
    (471, "agency revenue", "業務收入", "业务收入"),
 | 
					 | 
				
			||||||
    (488, "other operating revenue", "其他營業收入—其他", "其他营业收入—其他"),
 | 
					 | 
				
			||||||
    (511, "cost of goods sold", "銷貨成本", "销货成本"),
 | 
					 | 
				
			||||||
    (512, "purchases", "進貨", "进货"),
 | 
					 | 
				
			||||||
    (513, "materials purchased", "進料", "进料"),
 | 
					 | 
				
			||||||
    (514, "direct labor", "直接人工", "直接人工"),
 | 
					 | 
				
			||||||
    (515, "manufacturing overhead", "製造費用", "制造费用"),
 | 
					 | 
				
			||||||
    (516, "manufacturing overhead", "製造費用", "制造费用"),
 | 
					 | 
				
			||||||
    (517, "manufacturing overhead", "製造費用", "制造费用"),
 | 
					 | 
				
			||||||
    (518, "manufacturing overhead", "製造費用", "制造费用"),
 | 
					 | 
				
			||||||
    (561, "service costs", "勞務成本", "劳务成本"),
 | 
					 | 
				
			||||||
    (571, "agency costs", "業務成本", "业务成本"),
 | 
					 | 
				
			||||||
    (588, "other operating costs-other", "其他營業成本—其他", "其他营业成本—其他"),
 | 
					 | 
				
			||||||
    (615, "selling expenses", "推銷費用", "推销费用"),
 | 
					 | 
				
			||||||
    (616, "selling expenses", "推銷費用", "推销费用"),
 | 
					 | 
				
			||||||
    (617, "selling expenses", "推銷費用", "推销费用"),
 | 
					 | 
				
			||||||
    (618, "selling expenses", "推銷費用", "推销费用"),
 | 
					 | 
				
			||||||
    (625, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
 | 
					 | 
				
			||||||
    (626, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
 | 
					 | 
				
			||||||
    (627, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
 | 
					 | 
				
			||||||
    (628, "general & administrative expenses", "管理及總務費用", "管理及总务费用"),
 | 
					 | 
				
			||||||
    (635, "research and development expenses", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (636, "research and development expenses", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (637, "research and development expenses", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (638, "research and development expenses", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (711, "interest revenue", "利息收入", "利息收入"),
 | 
					 | 
				
			||||||
    (712, "investment income", "投資收益", "投资收益"),
 | 
					 | 
				
			||||||
    (713, "foreign exchange gain", "兌換利益", "兑换利益"),
 | 
					 | 
				
			||||||
    (714, "gain on disposal of investments", "處分投資收益", "处分投资收益"),
 | 
					 | 
				
			||||||
    (715, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"),
 | 
					 | 
				
			||||||
    (748, "other non-operating revenue", "其他營業外收入", "其他营业外收入"),
 | 
					 | 
				
			||||||
    (751, "interest expense", "利息費用", "利息费用"),
 | 
					 | 
				
			||||||
    (752, "investment loss", "投資損失", "投资损失"),
 | 
					 | 
				
			||||||
    (753, "foreign exchange loss", "兌換損失", "兑换损失"),
 | 
					 | 
				
			||||||
    (754, "loss on disposal of investments", "處分投資損失", "处分投资损失"),
 | 
					 | 
				
			||||||
    (755, "loss on disposal of assets", "處分資產損失", "处分资产损失"),
 | 
					 | 
				
			||||||
    (788, "other non-operating expenses", "其他營業外費用", "其他营业外费用"),
 | 
					 | 
				
			||||||
    (811, "income tax expense (or benefit)", "所得稅費用(或利益)",
 | 
					 | 
				
			||||||
     "所得税费用(或利益)"),
 | 
					 | 
				
			||||||
    (911, "income (loss) from operations of discontinued segments",
 | 
					 | 
				
			||||||
     "停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
 | 
					 | 
				
			||||||
    (912, "gain (loss) from disposal of discontinued segments",
 | 
					 | 
				
			||||||
     "停業部門損益—處分損益", "停业部门损益—处分损益"),
 | 
					 | 
				
			||||||
    (921, "extraordinary gain or loss", "非常損益", "非常损益"),
 | 
					 | 
				
			||||||
    (931, "cumulative effect of changes in accounting principles",
 | 
					 | 
				
			||||||
     "會計原則變動累積影響數", "会计原则变动累积影响数"),
 | 
					 | 
				
			||||||
    (941, "minority interest income", "少數股權淨利", "少数股权净利"),
 | 
					 | 
				
			||||||
    (1111, "cash on hand", "庫存現金", "库存现金"),
 | 
					 | 
				
			||||||
    (1112, "petty cash/revolving funds", "零用金/週轉金", "零用金/周转金"),
 | 
					 | 
				
			||||||
    (1113, "cash in banks", "銀行存款", "银行存款"),
 | 
					 | 
				
			||||||
    (1116, "cash in transit", "在途現金", "在途现金"),
 | 
					 | 
				
			||||||
    (1117, "cash equivalents", "約當現金", "约当现金"),
 | 
					 | 
				
			||||||
    (1118, "other cash and cash equivalents", "其他現金及約當現金",
 | 
					 | 
				
			||||||
     "其他现金及约当现金"),
 | 
					 | 
				
			||||||
    (1121, "short-term investments – stock", "短期投資—股票", "短期投资—股票"),
 | 
					 | 
				
			||||||
    (1122, "short-term investments – short-term notes and bills",
 | 
					 | 
				
			||||||
     "短期投資—短期票券", "短期投资—短期票券"),
 | 
					 | 
				
			||||||
    (1123, "short-term investments – government bonds", "短期投資—政府債券",
 | 
					 | 
				
			||||||
     "短期投资—政府债券"),
 | 
					 | 
				
			||||||
    (1124, "short-term investments – beneficiary certificates",
 | 
					 | 
				
			||||||
     "短期投資—受益憑證", "短期投资—受益凭证"),
 | 
					 | 
				
			||||||
    (1125, "short-term investments – corporate bonds", "短期投資—公司債",
 | 
					 | 
				
			||||||
     "短期投资—公司债"),
 | 
					 | 
				
			||||||
    (1128, "short-term investments – other", "短期投資—其他", "短期投资—其他"),
 | 
					 | 
				
			||||||
    (1129, "allowance for reduction of short-term investment to market",
 | 
					 | 
				
			||||||
     "備抵短期投資跌價損失", "备抵短期投资跌价损失"),
 | 
					 | 
				
			||||||
    (1131, "notes receivable", "應收票據", "应收票据"),
 | 
					 | 
				
			||||||
    (1132, "discounted notes receivable", "應收票據貼現", "应收票据贴现"),
 | 
					 | 
				
			||||||
    (1137, "notes receivable – related parties", "應收票據—關係人",
 | 
					 | 
				
			||||||
     "应收票据—关系人"),
 | 
					 | 
				
			||||||
    (1138, "other notes receivable", "其他應收票據", "其他应收票据"),
 | 
					 | 
				
			||||||
    (1139, "allowance for uncollectible accounts – notes receivable",
 | 
					 | 
				
			||||||
     "備抵呆帳-應收票據", "备抵呆帐-应收票据"),
 | 
					 | 
				
			||||||
    (1141, "accounts receivable", "應收帳款", "应收帐款"),
 | 
					 | 
				
			||||||
    (1142, "installment accounts receivable", "應收分期帳款",
 | 
					 | 
				
			||||||
     "应收分期帐款"),
 | 
					 | 
				
			||||||
    (1147, "accounts receivable – related parties", "應收帳款—關係人",
 | 
					 | 
				
			||||||
     "应收帐款—关系人"),
 | 
					 | 
				
			||||||
    (1149, "allowance for uncollectible accounts – accounts receivable",
 | 
					 | 
				
			||||||
     "備抵呆帳-應收帳款", "备抵呆帐-应收帐款"),
 | 
					 | 
				
			||||||
    (1181, "forward exchange contract receivable", "應收出售遠匯款",
 | 
					 | 
				
			||||||
     "应收出售远汇款"),
 | 
					 | 
				
			||||||
    (1182, "forward exchange contract receivable – foreign currencies",
 | 
					 | 
				
			||||||
     "應收遠匯款—外幣", "应收远汇款—外币"),
 | 
					 | 
				
			||||||
    (1183, "discount on forward ex-change contract", "買賣遠匯折價",
 | 
					 | 
				
			||||||
     "买卖远汇折价"),
 | 
					 | 
				
			||||||
    (1184, "earned revenue receivable", "應收收益", "应收收益"),
 | 
					 | 
				
			||||||
    (1185, "income tax refund receivable", "應收退稅款", "应收退税款"),
 | 
					 | 
				
			||||||
    (1187, "other receivables – related parties", "其他應收款—關係人",
 | 
					 | 
				
			||||||
     "其他应收款—关系人"),
 | 
					 | 
				
			||||||
    (1188, "other receivables – other", "其他應收款—其他", "其他应收款—其他"),
 | 
					 | 
				
			||||||
    (1189, "allowance for uncollectible accounts – other receivables",
 | 
					 | 
				
			||||||
     "備抵呆帳—其他應收款", "备抵呆帐—其他应收款"),
 | 
					 | 
				
			||||||
    (1211, "merchandise inventory", "商品存貨", "商品存货"),
 | 
					 | 
				
			||||||
    (1212, "consigned goods", "寄銷商品", "寄销商品"),
 | 
					 | 
				
			||||||
    (1213, "goods in transit", "在途商品", "在途商品"),
 | 
					 | 
				
			||||||
    (1219, "allowance for reduction of inventory to market", "備抵存貨跌價損失",
 | 
					 | 
				
			||||||
     "备抵存货跌价损失"),
 | 
					 | 
				
			||||||
    (1221, "finished goods", "製成品", "制成品"),
 | 
					 | 
				
			||||||
    (1222, "consigned finished goods", "寄銷製成品", "寄销制成品"),
 | 
					 | 
				
			||||||
    (1223, "by-products", "副產品", "副产品"),
 | 
					 | 
				
			||||||
    (1224, "work in process", "在製品", "在制品"),
 | 
					 | 
				
			||||||
    (1225, "work in process – outsourced", "委外加工", "委外加工"),
 | 
					 | 
				
			||||||
    (1226, "raw materials", "原料", "原料"),
 | 
					 | 
				
			||||||
    (1227, "supplies", "物料", "物料"),
 | 
					 | 
				
			||||||
    (1228, "materials and supplies in transit", "在途原物料", "在途原物料"),
 | 
					 | 
				
			||||||
    (1229, "allowance for reduction of inventory to market", "備抵存貨跌價損失",
 | 
					 | 
				
			||||||
     "备抵存货跌价损失"),
 | 
					 | 
				
			||||||
    (1251, "prepaid payroll", "預付薪資", "预付薪资"),
 | 
					 | 
				
			||||||
    (1252, "prepaid rents", "預付租金", "预付租金"),
 | 
					 | 
				
			||||||
    (1253, "prepaid insurance", "預付保險費", "预付保险费"),
 | 
					 | 
				
			||||||
    (1254, "office supplies", "用品盤存", "用品盘存"),
 | 
					 | 
				
			||||||
    (1255, "prepaid income tax", "預付所得稅", "预付所得税"),
 | 
					 | 
				
			||||||
    (1258, "other prepaid expenses", "其他預付費用", "其他预付费用"),
 | 
					 | 
				
			||||||
    (1261, "prepayment for purchases", "預付貨款", "预付货款"),
 | 
					 | 
				
			||||||
    (1268, "other prepayments", "其他預付款項", "其他预付款项"),
 | 
					 | 
				
			||||||
    (1281, "VAT paid ( or input tax)", "進項稅額", "进项税额"),
 | 
					 | 
				
			||||||
    (1282, "excess VAT paid (or overpaid VAT)", "留抵稅額", "留抵税额"),
 | 
					 | 
				
			||||||
    (1283, "temporary payments", "暫付款", "暂付款"),
 | 
					 | 
				
			||||||
    (1284, "payment on behalf of others", "代付款", "代付款"),
 | 
					 | 
				
			||||||
    (1285, "advances to employees", "員工借支", "员工借支"),
 | 
					 | 
				
			||||||
    (1286, "refundable deposits", "存出保證金", "存出保证金"),
 | 
					 | 
				
			||||||
    (1287, "certificate of deposit-restricted", "受限制存款", "受限制存款"),
 | 
					 | 
				
			||||||
    (1291, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"),
 | 
					 | 
				
			||||||
    (1292, "deferred foreign exchange losses", "遞延兌換損失", "递延兑换损失"),
 | 
					 | 
				
			||||||
    (1293, "owners’ (stockholders’) current account", "業主(股東)往來",
 | 
					 | 
				
			||||||
     "业主(股东)往来"),
 | 
					 | 
				
			||||||
    (1294, "current account with others", "同業往來", "同业往来"),
 | 
					 | 
				
			||||||
    (1298, "other current assets – other", "其他流動資產—其他",
 | 
					 | 
				
			||||||
     "其他流动资产—其他"),
 | 
					 | 
				
			||||||
    (1311, "redemption fund (or sinking fund)", "償債基金", "偿债基金"),
 | 
					 | 
				
			||||||
    (1312, "fund for improvement and expansion", "改良及擴充基金",
 | 
					 | 
				
			||||||
     "改良及扩充基金"),
 | 
					 | 
				
			||||||
    (1313, "contingency fund", "意外損失準備基金", "意外损失准备基金"),
 | 
					 | 
				
			||||||
    (1314, "pension fund", "退休基金", "退休基金"),
 | 
					 | 
				
			||||||
    (1318, "other funds", "其他基金", "其他基金"),
 | 
					 | 
				
			||||||
    (1321, "long-term equity investments", "長期股權投資", "长期股权投资"),
 | 
					 | 
				
			||||||
    (1322, "long-term bond investments", "長期債券投資", "长期债券投资"),
 | 
					 | 
				
			||||||
    (1323, "long-term real estate in-vestments", "長期不動產投資",
 | 
					 | 
				
			||||||
     "长期不动产投资"),
 | 
					 | 
				
			||||||
    (1324, "cash surrender value of life insurance", "人壽保險現金解約價值",
 | 
					 | 
				
			||||||
     "人寿保险现金解约价值"),
 | 
					 | 
				
			||||||
    (1328, "other long-term investments", "其他長期投資", "其他长期投资"),
 | 
					 | 
				
			||||||
    (1329,
 | 
					 | 
				
			||||||
     "allowance for excess of cost over market value of long-term investments",
 | 
					 | 
				
			||||||
     "備抵長期投資跌價損失", "备抵长期投资跌价损失"),
 | 
					 | 
				
			||||||
    (1411, "land", "土地", "土地"),
 | 
					 | 
				
			||||||
    (1418, "land – revaluation increments", "土地—重估增值", "土地—重估增值"),
 | 
					 | 
				
			||||||
    (1421, "land improvements", "土地改良物", "土地改良物"),
 | 
					 | 
				
			||||||
    (1428, "land improvements – revaluation increments", "土地改良物—重估增值",
 | 
					 | 
				
			||||||
     "土地改良物—重估增值"),
 | 
					 | 
				
			||||||
    (1429, "accumulated depreciation – land improvements", "累積折舊—土地改良物",
 | 
					 | 
				
			||||||
     "累积折旧—土地改良物"),
 | 
					 | 
				
			||||||
    (1431, "buildings", "房屋及建物", "房屋及建物"),
 | 
					 | 
				
			||||||
    (1438, "buildings –revaluation increments", "房屋及建物—重估增值",
 | 
					 | 
				
			||||||
     "房屋及建物—重估增值"),
 | 
					 | 
				
			||||||
    (1439, "accumulated depreciation – buildings", "累積折舊—房屋及建物",
 | 
					 | 
				
			||||||
     "累积折旧—房屋及建物"),
 | 
					 | 
				
			||||||
    (1441, "machinery", "機(器)具", "机(器)具"),
 | 
					 | 
				
			||||||
    (1448, "machinery – revaluation increments", "機(器)具—重估增值",
 | 
					 | 
				
			||||||
     "机(器)具—重估增值"),
 | 
					 | 
				
			||||||
    (1449, "accumulated depreciation – machinery", "累積折舊—機(器)具",
 | 
					 | 
				
			||||||
     "累积折旧—机(器)具"),
 | 
					 | 
				
			||||||
    (1511, "leased assets", "租賃資產", "租赁资产"),
 | 
					 | 
				
			||||||
    (1519, "accumulated depreciation – leased assets", "累積折舊—租賃資產",
 | 
					 | 
				
			||||||
     "累积折旧—租赁资产"),
 | 
					 | 
				
			||||||
    (1521, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
 | 
					 | 
				
			||||||
    (1529, "accumulated depreciation – leasehold improvements",
 | 
					 | 
				
			||||||
     "累積折舊—租賃權益改良", "累积折旧—租赁权益改良"),
 | 
					 | 
				
			||||||
    (1561, "construction in progress", "未完工程", "未完工程"),
 | 
					 | 
				
			||||||
    (1562, "prepayment for equipment", "預付購置設備款", "预付购置设备款"),
 | 
					 | 
				
			||||||
    (1581, "miscellaneous property, plant, and equipment", "雜項固定資產",
 | 
					 | 
				
			||||||
     "杂项固定资产"),
 | 
					 | 
				
			||||||
    (1588,
 | 
					 | 
				
			||||||
     "miscellaneous property, plant, and equipment – revaluation increments",
 | 
					 | 
				
			||||||
     "雜項固定資產—重估增值", "杂项固定资产—重估增值"),
 | 
					 | 
				
			||||||
    (1589,
 | 
					 | 
				
			||||||
     "accumulated depreciation – miscellaneous property, plant, and equipment",
 | 
					 | 
				
			||||||
     "累積折舊—雜項固定資產", "累积折旧—杂项固定资产"),
 | 
					 | 
				
			||||||
    (1611, "natural resources", "天然資源", "天然资源"),
 | 
					 | 
				
			||||||
    (1618, "natural resources –revaluation increments", "天然資源—重估增值",
 | 
					 | 
				
			||||||
     "天然资源—重估增值"),
 | 
					 | 
				
			||||||
    (1619, "accumulated depletion – natural resources", "累積折耗—天然資源",
 | 
					 | 
				
			||||||
     "累积折耗—天然资源"),
 | 
					 | 
				
			||||||
    (1711, "trademarks", "商標權", "商标权"),
 | 
					 | 
				
			||||||
    (1721, "patents", "專利權", "专利权"),
 | 
					 | 
				
			||||||
    (1731, "franchise", "特許權", "特许权"),
 | 
					 | 
				
			||||||
    (1741, "copyright", "著作權", "著作权"),
 | 
					 | 
				
			||||||
    (1751, "computer software cost", "電腦軟體", "电脑软体"),
 | 
					 | 
				
			||||||
    (1761, "goodwill", "商譽", "商誉"),
 | 
					 | 
				
			||||||
    (1771, "organization costs", "開辦費", "开办费"),
 | 
					 | 
				
			||||||
    (1781, "deferred pension costs", "遞延退休金成本", "递延退休金成本"),
 | 
					 | 
				
			||||||
    (1782, "leasehold improvements", "租賃權益改良", "租赁权益改良"),
 | 
					 | 
				
			||||||
    (1788, "other intangible assets – other", "其他無形資產—其他",
 | 
					 | 
				
			||||||
     "其他无形资产—其他"),
 | 
					 | 
				
			||||||
    (1811, "deferred bond issuance costs", "債券發行成本", "债券发行成本"),
 | 
					 | 
				
			||||||
    (1812, "long-term prepaid rent", "長期預付租金", "长期预付租金"),
 | 
					 | 
				
			||||||
    (1813, "long-term prepaid insurance", "長期預付保險費", "长期预付保险费"),
 | 
					 | 
				
			||||||
    (1814, "deferred income tax assets", "遞延所得稅資產", "递延所得税资产"),
 | 
					 | 
				
			||||||
    (1815, "prepaid pension cost", "預付退休金", "预付退休金"),
 | 
					 | 
				
			||||||
    (1818, "other deferred assets", "其他遞延資產", "其他递延资产"),
 | 
					 | 
				
			||||||
    (1821, "idle assets", "閒置資產", "闲置资产"),
 | 
					 | 
				
			||||||
    (1841, "long-term notes receivable", "長期應收票據", "长期应收票据"),
 | 
					 | 
				
			||||||
    (1842, "long-term accounts receivable", "長期應收帳款", "长期应收帐款"),
 | 
					 | 
				
			||||||
    (1843, "overdue receivables", "催收帳款", "催收帐款"),
 | 
					 | 
				
			||||||
    (1847,
 | 
					 | 
				
			||||||
     "long-term notes, accounts and overdue receivables – related parties",
 | 
					 | 
				
			||||||
     "長期應收票據及款項與催收帳款—關係人", "长期应收票据及款项与催收帐款—关系人"),
 | 
					 | 
				
			||||||
    (1848, "other long-term receivables", "其他長期應收款項", "其他长期应收款项"),
 | 
					 | 
				
			||||||
    (1849,
 | 
					 | 
				
			||||||
     "allowance for uncollectible accounts – long-term notes, accounts and"
 | 
					 | 
				
			||||||
     " overdue receivables",
 | 
					 | 
				
			||||||
     "備抵呆帳—長期應收票據及款項與催收帳款", "备抵呆帐—长期应收票据及款项与催收帐款"),
 | 
					 | 
				
			||||||
    (1851, "assets leased to others", "出租資產", "出租资产"),
 | 
					 | 
				
			||||||
    (1858, "assets leased to others – incremental value from revaluation",
 | 
					 | 
				
			||||||
     "出租資產—重估增值", "出租资产—重估增值"),
 | 
					 | 
				
			||||||
    (1859, "accumulated depreciation – assets leased to others",
 | 
					 | 
				
			||||||
     "累積折舊—出租資產", "累积折旧—出租资产"),
 | 
					 | 
				
			||||||
    (1861, "refundable deposits", "存出保證金", "存出保证金"),
 | 
					 | 
				
			||||||
    (1881, "certificate of deposit – restricted", "受限制存款", "受限制存款"),
 | 
					 | 
				
			||||||
    (1888, "miscellaneous assets – other", "雜項資產—其他", "杂项资产—其他"),
 | 
					 | 
				
			||||||
    (2111, "bank overdraft", "銀行透支", "银行透支"),
 | 
					 | 
				
			||||||
    (2112, "bank loan", "銀行借款", "银行借款"),
 | 
					 | 
				
			||||||
    (2114, "short-term borrowings – owners", "短期借款—業主", "短期借款—业主"),
 | 
					 | 
				
			||||||
    (2115, "short-term borrowings – employees", "短期借款—員工", "短期借款—员工"),
 | 
					 | 
				
			||||||
    (2117, "short-term borrowings – related parties", "短期借款—關係人",
 | 
					 | 
				
			||||||
     "短期借款—关系人"),
 | 
					 | 
				
			||||||
    (2118, "short-term borrowings – other", "短期借款—其他", "短期借款—其他"),
 | 
					 | 
				
			||||||
    (2121, "commercial paper payable", "應付商業本票", "应付商业本票"),
 | 
					 | 
				
			||||||
    (2122, "bank acceptance", "銀行承兌匯票", "银行承兑汇票"),
 | 
					 | 
				
			||||||
    (2128, "other short-term notes and bills payable", "其他應付短期票券",
 | 
					 | 
				
			||||||
     "其他应付短期票券"),
 | 
					 | 
				
			||||||
    (2129, "discount on short-term notes and bills payable", "應付短期票券折價",
 | 
					 | 
				
			||||||
     "应付短期票券折价"),
 | 
					 | 
				
			||||||
    (2131, "notes payable", "應付票據", "应付票据"),
 | 
					 | 
				
			||||||
    (2137, "notes payable – related parties", "應付票據—關係人",
 | 
					 | 
				
			||||||
     "应付票据—关系人"),
 | 
					 | 
				
			||||||
    (2138, "other notes payable", "其他應付票據", "其他应付票据"),
 | 
					 | 
				
			||||||
    (2141, "accounts payable", "應付帳款", "应付帐款"),
 | 
					 | 
				
			||||||
    (2147, "accounts payable – related parties", "應付帳款—關係人",
 | 
					 | 
				
			||||||
     "应付帐款—关系人"),
 | 
					 | 
				
			||||||
    (2161, "income tax payable", "應付所得稅", "应付所得税"),
 | 
					 | 
				
			||||||
    (2171, "accrued payroll", "應付薪工", "应付薪工"),
 | 
					 | 
				
			||||||
    (2172, "accrued rent payable", "應付租金", "应付租金"),
 | 
					 | 
				
			||||||
    (2173, "accrued interest payable", "應付利息", "应付利息"),
 | 
					 | 
				
			||||||
    (2174, "accrued VAT payable", "應付營業稅", "应付营业税"),
 | 
					 | 
				
			||||||
    (2175, "accrued taxes payable – other", "應付稅捐—其他", "应付税捐—其他"),
 | 
					 | 
				
			||||||
    (2178, "other accrued expenses payable", "其他應付費用", "其他应付费用"),
 | 
					 | 
				
			||||||
    (2181, "forward exchange contract payable", "應付購入遠匯款", "应付购入远汇款"),
 | 
					 | 
				
			||||||
    (2182, "forward exchange contract payable – foreign currencies",
 | 
					 | 
				
			||||||
     "應付遠匯款—外幣", "应付远汇款—外币"),
 | 
					 | 
				
			||||||
    (2183, "premium on forward exchange contract", "買賣遠匯溢價", "买卖远汇溢价"),
 | 
					 | 
				
			||||||
    (2184, "payables on land and building purchased", "應付土地房屋款",
 | 
					 | 
				
			||||||
     "应付土地房屋款"),
 | 
					 | 
				
			||||||
    (2185, "Payables on equipment", "應付設備款", "应付设备款"),
 | 
					 | 
				
			||||||
    (2187, "other payables – related parties", "其他應付款—關係人",
 | 
					 | 
				
			||||||
     "其他应付款—关系人"),
 | 
					 | 
				
			||||||
    (2191, "dividend payable", "應付股利", "应付股利"),
 | 
					 | 
				
			||||||
    (2192, "bonus payable", "應付紅利", "应付红利"),
 | 
					 | 
				
			||||||
    (2193, "compensation payable to directors and supervisors", "應付董監事酬勞",
 | 
					 | 
				
			||||||
     "应付董监事酬劳"),
 | 
					 | 
				
			||||||
    (2198, "other payables – other", "其他應付款—其他", "其他应付款—其他"),
 | 
					 | 
				
			||||||
    (2261, "sales revenue received in advance", "預收貨款", "预收货款"),
 | 
					 | 
				
			||||||
    (2262, "revenue received in advance", "預收收入", "预收收入"),
 | 
					 | 
				
			||||||
    (2268, "other advance receipts", "其他預收款", "其他预收款"),
 | 
					 | 
				
			||||||
    (2271, "corporate bonds payable – current portion",
 | 
					 | 
				
			||||||
     "一年或一營業週期內到期公司債", "一年或一营业周期内到期公司债"),
 | 
					 | 
				
			||||||
    (2272, "long-term loans payable – current portion",
 | 
					 | 
				
			||||||
     "一年或一營業週期內到期長期借款", "一年或一营业周期内到期长期借款"),
 | 
					 | 
				
			||||||
    (2273,
 | 
					 | 
				
			||||||
     "long-term notes and accounts payable due within one year or one"
 | 
					 | 
				
			||||||
     " operating cycle",
 | 
					 | 
				
			||||||
     "一年或一營業週期內到期長期應付票據及款項",
 | 
					 | 
				
			||||||
     "一年或一营业周期内到期长期应付票据及款项"),
 | 
					 | 
				
			||||||
    (2277,
 | 
					 | 
				
			||||||
     "long-term notes and accounts payables to related parties – current"
 | 
					 | 
				
			||||||
     " portion",
 | 
					 | 
				
			||||||
     "一年或一營業週期內到期長期應付票據及款項—關係人",
 | 
					 | 
				
			||||||
     "一年或一营业周期内到期长期应付票据及款项—关系人"),
 | 
					 | 
				
			||||||
    (2278, "other long-term liabilities – current portion",
 | 
					 | 
				
			||||||
     "其他一年或一營業週期內到期長期負債", "其他一年或一营业周期内到期长期负债"),
 | 
					 | 
				
			||||||
    (2281, "VAT received (or output tax)", "銷項稅額", "销项税额"),
 | 
					 | 
				
			||||||
    (2283, "temporary receipts", "暫收款", "暂收款"),
 | 
					 | 
				
			||||||
    (2284, "receipts under custody", "代收款", "代收款"),
 | 
					 | 
				
			||||||
    (2285, "estimated warranty liabilities", "估計售後服務/保固負債",
 | 
					 | 
				
			||||||
     "估计售后服务/保固负债"),
 | 
					 | 
				
			||||||
    (2291, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"),
 | 
					 | 
				
			||||||
    (2292, "deferred foreign exchange gain", "遞延兌換利益", "递延兑换利益"),
 | 
					 | 
				
			||||||
    (2293, "owners’ current account", "業主(股東)往來", "业主(股东)往来"),
 | 
					 | 
				
			||||||
    (2294, "current account with others", "同業往來", "同业往来"),
 | 
					 | 
				
			||||||
    (2298, "other current liabilities – others", "其他流動負債—其他",
 | 
					 | 
				
			||||||
     "其他流动负债—其他"),
 | 
					 | 
				
			||||||
    (2311, "corporate bonds payable", "應付公司債", "应付公司债"),
 | 
					 | 
				
			||||||
    (2319, "premium (discount) on corporate bonds payable",
 | 
					 | 
				
			||||||
     "應付公司債溢(折)價", "应付公司债溢(折)价"),
 | 
					 | 
				
			||||||
    (2321, "long-term loans payable – bank", "長期銀行借款", "长期银行借款"),
 | 
					 | 
				
			||||||
    (2324, "long-term loans payable – owners", "長期借款—業主", "长期借款—业主"),
 | 
					 | 
				
			||||||
    (2325, "long-term loans payable – employees", "長期借款—員工",
 | 
					 | 
				
			||||||
     "长期借款—员工"),
 | 
					 | 
				
			||||||
    (2327, "long-term loans payable – related parties", "長期借款—關係人",
 | 
					 | 
				
			||||||
     "长期借款—关系人"),
 | 
					 | 
				
			||||||
    (2328, "long-term loans payable – other", "長期借款—其他", "长期借款—其他"),
 | 
					 | 
				
			||||||
    (2331, "long-term notes payable", "長期應付票據", "长期应付票据"),
 | 
					 | 
				
			||||||
    (2332, "long-term accounts pay-able", "長期應付帳款", "长期应付帐款"),
 | 
					 | 
				
			||||||
    (2333, "long-term capital lease liabilities", "長期應付租賃負債",
 | 
					 | 
				
			||||||
     "长期应付租赁负债"),
 | 
					 | 
				
			||||||
    (2337, "Long-term notes and accounts payable – related parties",
 | 
					 | 
				
			||||||
     "長期應付票據及款項—關係人", "长期应付票据及款项—关系人"),
 | 
					 | 
				
			||||||
    (2338, "other long-term payables", "其他長期應付款項", "其他长期应付款项"),
 | 
					 | 
				
			||||||
    (2341, "estimated accrued land value incremental tax pay-able",
 | 
					 | 
				
			||||||
     "估計應付土地增值稅", "估计应付土地增值税"),
 | 
					 | 
				
			||||||
    (2351, "accrued pension liabilities", "應計退休金負債", "应计退休金负债"),
 | 
					 | 
				
			||||||
    (2388, "other long-term liabilities – other", "其他長期負債—其他",
 | 
					 | 
				
			||||||
     "其他长期负债—其他"),
 | 
					 | 
				
			||||||
    (2811, "deferred revenue", "遞延收入", "递延收入"),
 | 
					 | 
				
			||||||
    (2814, "deferred income tax liabilities", "遞延所得稅負債", "递延所得税负债"),
 | 
					 | 
				
			||||||
    (2818, "other deferred liabilities", "其他遞延負債", "其他递延负债"),
 | 
					 | 
				
			||||||
    (2861, "guarantee deposit received", "存入保證金", "存入保证金"),
 | 
					 | 
				
			||||||
    (2888, "miscellaneous liabilities – other", "雜項負債—其他", "杂项负债—其他"),
 | 
					 | 
				
			||||||
    (3111, "capital – common stock", "普通股股本", "普通股股本"),
 | 
					 | 
				
			||||||
    (3112, "capital – preferred stock", "特別股股本", "特别股股本"),
 | 
					 | 
				
			||||||
    (3113, "capital collected in advance", "預收股本", "预收股本"),
 | 
					 | 
				
			||||||
    (3114, "stock dividends to be distributed", "待分配股票股利",
 | 
					 | 
				
			||||||
     "待分配股票股利"),
 | 
					 | 
				
			||||||
    (3115, "capital", "資本", "资本"),
 | 
					 | 
				
			||||||
    (3211, "paid-in capital in excess of par- common stock", "普通股股票溢價",
 | 
					 | 
				
			||||||
     "普通股股票溢价"),
 | 
					 | 
				
			||||||
    (3212, "paid-in capital in excess of par- preferred stock", "特別股股票溢價",
 | 
					 | 
				
			||||||
     "特别股股票溢价"),
 | 
					 | 
				
			||||||
    (3231, "capital surplus from assets revaluation", "資產重估增值準備",
 | 
					 | 
				
			||||||
     "资产重估增值准备"),
 | 
					 | 
				
			||||||
    (3241, "capital surplus from gain on disposal of assets", "處分資產溢價公積",
 | 
					 | 
				
			||||||
     "处分资产溢价公积"),
 | 
					 | 
				
			||||||
    (3251, "capital surplus from business combination", "合併公積", "合并公积"),
 | 
					 | 
				
			||||||
    (3261, "donated surplus", "受贈公積", "受赠公积"),
 | 
					 | 
				
			||||||
    (3281, "additional paid-in capital from investee under equity method",
 | 
					 | 
				
			||||||
     "權益法長期股權投資資本公積", "权益法长期股权投资资本公积"),
 | 
					 | 
				
			||||||
    (3282, "additional paid-in capital – treasury stock trans-actions",
 | 
					 | 
				
			||||||
     "資本公積—庫藏股票交易", "资本公积—库藏股票交易"),
 | 
					 | 
				
			||||||
    (3311, "legal reserve", "法定盈餘公積", "法定盈余公积"),
 | 
					 | 
				
			||||||
    (3321, "contingency reserve", "意外損失準備", "意外损失准备"),
 | 
					 | 
				
			||||||
    (3322, "improvement and expansion reserve", "改良擴充準備", "改良扩充准备"),
 | 
					 | 
				
			||||||
    (3323, "special reserve for redemption of liabilities", "償債準備",
 | 
					 | 
				
			||||||
     "偿债准备"),
 | 
					 | 
				
			||||||
    (3328, "other special reserve", "其他特別盈餘公積", "其他特别盈余公积"),
 | 
					 | 
				
			||||||
    (3351, "accumulated profit or loss", "累積盈虧", "累积盈亏"),
 | 
					 | 
				
			||||||
    (3352, "prior period adjustments", "前期損益調整", "前期损益调整"),
 | 
					 | 
				
			||||||
    (3353, "net income or loss for current period", "本期損益", "本期损益"),
 | 
					 | 
				
			||||||
    (3411,
 | 
					 | 
				
			||||||
     "unrealized loss on market value decline of long-term equity investments",
 | 
					 | 
				
			||||||
     "長期股權投資未實現跌價損失", "长期股权投资未实现跌价损失"),
 | 
					 | 
				
			||||||
    (3421, "cumulative translation adjustments", "累積換算調整數",
 | 
					 | 
				
			||||||
     "累积换算调整数"),
 | 
					 | 
				
			||||||
    (3431, "net loss not recognized as pension costs",
 | 
					 | 
				
			||||||
     "未認列為退休金成本之淨損失", "未认列为退休金成本之净损失"),
 | 
					 | 
				
			||||||
    (3511, "treasury stock", "庫藏股", "库藏股"),
 | 
					 | 
				
			||||||
    (3611, "minority interest", "少數股權", "少数股权"),
 | 
					 | 
				
			||||||
    (4111, "sales revenue", "銷貨收入", "销货收入"),
 | 
					 | 
				
			||||||
    (4112, "installment sales revenue", "分期付款銷貨收入", "分期付款销货收入"),
 | 
					 | 
				
			||||||
    (4171, "sales return", "銷貨退回", "销货退回"),
 | 
					 | 
				
			||||||
    (4191, "sales discounts and allowances", "銷貨折讓", "销货折让"),
 | 
					 | 
				
			||||||
    (4611, "service revenue", "勞務收入", "劳务收入"),
 | 
					 | 
				
			||||||
    (4711, "agency revenue", "業務收入", "业务收入"),
 | 
					 | 
				
			||||||
    (4888, "other operating revenue – other", "其他營業收入—其他",
 | 
					 | 
				
			||||||
     "其他营业收入—其他"),
 | 
					 | 
				
			||||||
    (5111, "cost of goods sold", "銷貨成本", "销货成本"),
 | 
					 | 
				
			||||||
    (5112, "installment cost of goods sold", "分期付款銷貨成本",
 | 
					 | 
				
			||||||
     "分期付款销货成本"),
 | 
					 | 
				
			||||||
    (5121, "purchases", "進貨", "进货"),
 | 
					 | 
				
			||||||
    (5122, "purchase expenses", "進貨費用", "进货费用"),
 | 
					 | 
				
			||||||
    (5123, "purchase returns", "進貨退出", "进货退出"),
 | 
					 | 
				
			||||||
    (5124, "charges on purchased merchandise", "進貨折讓", "进货折让"),
 | 
					 | 
				
			||||||
    (5131, "material purchased", "進料", "进料"),
 | 
					 | 
				
			||||||
    (5132, "charges on purchased material", "進料費用", "进料费用"),
 | 
					 | 
				
			||||||
    (5133, "material purchase returns", "進料退出", "进料退出"),
 | 
					 | 
				
			||||||
    (5134, "material purchase allowances", "進料折讓", "进料折让"),
 | 
					 | 
				
			||||||
    (5141, "direct labor", "直接人工", "直接人工"),
 | 
					 | 
				
			||||||
    (5151, "indirect labor", "間接人工", "间接人工"),
 | 
					 | 
				
			||||||
    (5152, "rent expense, rent", "租金支出", "租金支出"),
 | 
					 | 
				
			||||||
    (5153, "office supplies (expense)", "文具用品", "文具用品"),
 | 
					 | 
				
			||||||
    (5154, "travelling expense, travel", "旅費", "旅费"),
 | 
					 | 
				
			||||||
    (5155, "shipping expenses, freight", "運費", "运费"),
 | 
					 | 
				
			||||||
    (5156, "postage (expenses)", "郵電費", "邮电费"),
 | 
					 | 
				
			||||||
    (5157, "repair (s) and maintenance (expense )", "修繕費", "修缮费"),
 | 
					 | 
				
			||||||
    (5158, "packing expenses", "包裝費", "包装费"),
 | 
					 | 
				
			||||||
    (5161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
 | 
					 | 
				
			||||||
    (5162, "insurance (expense)", "保險費", "保险费"),
 | 
					 | 
				
			||||||
    (5163, "manufacturing overhead – outsourced", "加工費", "加工费"),
 | 
					 | 
				
			||||||
    (5166, "taxes", "稅捐", "税捐"),
 | 
					 | 
				
			||||||
    (5168, "depreciation expense", "折舊", "折旧"),
 | 
					 | 
				
			||||||
    (5169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
 | 
					 | 
				
			||||||
    (5172, "meal (expenses)", "伙食費", "伙食费"),
 | 
					 | 
				
			||||||
    (5173, "employee benefits/welfare", "職工福利", "职工福利"),
 | 
					 | 
				
			||||||
    (5176, "training (expense)", "訓練費", "训练费"),
 | 
					 | 
				
			||||||
    (5177, "indirect materials", "間接材料", "间接材料"),
 | 
					 | 
				
			||||||
    (5188, "other manufacturing expenses", "其他製造費用", "其他制造费用"),
 | 
					 | 
				
			||||||
    (5611, "service costs", "勞務成本", "劳务成本"),
 | 
					 | 
				
			||||||
    (5711, "agency costs", "業務成本", "业务成本"),
 | 
					 | 
				
			||||||
    (5888, "other operating costs – other", "其他營業成本—其他",
 | 
					 | 
				
			||||||
     "其他营业成本—其他"),
 | 
					 | 
				
			||||||
    (6151, "payroll expense", "薪資支出", "薪资支出"),
 | 
					 | 
				
			||||||
    (6152, "rent expense, rent", "租金支出", "租金支出"),
 | 
					 | 
				
			||||||
    (6153, "office supplies (expense)", "文具用品", "文具用品"),
 | 
					 | 
				
			||||||
    (6154, "travelling expense, travel", "旅費", "旅费"),
 | 
					 | 
				
			||||||
    (6155, "shipping expenses, freight", "運費", "运费"),
 | 
					 | 
				
			||||||
    (6156, "postage (expenses)", "郵電費", "邮电费"),
 | 
					 | 
				
			||||||
    (6157, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
 | 
					 | 
				
			||||||
    (6159, "advertisement expense, advertisement", "廣告費", "广告费"),
 | 
					 | 
				
			||||||
    (6161, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
 | 
					 | 
				
			||||||
    (6162, "insurance (expense)", "保險費", "保险费"),
 | 
					 | 
				
			||||||
    (6164, "entertainment (expense)", "交際費", "交际费"),
 | 
					 | 
				
			||||||
    (6165, "donation (expense)", "捐贈", "捐赠"),
 | 
					 | 
				
			||||||
    (6166, "taxes", "稅捐", "税捐"),
 | 
					 | 
				
			||||||
    (6167, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
 | 
					 | 
				
			||||||
    (6168, "depreciation expense", "折舊", "折旧"),
 | 
					 | 
				
			||||||
    (6169, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
 | 
					 | 
				
			||||||
    (6172, "meal (expenses)", "伙食費", "伙食费"),
 | 
					 | 
				
			||||||
    (6173, "employee benefits/welfare", "職工福利", "职工福利"),
 | 
					 | 
				
			||||||
    (6175, "commission (expense)", "佣金支出", "佣金支出"),
 | 
					 | 
				
			||||||
    (6176, "training (expense)", "訓練費", "训练费"),
 | 
					 | 
				
			||||||
    (6188, "other selling expenses", "其他推銷費用", "其他推销费用"),
 | 
					 | 
				
			||||||
    (6251, "payroll expense", "薪資支出", "薪资支出"),
 | 
					 | 
				
			||||||
    (6252, "rent expense, rent", "租金支出", "租金支出"),
 | 
					 | 
				
			||||||
    (6253, "office supplies", "文具用品", "文具用品"),
 | 
					 | 
				
			||||||
    (6254, "travelling expense, travel", "旅費", "旅费"),
 | 
					 | 
				
			||||||
    (6255, "shipping expenses,freight", "運費", "运费"),
 | 
					 | 
				
			||||||
    (6256, "postage (expenses)", "郵電費", "邮电费"),
 | 
					 | 
				
			||||||
    (6257, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
 | 
					 | 
				
			||||||
    (6259, "advertisement expense, advertisement", "廣告費", "广告费"),
 | 
					 | 
				
			||||||
    (6261, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
 | 
					 | 
				
			||||||
    (6262, "insurance (expense)", "保險費", "保险费"),
 | 
					 | 
				
			||||||
    (6264, "entertainment (expense)", "交際費", "交际费"),
 | 
					 | 
				
			||||||
    (6265, "donation (expense)", "捐贈", "捐赠"),
 | 
					 | 
				
			||||||
    (6266, "taxes", "稅捐", "税捐"),
 | 
					 | 
				
			||||||
    (6267, "loss on uncollectible accounts", "呆帳損失", "呆帐损失"),
 | 
					 | 
				
			||||||
    (6268, "depreciation expense", "折舊", "折旧"),
 | 
					 | 
				
			||||||
    (6269, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
 | 
					 | 
				
			||||||
    (6271, "loss on export sales", "外銷損失", "外销损失"),
 | 
					 | 
				
			||||||
    (6272, "meal (expenses)", "伙食費", "伙食费"),
 | 
					 | 
				
			||||||
    (6273, "employee benefits/welfare", "職工福利", "职工福利"),
 | 
					 | 
				
			||||||
    (6274, "research and development expense", "研究發展費用", "研究发展费用"),
 | 
					 | 
				
			||||||
    (6275, "commission (expense)", "佣金支出", "佣金支出"),
 | 
					 | 
				
			||||||
    (6276, "training (expense)", "訓練費", "训练费"),
 | 
					 | 
				
			||||||
    (6278, "professional service fees", "勞務費", "劳务费"),
 | 
					 | 
				
			||||||
    (6288, "other general and administrative expenses", "其他管理及總務費用",
 | 
					 | 
				
			||||||
     "其他管理及总务费用"),
 | 
					 | 
				
			||||||
    (6351, "payroll expense", "薪資支出", "薪资支出"),
 | 
					 | 
				
			||||||
    (6352, "rent expense, rent", "租金支出", "租金支出"),
 | 
					 | 
				
			||||||
    (6353, "office supplies", "文具用品", "文具用品"),
 | 
					 | 
				
			||||||
    (6354, "travelling expense, travel", "旅費", "旅费"),
 | 
					 | 
				
			||||||
    (6355, "shipping expenses, freight", "運費", "运费"),
 | 
					 | 
				
			||||||
    (6356, "postage (expenses)", "郵電費", "邮电费"),
 | 
					 | 
				
			||||||
    (6357, "repair (s) and maintenance (expense)", "修繕費", "修缮费"),
 | 
					 | 
				
			||||||
    (6361, "utilities (expense)", "水電瓦斯費", "水电瓦斯费"),
 | 
					 | 
				
			||||||
    (6362, "insurance (expense)", "保險費", "保险费"),
 | 
					 | 
				
			||||||
    (6364, "entertainment (expense)", "交際費", "交际费"),
 | 
					 | 
				
			||||||
    (6366, "taxes", "稅捐", "税捐"),
 | 
					 | 
				
			||||||
    (6368, "depreciation expense", "折舊", "折旧"),
 | 
					 | 
				
			||||||
    (6369, "various amortization", "各項耗竭及攤提", "各项耗竭及摊提"),
 | 
					 | 
				
			||||||
    (6372, "meal (expenses)", "伙食費", "伙食费"),
 | 
					 | 
				
			||||||
    (6373, "employee benefits/welfare", "職工福利", "职工福利"),
 | 
					 | 
				
			||||||
    (6376, "training (expense)", "訓練費", "训练费"),
 | 
					 | 
				
			||||||
    (6378, "other research and development expenses", "其他研究發展費用",
 | 
					 | 
				
			||||||
     "其他研究发展费用"),
 | 
					 | 
				
			||||||
    (7111, "interest revenue/income", "利息收入", "利息收入"),
 | 
					 | 
				
			||||||
    (7121, "investment income recognized under equity method",
 | 
					 | 
				
			||||||
     "權益法認列之投資收益", "权益法认列之投资收益"),
 | 
					 | 
				
			||||||
    (7122, "dividends income", "股利收入", "股利收入"),
 | 
					 | 
				
			||||||
    (7123, "gain on market price recovery of short-term investment",
 | 
					 | 
				
			||||||
     "短期投資市價回升利益", "短期投资市价回升利益"),
 | 
					 | 
				
			||||||
    (7131, "foreign exchange gain", "兌換利益", "兑换利益"),
 | 
					 | 
				
			||||||
    (7141, "gain on disposal of investments", "處分投資收益", "处分投资收益"),
 | 
					 | 
				
			||||||
    (7151, "gain on disposal of assets", "處分資產溢價收入", "处分资产溢价收入"),
 | 
					 | 
				
			||||||
    (7481, "donation income", "捐贈收入", "捐赠收入"),
 | 
					 | 
				
			||||||
    (7482, "rent revenue/income", "租金收入", "租金收入"),
 | 
					 | 
				
			||||||
    (7483, "commission revenue/income", "佣金收入", "佣金收入"),
 | 
					 | 
				
			||||||
    (7484, "revenue from sale of scraps", "出售下腳及廢料收入",
 | 
					 | 
				
			||||||
     "出售下脚及废料收入"),
 | 
					 | 
				
			||||||
    (7485, "gain on physical inventory", "存貨盤盈", "存货盘盈"),
 | 
					 | 
				
			||||||
    (7486, "gain from price recovery of inventory", "存貨跌價回升利益",
 | 
					 | 
				
			||||||
     "存货跌价回升利益"),
 | 
					 | 
				
			||||||
    (7487, "gain on reversal of bad debts", "壞帳轉回利益", "坏帐转回利益"),
 | 
					 | 
				
			||||||
    (7488, "other non-operating revenue – other items", "其他營業外收入—其他",
 | 
					 | 
				
			||||||
     "其他营业外收入—其他"),
 | 
					 | 
				
			||||||
    (7511, "interest expense", "利息費用", "利息费用"),
 | 
					 | 
				
			||||||
    (7521, "investment loss recognized under equity method",
 | 
					 | 
				
			||||||
     "權益法認列之投資損失", "权益法认列之投资损失"),
 | 
					 | 
				
			||||||
    (7523, "unrealized loss on reduction of short-term investments to market",
 | 
					 | 
				
			||||||
     "短期投資未實現跌價損失", "短期投资未实现跌价损失"),
 | 
					 | 
				
			||||||
    (7531, "foreign exchange loss", "兌換損失", "兑换损失"),
 | 
					 | 
				
			||||||
    (7541, "loss on disposal of investments", "處分投資損失", "处分投资损失"),
 | 
					 | 
				
			||||||
    (7551, "loss on disposal of assets", "處分資產損失", "处分资产损失"),
 | 
					 | 
				
			||||||
    (7881, "loss on work stoppages", "停工損失", "停工损失"),
 | 
					 | 
				
			||||||
    (7882, "casualty loss", "災害損失", "灾害损失"),
 | 
					 | 
				
			||||||
    (7885, "loss on physical inventory", "存貨盤損", "存货盘损"),
 | 
					 | 
				
			||||||
    (7886,
 | 
					 | 
				
			||||||
     "loss for market price decline and obsolete and slow-moving inventories",
 | 
					 | 
				
			||||||
     "存貨跌價及呆滯損失", "存货跌价及呆滞损失"),
 | 
					 | 
				
			||||||
    (7888, "other non-operating expenses – other", "其他營業外費用—其他",
 | 
					 | 
				
			||||||
     "其他营业外费用—其他"),
 | 
					 | 
				
			||||||
    (8111, "income tax expense ( or benefit)", "所得稅費用(或利益)",
 | 
					 | 
				
			||||||
     "所得税费用(或利益)"),
 | 
					 | 
				
			||||||
    (9111, "income (loss) from operations of discontinued segment",
 | 
					 | 
				
			||||||
     "停業部門損益—停業前營業損益", "停业部门损益—停业前营业损益"),
 | 
					 | 
				
			||||||
    (9121, "gain (loss) from disposal of discontinued segment",
 | 
					 | 
				
			||||||
     "停業部門損益—處分損益", "停业部门损益—处分损益"),
 | 
					 | 
				
			||||||
    (9211, "extraordinary gain or loss", "非常損益", "非常损益"),
 | 
					 | 
				
			||||||
    (9311, "cumulative effect of changes in accounting principles",
 | 
					 | 
				
			||||||
     "會計原則變動累積影響數", "会计原则变动累积影响数"),
 | 
					 | 
				
			||||||
    (9411, "minority interest income", "少數股權淨利", "少数股权净利"),
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
"""The base account data."""
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@
 | 
				
			|||||||
from flask import abort
 | 
					from flask import abort
 | 
				
			||||||
from werkzeug.routing import BaseConverter
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import BaseAccount
 | 
					from accounting.models import BaseAccount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
#  See the License for the specific language governing permissions and
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
"""The base account query.
 | 
					"""The queries for the base account management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
@@ -35,10 +35,10 @@ def get_base_account_query() -> list[BaseAccount]:
 | 
				
			|||||||
    conditions: list[sa.BinaryExpression] = []
 | 
					    conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
    for k in keywords:
 | 
					    for k in keywords:
 | 
				
			||||||
        l10n: list[BaseAccountL10n] = BaseAccountL10n.query\
 | 
					        l10n: list[BaseAccountL10n] = BaseAccountL10n.query\
 | 
				
			||||||
            .filter(BaseAccountL10n.title.contains(k)).all()
 | 
					            .filter(BaseAccountL10n.title.icontains(k)).all()
 | 
				
			||||||
        l10n_matches: set[str] = {x.account_code for x in l10n}
 | 
					        l10n_matches: set[str] = {x.account_code for x in l10n}
 | 
				
			||||||
        conditions.append(sa.or_(BaseAccount.code.contains(k),
 | 
					        conditions.append(sa.or_(BaseAccount.code.contains(k),
 | 
				
			||||||
                                 BaseAccount.title_l10n.contains(k),
 | 
					                                 BaseAccount.title_l10n.icontains(k),
 | 
				
			||||||
                                 BaseAccount.code.in_(l10n_matches)))
 | 
					                                 BaseAccount.code.in_(l10n_matches)))
 | 
				
			||||||
    return BaseAccount.query.filter(*conditions)\
 | 
					    return BaseAccount.query.filter(*conditions)\
 | 
				
			||||||
        .order_by(BaseAccount.code).all()
 | 
					        .order_by(BaseAccount.code).all()
 | 
				
			||||||
@@ -34,7 +34,7 @@ def list_accounts() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The account list.
 | 
					    :return: The account list.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from .query import get_base_account_query
 | 
					    from .queries import get_base_account_query
 | 
				
			||||||
    accounts: list[BaseAccount] = get_base_account_query()
 | 
					    accounts: list[BaseAccount] = get_base_account_query()
 | 
				
			||||||
    pagination: Pagination = Pagination[BaseAccount](accounts)
 | 
					    pagination: Pagination = Pagination[BaseAccount](accounts)
 | 
				
			||||||
    return render_template("accounting/base-account/list.html",
 | 
					    return render_template("accounting/base-account/list.html",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,12 +17,14 @@
 | 
				
			|||||||
"""The console commands for the currency management.
 | 
					"""The console commands for the currency management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import click
 | 
					import click
 | 
				
			||||||
from flask.cli import with_appcontext
 | 
					from flask.cli import with_appcontext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db, data_dir
 | 
				
			||||||
from accounting.models import Currency, CurrencyL10n
 | 
					from accounting.models import Currency, CurrencyL10n
 | 
				
			||||||
from accounting.utils.user import has_user, get_user_pk
 | 
					from accounting.utils.user import has_user, get_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,25 +56,29 @@ def __validate_username(ctx: click.core.Context, param: click.core.Option,
 | 
				
			|||||||
@with_appcontext
 | 
					@with_appcontext
 | 
				
			||||||
def init_currencies_command(username: str) -> None:
 | 
					def init_currencies_command(username: str) -> None:
 | 
				
			||||||
    """Initializes the currencies."""
 | 
					    """Initializes the currencies."""
 | 
				
			||||||
    data: list[CurrencyData] = [
 | 
					    existing_codes: set[str] = {x.code for x in Currency.query.all()}
 | 
				
			||||||
        ("TWD", "New Taiwan dollar", "新臺幣", "新台币"),
 | 
					
 | 
				
			||||||
        ("USD", "United States dollar", "美元", "美元"),
 | 
					    with open(data_dir / "currencies.csv") as fp:
 | 
				
			||||||
    ]
 | 
					        data: list[dict[str, str]] = [x for x in csv.DictReader(fp)]
 | 
				
			||||||
    creator_pk: int = get_user_pk(username)
 | 
					    to_add: list[dict[str, str]] = [x for x in data
 | 
				
			||||||
    existing: list[Currency] = Currency.query.all()
 | 
					                                    if x["code"] not in existing_codes]
 | 
				
			||||||
    existing_code: set[str] = {x.code for x in existing}
 | 
					 | 
				
			||||||
    to_add: list[CurrencyData] = [x for x in data if x[0] not in existing_code]
 | 
					 | 
				
			||||||
    if len(to_add) == 0:
 | 
					    if len(to_add) == 0:
 | 
				
			||||||
        click.echo("No more currency to add.")
 | 
					        click.echo("No more currency to add.")
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.session.bulk_save_objects(
 | 
					    creator_pk: int = get_user_pk(username)
 | 
				
			||||||
        [Currency(code=x[0], name_l10n=x[1],
 | 
					    currency_data: list[dict[str, t.Any]] = [{"code": x["code"],
 | 
				
			||||||
                  created_by_id=creator_pk, updated_by_id=creator_pk)
 | 
					                                              "name_l10n": x["name"],
 | 
				
			||||||
         for x in data])
 | 
					                                              "created_by_id": creator_pk,
 | 
				
			||||||
    db.session.bulk_save_objects(
 | 
					                                              "updated_by_id": creator_pk}
 | 
				
			||||||
        [CurrencyL10n(currency_code=x[0], locale=y[0], name=y[1])
 | 
					                                             for x in to_add]
 | 
				
			||||||
         for x in data for y in (("zh_Hant", x[2]), ("zh_Hans", x[3]))])
 | 
					    locales: list[str] = [x[5:] for x in to_add[0] if x.startswith("l10n-")]
 | 
				
			||||||
 | 
					    l10n_data: list[dict[str, str]] = [{"currency_code": x["code"],
 | 
				
			||||||
 | 
					                                        "locale": y,
 | 
				
			||||||
 | 
					                                        "name": x[f"l10n-{y}"]}
 | 
				
			||||||
 | 
					                                       for x in to_add for y in locales]
 | 
				
			||||||
 | 
					    db.session.bulk_insert_mappings(Currency, currency_data)
 | 
				
			||||||
 | 
					    db.session.bulk_insert_mappings(CurrencyL10n, l10n_data)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    click.echo(F"{len(to_add)} added.  Currencies initialized.")
 | 
					    click.echo(F"{len(to_add)} added.  Currencies initialized.")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@
 | 
				
			|||||||
from flask import abort
 | 
					from flask import abort
 | 
				
			||||||
from werkzeug.routing import BaseConverter
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.models import Currency
 | 
					from accounting.models import Currency
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,36 +17,35 @@
 | 
				
			|||||||
"""The forms for the currency management.
 | 
					"""The forms for the currency management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from __future__ import annotations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import sqlalchemy as sa
 | 
					 | 
				
			||||||
from flask_wtf import FlaskForm
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
from wtforms import StringField, ValidationError
 | 
					from wtforms import StringField, ValidationError
 | 
				
			||||||
from wtforms.validators import DataRequired, Regexp, NoneOf
 | 
					from wtforms.validators import DataRequired, Regexp, NoneOf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Currency
 | 
					from accounting.models import Currency
 | 
				
			||||||
from accounting.utils.strip_text import strip_text
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
from accounting.utils.user import get_current_user_pk
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CodeUnique:
 | 
				
			||||||
 | 
					    """The validator to check if the code is unique."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, CurrencyForm)
 | 
				
			||||||
 | 
					        if field.data == "":
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if form.obj_code is not None and form.obj_code == field.data:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if db.session.get(Currency, field.data) is not None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "Code conflicts with another currency."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CurrencyForm(FlaskForm):
 | 
					class CurrencyForm(FlaskForm):
 | 
				
			||||||
    """The form to create or edit a currency."""
 | 
					    """The form to create or edit a currency."""
 | 
				
			||||||
    CODE_BLOCKLIST: list[str] = ["create", "store", "exists-code"]
 | 
					    CODE_BLOCKLIST: list[str] = ["create", "store", "exists-code"]
 | 
				
			||||||
    """The reserved codes that are not available."""
 | 
					    """The reserved codes that are not available."""
 | 
				
			||||||
 | 
					 | 
				
			||||||
    class CodeUnique:
 | 
					 | 
				
			||||||
        """The validator to check if the code is unique."""
 | 
					 | 
				
			||||||
        def __call__(self, form: CurrencyForm, field: StringField) -> None:
 | 
					 | 
				
			||||||
            if field.data == "":
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            if form.obj_code is not None and form.obj_code == field.data:
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            if db.session.get(Currency, field.data) is not None:
 | 
					 | 
				
			||||||
                raise ValidationError(lazy_gettext(
 | 
					 | 
				
			||||||
                    "Code conflicts with another currency."))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    code = StringField(
 | 
					    code = StringField(
 | 
				
			||||||
        filters=[strip_text],
 | 
					        filters=[strip_text],
 | 
				
			||||||
        validators=[DataRequired(lazy_gettext("Please fill in the code.")),
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the code.")),
 | 
				
			||||||
@@ -82,12 +81,3 @@ class CurrencyForm(FlaskForm):
 | 
				
			|||||||
            current_user_pk: int = get_current_user_pk()
 | 
					            current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
            obj.created_by_id = current_user_pk
 | 
					            obj.created_by_id = current_user_pk
 | 
				
			||||||
            obj.updated_by_id = current_user_pk
 | 
					            obj.updated_by_id = current_user_pk
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def post_update(self, obj) -> None:
 | 
					 | 
				
			||||||
        """The post-processing after the update.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: None
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        current_user_pk: int = get_current_user_pk()
 | 
					 | 
				
			||||||
        obj.updated_by_id = current_user_pk
 | 
					 | 
				
			||||||
        obj.updated_at = sa.func.now()
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
#  See the License for the specific language governing permissions and
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
"""The currency query.
 | 
					"""The queries for the currency management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
@@ -35,10 +35,10 @@ def get_currency_query() -> list[Currency]:
 | 
				
			|||||||
    conditions: list[sa.BinaryExpression] = []
 | 
					    conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
    for k in keywords:
 | 
					    for k in keywords:
 | 
				
			||||||
        l10n: list[CurrencyL10n] = CurrencyL10n.query\
 | 
					        l10n: list[CurrencyL10n] = CurrencyL10n.query\
 | 
				
			||||||
            .filter(CurrencyL10n.name.contains(k)).all()
 | 
					            .filter(CurrencyL10n.name.icontains(k)).all()
 | 
				
			||||||
        l10n_matches: set[str] = {x.account_code for x in l10n}
 | 
					        l10n_matches: set[str] = {x.account_code for x in l10n}
 | 
				
			||||||
        conditions.append(sa.or_(Currency.code.contains(k),
 | 
					        conditions.append(sa.or_(Currency.code.icontains(k),
 | 
				
			||||||
                                 Currency.name_l10n.contains(k),
 | 
					                                 Currency.name_l10n.icontains(k),
 | 
				
			||||||
                                 Currency.code.in_(l10n_matches)))
 | 
					                                 Currency.code.in_(l10n_matches)))
 | 
				
			||||||
    return Currency.query.filter(*conditions)\
 | 
					    return Currency.query.filter(*conditions)\
 | 
				
			||||||
        .order_by(Currency.code).all()
 | 
					        .order_by(Currency.code).all()
 | 
				
			||||||
@@ -19,16 +19,20 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
from urllib.parse import urlencode, parse_qsl
 | 
					from urllib.parse import urlencode, parse_qsl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import Blueprint, render_template, redirect, session, request, \
 | 
					from flask import Blueprint, render_template, redirect, session, request, \
 | 
				
			||||||
    flash, url_for
 | 
					    flash, url_for
 | 
				
			||||||
from werkzeug.datastructures import ImmutableMultiDict
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
from accounting.locale import lazy_gettext
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
from accounting.models import Currency
 | 
					from accounting.models import Currency
 | 
				
			||||||
from accounting.utils.next_url import inherit_next, or_next
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next, or_next
 | 
				
			||||||
from accounting.utils.pagination import Pagination
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
from accounting.utils.permission import has_permission, can_view, can_edit
 | 
					from accounting.utils.permission import has_permission, can_view, can_edit
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
from .forms import CurrencyForm
 | 
					from .forms import CurrencyForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bp: Blueprint = Blueprint("currency", __name__)
 | 
					bp: Blueprint = Blueprint("currency", __name__)
 | 
				
			||||||
@@ -44,7 +48,7 @@ def list_currencies() -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    :return: The currency list.
 | 
					    :return: The currency list.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    from .query import get_currency_query
 | 
					    from .queries import get_currency_query
 | 
				
			||||||
    currencies: list[Currency] = get_currency_query()
 | 
					    currencies: list[Currency] = get_currency_query()
 | 
				
			||||||
    pagination: Pagination = Pagination[Currency](currencies)
 | 
					    pagination: Pagination = Pagination[Currency](currencies)
 | 
				
			||||||
    return render_template("accounting/currency/list.html",
 | 
					    return render_template("accounting/currency/list.html",
 | 
				
			||||||
@@ -78,18 +82,15 @@ def add_currency() -> redirect:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    form = CurrencyForm(request.form)
 | 
					    form = CurrencyForm(request.form)
 | 
				
			||||||
    if not form.validate():
 | 
					    if not form.validate():
 | 
				
			||||||
        for key in form.errors:
 | 
					        flash_form_errors(form)
 | 
				
			||||||
            for error in form.errors[key]:
 | 
					 | 
				
			||||||
                flash(error, "error")
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.currency.create")))
 | 
					        return redirect(inherit_next(url_for("accounting.currency.create")))
 | 
				
			||||||
    currency: Currency = Currency()
 | 
					    currency: Currency = Currency()
 | 
				
			||||||
    form.populate_obj(currency)
 | 
					    form.populate_obj(currency)
 | 
				
			||||||
    db.session.add(currency)
 | 
					    db.session.add(currency)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The currency is added successfully"), "success")
 | 
					    flash(s(lazy_gettext("The currency is added successfully.")), "success")
 | 
				
			||||||
    return redirect(inherit_next(url_for("accounting.currency.detail",
 | 
					    return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
                                         currency=currency)))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.get("/<currency:currency>", endpoint="detail")
 | 
					@bp.get("/<currency:currency>", endpoint="detail")
 | 
				
			||||||
@@ -134,23 +135,20 @@ def update_currency(currency: Currency) -> redirect:
 | 
				
			|||||||
    form = CurrencyForm(request.form)
 | 
					    form = CurrencyForm(request.form)
 | 
				
			||||||
    form.obj_code = currency.code
 | 
					    form.obj_code = currency.code
 | 
				
			||||||
    if not form.validate():
 | 
					    if not form.validate():
 | 
				
			||||||
        for key in form.errors:
 | 
					        flash_form_errors(form)
 | 
				
			||||||
            for error in form.errors[key]:
 | 
					 | 
				
			||||||
                flash(error, "error")
 | 
					 | 
				
			||||||
        session["form"] = urlencode(list(request.form.items()))
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.currency.edit",
 | 
					        return redirect(inherit_next(url_for("accounting.currency.edit",
 | 
				
			||||||
                                             currency=currency)))
 | 
					                                             currency=currency)))
 | 
				
			||||||
    with db.session.no_autoflush:
 | 
					    with db.session.no_autoflush:
 | 
				
			||||||
        form.populate_obj(currency)
 | 
					        form.populate_obj(currency)
 | 
				
			||||||
    if not currency.is_modified:
 | 
					    if not currency.is_modified:
 | 
				
			||||||
        flash(lazy_gettext("The currency was not modified."), "success")
 | 
					        flash(s(lazy_gettext("The currency was not modified.")), "success")
 | 
				
			||||||
        return redirect(inherit_next(url_for("accounting.currency.detail",
 | 
					        return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
                                             currency=currency)))
 | 
					    currency.updated_by_id = get_current_user_pk()
 | 
				
			||||||
    form.post_update(currency)
 | 
					    currency.updated_at = sa.func.now()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The currency is updated successfully."), "success")
 | 
					    flash(s(lazy_gettext("The currency is updated successfully.")), "success")
 | 
				
			||||||
    return redirect(inherit_next(url_for("accounting.currency.detail",
 | 
					    return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
                                         currency=currency)))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.post("/<currency:currency>/delete", endpoint="delete")
 | 
					@bp.post("/<currency:currency>/delete", endpoint="delete")
 | 
				
			||||||
@@ -162,9 +160,12 @@ def delete_currency(currency: Currency) -> redirect:
 | 
				
			|||||||
    :return: The redirection to the currency list on success, or the currency
 | 
					    :return: The redirection to the currency list on success, or the currency
 | 
				
			||||||
        detail on error.
 | 
					        detail on error.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					    if not currency.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The currency cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(currency)))
 | 
				
			||||||
    currency.delete()
 | 
					    currency.delete()
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    flash(lazy_gettext("The currency is deleted successfully."), "success")
 | 
					    flash(s(lazy_gettext("The currency is deleted successfully.")), "success")
 | 
				
			||||||
    return redirect(or_next(url_for("accounting.currency.list")))
 | 
					    return redirect(or_next(url_for("accounting.currency.list")))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,3 +177,12 @@ def exists_code() -> dict[str, bool]:
 | 
				
			|||||||
    :return: Whether the currency code exists.
 | 
					    :return: Whether the currency code exists.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    return {"exists": db.session.get(Currency, request.args["q"]) is not None}
 | 
					    return {"exists": db.session.get(Currency, request.args["q"]) is not None}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_detail_uri(currency: Currency) -> str:
 | 
				
			||||||
 | 
					    """Returns the detail URI of a currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :return: The detail URI of the currency.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.currency.detail", currency=currency)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										528
									
								
								src/accounting/data/base_accounts.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										528
									
								
								src/accounting/data/base_accounts.csv
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,528 @@
 | 
				
			|||||||
 | 
					code,title,l10n-zh_Hant,l10n-zh_Hans
 | 
				
			||||||
 | 
					1,assets,資產,资产
 | 
				
			||||||
 | 
					2,liabilities,負債,负债
 | 
				
			||||||
 | 
					3,owners’ equity,業主權益,业主权益
 | 
				
			||||||
 | 
					4,operating revenue,營業收入,营业收入
 | 
				
			||||||
 | 
					5,operating costs,營業成本,营业成本
 | 
				
			||||||
 | 
					6,operating expenses,營業費用,营业费用
 | 
				
			||||||
 | 
					7,"non-operating revenue and expenses, other income (expense)",營業外收入及費用,营业外收入及费用
 | 
				
			||||||
 | 
					8,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
 | 
				
			||||||
 | 
					9,nonrecurring gain or loss,非經常營業損益,非经常营业损益
 | 
				
			||||||
 | 
					11,current assets,流動資產,流动资产
 | 
				
			||||||
 | 
					12,current assets,流動資產,流动资产
 | 
				
			||||||
 | 
					13,funds and long-term investments,基金及長期投資,基金及长期投资
 | 
				
			||||||
 | 
					14,"property , plant, and equipment",固定資產,固定资产
 | 
				
			||||||
 | 
					15,"property , plant, and equipment",固定資產,固定资产
 | 
				
			||||||
 | 
					16,depletable assets,遞耗資產,递耗资产
 | 
				
			||||||
 | 
					17,intangible assets,無形資產,无形资产
 | 
				
			||||||
 | 
					18,other assets,其他資產,其他资产
 | 
				
			||||||
 | 
					21,current liabilities,流動負債,流动负债
 | 
				
			||||||
 | 
					22,current liabilities,流動負債,流动负债
 | 
				
			||||||
 | 
					23,long-term liabilities,長期負債,长期负债
 | 
				
			||||||
 | 
					28,other liabilities,其他負債,其他负债
 | 
				
			||||||
 | 
					31,capital,資本,资本
 | 
				
			||||||
 | 
					32,additional paid-in capital,資本公積,资本公积
 | 
				
			||||||
 | 
					33,retained earnings (accumulated deficit),保留盈餘(或累積虧損),保留盈余(或累积亏损)
 | 
				
			||||||
 | 
					34,equity adjustments,權益調整,权益调整
 | 
				
			||||||
 | 
					35,treasury stock,庫藏股,库藏股
 | 
				
			||||||
 | 
					36,minority interest,少數股權,少数股权
 | 
				
			||||||
 | 
					41,sales revenue,銷貨收入,销货收入
 | 
				
			||||||
 | 
					46,service revenue,勞務收入,劳务收入
 | 
				
			||||||
 | 
					47,agency revenue,業務收入,业务收入
 | 
				
			||||||
 | 
					48,other operating revenue,其他營業收入,其他营业收入
 | 
				
			||||||
 | 
					51,cost of goods sold,銷貨成本,销货成本
 | 
				
			||||||
 | 
					56,service costs,勞務成本,劳务成本
 | 
				
			||||||
 | 
					57,agency costs,業務成本,业务成本
 | 
				
			||||||
 | 
					58,other operating costs,其他營業成本,其他营业成本
 | 
				
			||||||
 | 
					61,selling expenses,推銷費用,推销费用
 | 
				
			||||||
 | 
					62,general & administrative expenses,管理及總務費用,管理及总务费用
 | 
				
			||||||
 | 
					63,research and development expenses,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					71,non-operating revenue,營業外收入,营业外收入
 | 
				
			||||||
 | 
					72,non-operating revenue,營業外收入,营业外收入
 | 
				
			||||||
 | 
					73,non-operating revenue,營業外收入,营业外收入
 | 
				
			||||||
 | 
					74,non-operating revenue,營業外收入,营业外收入
 | 
				
			||||||
 | 
					75,non-operating expenses,營業外費用,营业外费用
 | 
				
			||||||
 | 
					76,non-operating expenses,營業外費用,营业外费用
 | 
				
			||||||
 | 
					77,non-operating expenses,營業外費用,营业外费用
 | 
				
			||||||
 | 
					78,non-operating expenses,營業外費用,营业外费用
 | 
				
			||||||
 | 
					81,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
 | 
				
			||||||
 | 
					91,gain (loss) from discontinued operations,停業部門損益,停业部门损益
 | 
				
			||||||
 | 
					92,extraordinary gain or loss,非常損益,非常损益
 | 
				
			||||||
 | 
					93,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
 | 
				
			||||||
 | 
					94,minority interest income,少數股權淨利,少数股权净利
 | 
				
			||||||
 | 
					111,cash and cash equivalents,現金及約當現金,现金及约当现金
 | 
				
			||||||
 | 
					112,short-term investments,短期投資,短期投资
 | 
				
			||||||
 | 
					113,notes receivable,應收票據,应收票据
 | 
				
			||||||
 | 
					114,accounts receivable,應收帳款,应收帐款
 | 
				
			||||||
 | 
					118,other receivables,其他應收款,其他应收款
 | 
				
			||||||
 | 
					121,inventories,存貨,存货
 | 
				
			||||||
 | 
					122,inventories,存貨,存货
 | 
				
			||||||
 | 
					125,prepaid expenses,預付費用,预付费用
 | 
				
			||||||
 | 
					126,prepayments,預付款項,预付款项
 | 
				
			||||||
 | 
					128,other current assets,其他流動資產,其他流动资产
 | 
				
			||||||
 | 
					129,other current assets,其他流動資產,其他流动资产
 | 
				
			||||||
 | 
					131,funds,基金,基金
 | 
				
			||||||
 | 
					132,long-term investments,長期投資,长期投资
 | 
				
			||||||
 | 
					141,land,土地,土地
 | 
				
			||||||
 | 
					142,land improvements,土地改良物,土地改良物
 | 
				
			||||||
 | 
					143,buildings,房屋及建物,房屋及建物
 | 
				
			||||||
 | 
					144,machinery and equipment,機(器)具及設備,机(器)具及设备
 | 
				
			||||||
 | 
					145,machinery and equipment,機(器)具及設備,机(器)具及设备
 | 
				
			||||||
 | 
					146,machinery and equipment,機(器)具及設備,机(器)具及设备
 | 
				
			||||||
 | 
					151,leased assets,租賃資產,租赁资产
 | 
				
			||||||
 | 
					152,leasehold improvements,租賃權益改良,租赁权益改良
 | 
				
			||||||
 | 
					156,construction in progress and prepayments for equipment,未完工程及預付購置設備款,未完工程及预付购置设备款
 | 
				
			||||||
 | 
					158,"miscellaneous property, plant, and equipment",雜項固定資產,杂项固定资产
 | 
				
			||||||
 | 
					161,depletable assets,遞耗資產,递耗资产
 | 
				
			||||||
 | 
					171,trademarks,商標權,商标权
 | 
				
			||||||
 | 
					172,patents,專利權,专利权
 | 
				
			||||||
 | 
					173,franchise,特許權,特许权
 | 
				
			||||||
 | 
					174,copyright,著作權,著作权
 | 
				
			||||||
 | 
					175,computer software,電腦軟體,电脑软体
 | 
				
			||||||
 | 
					176,goodwill,商譽,商誉
 | 
				
			||||||
 | 
					177,organization costs,開辦費,开办费
 | 
				
			||||||
 | 
					178,other intangibles,其他無形資產,其他无形资产
 | 
				
			||||||
 | 
					181,deferred assets,遞延資產,递延资产
 | 
				
			||||||
 | 
					182,idle assets,閒置資產,闲置资产
 | 
				
			||||||
 | 
					184,"long-term notes , accounts and overdue receivables",長期應收票據及款項與催收帳款,长期应收票据及款项与催收帐款
 | 
				
			||||||
 | 
					185,assets leased to others,出租資產,出租资产
 | 
				
			||||||
 | 
					186,refundable deposit,存出保證金,存出保证金
 | 
				
			||||||
 | 
					188,miscellaneous assets,雜項資產,杂项资产
 | 
				
			||||||
 | 
					211,short-term borrowings (debt),短期借款,短期借款
 | 
				
			||||||
 | 
					212,short-term notes and bills payable,應付短期票券,应付短期票券
 | 
				
			||||||
 | 
					213,notes payable,應付票據,应付票据
 | 
				
			||||||
 | 
					214,accounts pay able,應付帳款,应付帐款
 | 
				
			||||||
 | 
					216,income taxes payable,應付所得稅,应付所得税
 | 
				
			||||||
 | 
					217,accrued expenses,應付費用,应付费用
 | 
				
			||||||
 | 
					218,other payables,其他應付款,其他应付款
 | 
				
			||||||
 | 
					219,other payables,其他應付款,其他应付款
 | 
				
			||||||
 | 
					226,advance receipts,預收款項,预收款项
 | 
				
			||||||
 | 
					227,long-term liabilities -current portion,一年或一營業週期內到期長期負債,一年或一营业周期内到期长期负债
 | 
				
			||||||
 | 
					228,other current liabilities,其他流動負債,其他流动负债
 | 
				
			||||||
 | 
					229,other current liabilities,其他流動負債,其他流动负债
 | 
				
			||||||
 | 
					231,corporate bonds payable,應付公司債,应付公司债
 | 
				
			||||||
 | 
					232,long-term loans payable,長期借款,长期借款
 | 
				
			||||||
 | 
					233,long-term notes and accounts payable,長期應付票據及款項,长期应付票据及款项
 | 
				
			||||||
 | 
					234,accrued liabilities for land value increment tax,估計應付土地增值稅,估计应付土地增值税
 | 
				
			||||||
 | 
					235,accrued pension liabilities,應計退休金負債,应计退休金负债
 | 
				
			||||||
 | 
					238,other long-term liabilities,其他長期負債,其他长期负债
 | 
				
			||||||
 | 
					281,deferred liabilities,遞延負債,递延负债
 | 
				
			||||||
 | 
					286,deposits received,存入保證金,存入保证金
 | 
				
			||||||
 | 
					288,miscellaneous liabilities,雜項負債,杂项负债
 | 
				
			||||||
 | 
					311,capital,資本(或股本),资本(或股本)
 | 
				
			||||||
 | 
					321,paid-in capital in excess of par,股票溢價,股票溢价
 | 
				
			||||||
 | 
					323,capital surplus from assets revaluation,資產重估增值準備,资产重估增值准备
 | 
				
			||||||
 | 
					324,capital surplus from gain on disposal of assets,處分資產溢價公積,处分资产溢价公积
 | 
				
			||||||
 | 
					325,capital surplus from business combination,合併公積,合并公积
 | 
				
			||||||
 | 
					326,donated surplus,受贈公積,受赠公积
 | 
				
			||||||
 | 
					328,other additional paid-in capital,其他資本公積,其他资本公积
 | 
				
			||||||
 | 
					331,legal reserve,法定盈餘公積,法定盈余公积
 | 
				
			||||||
 | 
					332,special reserve,特別盈餘公積,特别盈余公积
 | 
				
			||||||
 | 
					335,retained earnings-unappropriated (or accumulated deficit),未分配盈餘(或累積虧損),未分配盈余(或累积亏损)
 | 
				
			||||||
 | 
					341,unrealized loss on market value decline of long-term equity investments,長期股權投資未實現跌價損失,长期股权投资未实现跌价损失
 | 
				
			||||||
 | 
					342,cumulative translation adjustment,累積換算調整數,累积换算调整数
 | 
				
			||||||
 | 
					343,net loss not recognized as pension cost,未認列為退休金成本之淨損失,未认列为退休金成本之净损失
 | 
				
			||||||
 | 
					351,treasury stock,庫藏股,库藏股
 | 
				
			||||||
 | 
					361,minority interest,少數股權,少数股权
 | 
				
			||||||
 | 
					411,sales revenue,銷貨收入,销货收入
 | 
				
			||||||
 | 
					417,sales return,銷貨退回,销货退回
 | 
				
			||||||
 | 
					419,sales allowances,銷貨折讓,销货折让
 | 
				
			||||||
 | 
					461,service revenue,勞務收入,劳务收入
 | 
				
			||||||
 | 
					471,agency revenue,業務收入,业务收入
 | 
				
			||||||
 | 
					488,other operating revenue,其他營業收入—其他,其他营业收入—其他
 | 
				
			||||||
 | 
					511,cost of goods sold,銷貨成本,销货成本
 | 
				
			||||||
 | 
					512,purchases,進貨,进货
 | 
				
			||||||
 | 
					513,materials purchased,進料,进料
 | 
				
			||||||
 | 
					514,direct labor,直接人工,直接人工
 | 
				
			||||||
 | 
					515,manufacturing overhead,製造費用,制造费用
 | 
				
			||||||
 | 
					516,manufacturing overhead,製造費用,制造费用
 | 
				
			||||||
 | 
					517,manufacturing overhead,製造費用,制造费用
 | 
				
			||||||
 | 
					518,manufacturing overhead,製造費用,制造费用
 | 
				
			||||||
 | 
					561,service costs,勞務成本,劳务成本
 | 
				
			||||||
 | 
					571,agency costs,業務成本,业务成本
 | 
				
			||||||
 | 
					588,other operating costs-other,其他營業成本—其他,其他营业成本—其他
 | 
				
			||||||
 | 
					615,selling expenses,推銷費用,推销费用
 | 
				
			||||||
 | 
					616,selling expenses,推銷費用,推销费用
 | 
				
			||||||
 | 
					617,selling expenses,推銷費用,推销费用
 | 
				
			||||||
 | 
					618,selling expenses,推銷費用,推销费用
 | 
				
			||||||
 | 
					625,general & administrative expenses,管理及總務費用,管理及总务费用
 | 
				
			||||||
 | 
					626,general & administrative expenses,管理及總務費用,管理及总务费用
 | 
				
			||||||
 | 
					627,general & administrative expenses,管理及總務費用,管理及总务费用
 | 
				
			||||||
 | 
					628,general & administrative expenses,管理及總務費用,管理及总务费用
 | 
				
			||||||
 | 
					635,research and development expenses,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					636,research and development expenses,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					637,research and development expenses,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					638,research and development expenses,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					711,interest revenue,利息收入,利息收入
 | 
				
			||||||
 | 
					712,investment income,投資收益,投资收益
 | 
				
			||||||
 | 
					713,foreign exchange gain,兌換利益,兑换利益
 | 
				
			||||||
 | 
					714,gain on disposal of investments,處分投資收益,处分投资收益
 | 
				
			||||||
 | 
					715,gain on disposal of assets,處分資產溢價收入,处分资产溢价收入
 | 
				
			||||||
 | 
					748,other non-operating revenue,其他營業外收入,其他营业外收入
 | 
				
			||||||
 | 
					751,interest expense,利息費用,利息费用
 | 
				
			||||||
 | 
					752,investment loss,投資損失,投资损失
 | 
				
			||||||
 | 
					753,foreign exchange loss,兌換損失,兑换损失
 | 
				
			||||||
 | 
					754,loss on disposal of investments,處分投資損失,处分投资损失
 | 
				
			||||||
 | 
					755,loss on disposal of assets,處分資產損失,处分资产损失
 | 
				
			||||||
 | 
					788,other non-operating expenses,其他營業外費用,其他营业外费用
 | 
				
			||||||
 | 
					811,income tax expense (or benefit),所得稅費用(或利益),所得税费用(或利益)
 | 
				
			||||||
 | 
					911,income (loss) from operations of discontinued segments,停業部門損益—停業前營業損益,停业部门损益—停业前营业损益
 | 
				
			||||||
 | 
					912,gain (loss) from disposal of discontinued segments,停業部門損益—處分損益,停业部门损益—处分损益
 | 
				
			||||||
 | 
					921,extraordinary gain or loss,非常損益,非常损益
 | 
				
			||||||
 | 
					931,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
 | 
				
			||||||
 | 
					941,minority interest income,少數股權淨利,少数股权净利
 | 
				
			||||||
 | 
					1111,cash on hand,庫存現金,库存现金
 | 
				
			||||||
 | 
					1112,petty cash/revolving funds,零用金/週轉金,零用金/周转金
 | 
				
			||||||
 | 
					1113,cash in banks,銀行存款,银行存款
 | 
				
			||||||
 | 
					1116,cash in transit,在途現金,在途现金
 | 
				
			||||||
 | 
					1117,cash equivalents,約當現金,约当现金
 | 
				
			||||||
 | 
					1118,other cash and cash equivalents,其他現金及約當現金,其他现金及约当现金
 | 
				
			||||||
 | 
					1121,short-term investments – stock,短期投資—股票,短期投资—股票
 | 
				
			||||||
 | 
					1122,short-term investments – short-term notes and bills,短期投資—短期票券,短期投资—短期票券
 | 
				
			||||||
 | 
					1123,short-term investments – government bonds,短期投資—政府債券,短期投资—政府债券
 | 
				
			||||||
 | 
					1124,short-term investments – beneficiary certificates,短期投資—受益憑證,短期投资—受益凭证
 | 
				
			||||||
 | 
					1125,short-term investments – corporate bonds,短期投資—公司債,短期投资—公司债
 | 
				
			||||||
 | 
					1128,short-term investments – other,短期投資—其他,短期投资—其他
 | 
				
			||||||
 | 
					1129,allowance for reduction of short-term investment to market,備抵短期投資跌價損失,备抵短期投资跌价损失
 | 
				
			||||||
 | 
					1131,notes receivable,應收票據,应收票据
 | 
				
			||||||
 | 
					1132,discounted notes receivable,應收票據貼現,应收票据贴现
 | 
				
			||||||
 | 
					1137,notes receivable – related parties,應收票據—關係人,应收票据—关系人
 | 
				
			||||||
 | 
					1138,other notes receivable,其他應收票據,其他应收票据
 | 
				
			||||||
 | 
					1139,allowance for uncollectible accounts – notes receivable,備抵呆帳-應收票據,备抵呆帐-应收票据
 | 
				
			||||||
 | 
					1141,accounts receivable,應收帳款,应收帐款
 | 
				
			||||||
 | 
					1142,installment accounts receivable,應收分期帳款,应收分期帐款
 | 
				
			||||||
 | 
					1147,accounts receivable – related parties,應收帳款—關係人,应收帐款—关系人
 | 
				
			||||||
 | 
					1149,allowance for uncollectible accounts – accounts receivable,備抵呆帳-應收帳款,备抵呆帐-应收帐款
 | 
				
			||||||
 | 
					1181,forward exchange contract receivable,應收出售遠匯款,应收出售远汇款
 | 
				
			||||||
 | 
					1182,forward exchange contract receivable – foreign currencies,應收遠匯款—外幣,应收远汇款—外币
 | 
				
			||||||
 | 
					1183,discount on forward ex-change contract,買賣遠匯折價,买卖远汇折价
 | 
				
			||||||
 | 
					1184,earned revenue receivable,應收收益,应收收益
 | 
				
			||||||
 | 
					1185,income tax refund receivable,應收退稅款,应收退税款
 | 
				
			||||||
 | 
					1187,other receivables – related parties,其他應收款—關係人,其他应收款—关系人
 | 
				
			||||||
 | 
					1188,other receivables – other,其他應收款—其他,其他应收款—其他
 | 
				
			||||||
 | 
					1189,allowance for uncollectible accounts – other receivables,備抵呆帳—其他應收款,备抵呆帐—其他应收款
 | 
				
			||||||
 | 
					1211,merchandise inventory,商品存貨,商品存货
 | 
				
			||||||
 | 
					1212,consigned goods,寄銷商品,寄销商品
 | 
				
			||||||
 | 
					1213,goods in transit,在途商品,在途商品
 | 
				
			||||||
 | 
					1219,allowance for reduction of inventory to market,備抵存貨跌價損失,备抵存货跌价损失
 | 
				
			||||||
 | 
					1221,finished goods,製成品,制成品
 | 
				
			||||||
 | 
					1222,consigned finished goods,寄銷製成品,寄销制成品
 | 
				
			||||||
 | 
					1223,by-products,副產品,副产品
 | 
				
			||||||
 | 
					1224,work in process,在製品,在制品
 | 
				
			||||||
 | 
					1225,work in process – outsourced,委外加工,委外加工
 | 
				
			||||||
 | 
					1226,raw materials,原料,原料
 | 
				
			||||||
 | 
					1227,supplies,物料,物料
 | 
				
			||||||
 | 
					1228,materials and supplies in transit,在途原物料,在途原物料
 | 
				
			||||||
 | 
					1229,allowance for reduction of inventory to market,備抵存貨跌價損失,备抵存货跌价损失
 | 
				
			||||||
 | 
					1251,prepaid payroll,預付薪資,预付薪资
 | 
				
			||||||
 | 
					1252,prepaid rents,預付租金,预付租金
 | 
				
			||||||
 | 
					1253,prepaid insurance,預付保險費,预付保险费
 | 
				
			||||||
 | 
					1254,office supplies,用品盤存,用品盘存
 | 
				
			||||||
 | 
					1255,prepaid income tax,預付所得稅,预付所得税
 | 
				
			||||||
 | 
					1258,other prepaid expenses,其他預付費用,其他预付费用
 | 
				
			||||||
 | 
					1261,prepayment for purchases,預付貨款,预付货款
 | 
				
			||||||
 | 
					1268,other prepayments,其他預付款項,其他预付款项
 | 
				
			||||||
 | 
					1281,VAT paid ( or input tax),進項稅額,进项税额
 | 
				
			||||||
 | 
					1282,excess VAT paid (or overpaid VAT),留抵稅額,留抵税额
 | 
				
			||||||
 | 
					1283,temporary payments,暫付款,暂付款
 | 
				
			||||||
 | 
					1284,payment on behalf of others,代付款,代付款
 | 
				
			||||||
 | 
					1285,advances to employees,員工借支,员工借支
 | 
				
			||||||
 | 
					1286,refundable deposits,存出保證金,存出保证金
 | 
				
			||||||
 | 
					1287,certificate of deposit-restricted,受限制存款,受限制存款
 | 
				
			||||||
 | 
					1291,deferred income tax assets,遞延所得稅資產,递延所得税资产
 | 
				
			||||||
 | 
					1292,deferred foreign exchange losses,遞延兌換損失,递延兑换损失
 | 
				
			||||||
 | 
					1293,owners’ (stockholders’) current account,業主(股東)往來,业主(股东)往来
 | 
				
			||||||
 | 
					1294,current account with others,同業往來,同业往来
 | 
				
			||||||
 | 
					1298,other current assets – other,其他流動資產—其他,其他流动资产—其他
 | 
				
			||||||
 | 
					1311,redemption fund (or sinking fund),償債基金,偿债基金
 | 
				
			||||||
 | 
					1312,fund for improvement and expansion,改良及擴充基金,改良及扩充基金
 | 
				
			||||||
 | 
					1313,contingency fund,意外損失準備基金,意外损失准备基金
 | 
				
			||||||
 | 
					1314,pension fund,退休基金,退休基金
 | 
				
			||||||
 | 
					1318,other funds,其他基金,其他基金
 | 
				
			||||||
 | 
					1321,long-term equity investments,長期股權投資,长期股权投资
 | 
				
			||||||
 | 
					1322,long-term bond investments,長期債券投資,长期债券投资
 | 
				
			||||||
 | 
					1323,long-term real estate in-vestments,長期不動產投資,长期不动产投资
 | 
				
			||||||
 | 
					1324,cash surrender value of life insurance,人壽保險現金解約價值,人寿保险现金解约价值
 | 
				
			||||||
 | 
					1328,other long-term investments,其他長期投資,其他长期投资
 | 
				
			||||||
 | 
					1329,allowance for excess of cost over market value of long-term investments,備抵長期投資跌價損失,备抵长期投资跌价损失
 | 
				
			||||||
 | 
					1411,land,土地,土地
 | 
				
			||||||
 | 
					1418,land – revaluation increments,土地—重估增值,土地—重估增值
 | 
				
			||||||
 | 
					1421,land improvements,土地改良物,土地改良物
 | 
				
			||||||
 | 
					1428,land improvements – revaluation increments,土地改良物—重估增值,土地改良物—重估增值
 | 
				
			||||||
 | 
					1429,accumulated depreciation – land improvements,累積折舊—土地改良物,累积折旧—土地改良物
 | 
				
			||||||
 | 
					1431,buildings,房屋及建物,房屋及建物
 | 
				
			||||||
 | 
					1438,buildings –revaluation increments,房屋及建物—重估增值,房屋及建物—重估增值
 | 
				
			||||||
 | 
					1439,accumulated depreciation – buildings,累積折舊—房屋及建物,累积折旧—房屋及建物
 | 
				
			||||||
 | 
					1441,machinery,機(器)具,机(器)具
 | 
				
			||||||
 | 
					1448,machinery – revaluation increments,機(器)具—重估增值,机(器)具—重估增值
 | 
				
			||||||
 | 
					1449,accumulated depreciation – machinery,累積折舊—機(器)具,累积折旧—机(器)具
 | 
				
			||||||
 | 
					1511,leased assets,租賃資產,租赁资产
 | 
				
			||||||
 | 
					1519,accumulated depreciation – leased assets,累積折舊—租賃資產,累积折旧—租赁资产
 | 
				
			||||||
 | 
					1521,leasehold improvements,租賃權益改良,租赁权益改良
 | 
				
			||||||
 | 
					1529,accumulated depreciation – leasehold improvements,累積折舊—租賃權益改良,累积折旧—租赁权益改良
 | 
				
			||||||
 | 
					1561,construction in progress,未完工程,未完工程
 | 
				
			||||||
 | 
					1562,prepayment for equipment,預付購置設備款,预付购置设备款
 | 
				
			||||||
 | 
					1581,"miscellaneous property, plant, and equipment",雜項固定資產,杂项固定资产
 | 
				
			||||||
 | 
					1588,"miscellaneous property, plant, and equipment – revaluation increments",雜項固定資產—重估增值,杂项固定资产—重估增值
 | 
				
			||||||
 | 
					1589,"accumulated depreciation – miscellaneous property, plant, and equipment",累積折舊—雜項固定資產,累积折旧—杂项固定资产
 | 
				
			||||||
 | 
					1611,natural resources,天然資源,天然资源
 | 
				
			||||||
 | 
					1618,natural resources –revaluation increments,天然資源—重估增值,天然资源—重估增值
 | 
				
			||||||
 | 
					1619,accumulated depletion – natural resources,累積折耗—天然資源,累积折耗—天然资源
 | 
				
			||||||
 | 
					1711,trademarks,商標權,商标权
 | 
				
			||||||
 | 
					1721,patents,專利權,专利权
 | 
				
			||||||
 | 
					1731,franchise,特許權,特许权
 | 
				
			||||||
 | 
					1741,copyright,著作權,著作权
 | 
				
			||||||
 | 
					1751,computer software cost,電腦軟體,电脑软体
 | 
				
			||||||
 | 
					1761,goodwill,商譽,商誉
 | 
				
			||||||
 | 
					1771,organization costs,開辦費,开办费
 | 
				
			||||||
 | 
					1781,deferred pension costs,遞延退休金成本,递延退休金成本
 | 
				
			||||||
 | 
					1782,leasehold improvements,租賃權益改良,租赁权益改良
 | 
				
			||||||
 | 
					1788,other intangible assets – other,其他無形資產—其他,其他无形资产—其他
 | 
				
			||||||
 | 
					1811,deferred bond issuance costs,債券發行成本,债券发行成本
 | 
				
			||||||
 | 
					1812,long-term prepaid rent,長期預付租金,长期预付租金
 | 
				
			||||||
 | 
					1813,long-term prepaid insurance,長期預付保險費,长期预付保险费
 | 
				
			||||||
 | 
					1814,deferred income tax assets,遞延所得稅資產,递延所得税资产
 | 
				
			||||||
 | 
					1815,prepaid pension cost,預付退休金,预付退休金
 | 
				
			||||||
 | 
					1818,other deferred assets,其他遞延資產,其他递延资产
 | 
				
			||||||
 | 
					1821,idle assets,閒置資產,闲置资产
 | 
				
			||||||
 | 
					1841,long-term notes receivable,長期應收票據,长期应收票据
 | 
				
			||||||
 | 
					1842,long-term accounts receivable,長期應收帳款,长期应收帐款
 | 
				
			||||||
 | 
					1843,overdue receivables,催收帳款,催收帐款
 | 
				
			||||||
 | 
					1847,"long-term notes, accounts and overdue receivables – related parties",長期應收票據及款項與催收帳款—關係人,长期应收票据及款项与催收帐款—关系人
 | 
				
			||||||
 | 
					1848,other long-term receivables,其他長期應收款項,其他长期应收款项
 | 
				
			||||||
 | 
					1849,"allowance for uncollectible accounts – long-term notes, accounts and overdue receivables",備抵呆帳—長期應收票據及款項與催收帳款,备抵呆帐—长期应收票据及款项与催收帐款
 | 
				
			||||||
 | 
					1851,assets leased to others,出租資產,出租资产
 | 
				
			||||||
 | 
					1858,assets leased to others – incremental value from revaluation,出租資產—重估增值,出租资产—重估增值
 | 
				
			||||||
 | 
					1859,accumulated depreciation – assets leased to others,累積折舊—出租資產,累积折旧—出租资产
 | 
				
			||||||
 | 
					1861,refundable deposits,存出保證金,存出保证金
 | 
				
			||||||
 | 
					1881,certificate of deposit – restricted,受限制存款,受限制存款
 | 
				
			||||||
 | 
					1888,miscellaneous assets – other,雜項資產—其他,杂项资产—其他
 | 
				
			||||||
 | 
					2111,bank overdraft,銀行透支,银行透支
 | 
				
			||||||
 | 
					2112,bank loan,銀行借款,银行借款
 | 
				
			||||||
 | 
					2114,short-term borrowings – owners,短期借款—業主,短期借款—业主
 | 
				
			||||||
 | 
					2115,short-term borrowings – employees,短期借款—員工,短期借款—员工
 | 
				
			||||||
 | 
					2117,short-term borrowings – related parties,短期借款—關係人,短期借款—关系人
 | 
				
			||||||
 | 
					2118,short-term borrowings – other,短期借款—其他,短期借款—其他
 | 
				
			||||||
 | 
					2121,commercial paper payable,應付商業本票,应付商业本票
 | 
				
			||||||
 | 
					2122,bank acceptance,銀行承兌匯票,银行承兑汇票
 | 
				
			||||||
 | 
					2128,other short-term notes and bills payable,其他應付短期票券,其他应付短期票券
 | 
				
			||||||
 | 
					2129,discount on short-term notes and bills payable,應付短期票券折價,应付短期票券折价
 | 
				
			||||||
 | 
					2131,notes payable,應付票據,应付票据
 | 
				
			||||||
 | 
					2137,notes payable – related parties,應付票據—關係人,应付票据—关系人
 | 
				
			||||||
 | 
					2138,other notes payable,其他應付票據,其他应付票据
 | 
				
			||||||
 | 
					2141,accounts payable,應付帳款,应付帐款
 | 
				
			||||||
 | 
					2147,accounts payable – related parties,應付帳款—關係人,应付帐款—关系人
 | 
				
			||||||
 | 
					2161,income tax payable,應付所得稅,应付所得税
 | 
				
			||||||
 | 
					2171,accrued payroll,應付薪工,应付薪工
 | 
				
			||||||
 | 
					2172,accrued rent payable,應付租金,应付租金
 | 
				
			||||||
 | 
					2173,accrued interest payable,應付利息,应付利息
 | 
				
			||||||
 | 
					2174,accrued VAT payable,應付營業稅,应付营业税
 | 
				
			||||||
 | 
					2175,accrued taxes payable – other,應付稅捐—其他,应付税捐—其他
 | 
				
			||||||
 | 
					2178,other accrued expenses payable,其他應付費用,其他应付费用
 | 
				
			||||||
 | 
					2181,forward exchange contract payable,應付購入遠匯款,应付购入远汇款
 | 
				
			||||||
 | 
					2182,forward exchange contract payable – foreign currencies,應付遠匯款—外幣,应付远汇款—外币
 | 
				
			||||||
 | 
					2183,premium on forward exchange contract,買賣遠匯溢價,买卖远汇溢价
 | 
				
			||||||
 | 
					2184,payables on land and building purchased,應付土地房屋款,应付土地房屋款
 | 
				
			||||||
 | 
					2185,Payables on equipment,應付設備款,应付设备款
 | 
				
			||||||
 | 
					2187,other payables – related parties,其他應付款—關係人,其他应付款—关系人
 | 
				
			||||||
 | 
					2191,dividend payable,應付股利,应付股利
 | 
				
			||||||
 | 
					2192,bonus payable,應付紅利,应付红利
 | 
				
			||||||
 | 
					2193,compensation payable to directors and supervisors,應付董監事酬勞,应付董监事酬劳
 | 
				
			||||||
 | 
					2198,other payables – other,其他應付款—其他,其他应付款—其他
 | 
				
			||||||
 | 
					2261,sales revenue received in advance,預收貨款,预收货款
 | 
				
			||||||
 | 
					2262,revenue received in advance,預收收入,预收收入
 | 
				
			||||||
 | 
					2268,other advance receipts,其他預收款,其他预收款
 | 
				
			||||||
 | 
					2271,corporate bonds payable – current portion,一年或一營業週期內到期公司債,一年或一营业周期内到期公司债
 | 
				
			||||||
 | 
					2272,long-term loans payable – current portion,一年或一營業週期內到期長期借款,一年或一营业周期内到期长期借款
 | 
				
			||||||
 | 
					2273,long-term notes and accounts payable due within one year or one operating cycle,一年或一營業週期內到期長期應付票據及款項,一年或一营业周期内到期长期应付票据及款项
 | 
				
			||||||
 | 
					2277,long-term notes and accounts payables to related parties – current portion,一年或一營業週期內到期長期應付票據及款項—關係人,一年或一营业周期内到期长期应付票据及款项—关系人
 | 
				
			||||||
 | 
					2278,other long-term liabilities – current portion,其他一年或一營業週期內到期長期負債,其他一年或一营业周期内到期长期负债
 | 
				
			||||||
 | 
					2281,VAT received (or output tax),銷項稅額,销项税额
 | 
				
			||||||
 | 
					2283,temporary receipts,暫收款,暂收款
 | 
				
			||||||
 | 
					2284,receipts under custody,代收款,代收款
 | 
				
			||||||
 | 
					2285,estimated warranty liabilities,估計售後服務/保固負債,估计售后服务/保固负债
 | 
				
			||||||
 | 
					2291,deferred income tax liabilities,遞延所得稅負債,递延所得税负债
 | 
				
			||||||
 | 
					2292,deferred foreign exchange gain,遞延兌換利益,递延兑换利益
 | 
				
			||||||
 | 
					2293,owners’ current account,業主(股東)往來,业主(股东)往来
 | 
				
			||||||
 | 
					2294,current account with others,同業往來,同业往来
 | 
				
			||||||
 | 
					2298,other current liabilities – others,其他流動負債—其他,其他流动负债—其他
 | 
				
			||||||
 | 
					2311,corporate bonds payable,應付公司債,应付公司债
 | 
				
			||||||
 | 
					2319,premium (discount) on corporate bonds payable,應付公司債溢(折)價,应付公司债溢(折)价
 | 
				
			||||||
 | 
					2321,long-term loans payable – bank,長期銀行借款,长期银行借款
 | 
				
			||||||
 | 
					2324,long-term loans payable – owners,長期借款—業主,长期借款—业主
 | 
				
			||||||
 | 
					2325,long-term loans payable – employees,長期借款—員工,长期借款—员工
 | 
				
			||||||
 | 
					2327,long-term loans payable – related parties,長期借款—關係人,长期借款—关系人
 | 
				
			||||||
 | 
					2328,long-term loans payable – other,長期借款—其他,长期借款—其他
 | 
				
			||||||
 | 
					2331,long-term notes payable,長期應付票據,长期应付票据
 | 
				
			||||||
 | 
					2332,long-term accounts pay-able,長期應付帳款,长期应付帐款
 | 
				
			||||||
 | 
					2333,long-term capital lease liabilities,長期應付租賃負債,长期应付租赁负债
 | 
				
			||||||
 | 
					2337,Long-term notes and accounts payable – related parties,長期應付票據及款項—關係人,长期应付票据及款项—关系人
 | 
				
			||||||
 | 
					2338,other long-term payables,其他長期應付款項,其他长期应付款项
 | 
				
			||||||
 | 
					2341,estimated accrued land value incremental tax pay-able,估計應付土地增值稅,估计应付土地增值税
 | 
				
			||||||
 | 
					2351,accrued pension liabilities,應計退休金負債,应计退休金负债
 | 
				
			||||||
 | 
					2388,other long-term liabilities – other,其他長期負債—其他,其他长期负债—其他
 | 
				
			||||||
 | 
					2811,deferred revenue,遞延收入,递延收入
 | 
				
			||||||
 | 
					2814,deferred income tax liabilities,遞延所得稅負債,递延所得税负债
 | 
				
			||||||
 | 
					2818,other deferred liabilities,其他遞延負債,其他递延负债
 | 
				
			||||||
 | 
					2861,guarantee deposit received,存入保證金,存入保证金
 | 
				
			||||||
 | 
					2888,miscellaneous liabilities – other,雜項負債—其他,杂项负债—其他
 | 
				
			||||||
 | 
					3111,capital – common stock,普通股股本,普通股股本
 | 
				
			||||||
 | 
					3112,capital – preferred stock,特別股股本,特别股股本
 | 
				
			||||||
 | 
					3113,capital collected in advance,預收股本,预收股本
 | 
				
			||||||
 | 
					3114,stock dividends to be distributed,待分配股票股利,待分配股票股利
 | 
				
			||||||
 | 
					3115,capital,資本,资本
 | 
				
			||||||
 | 
					3211,paid-in capital in excess of par- common stock,普通股股票溢價,普通股股票溢价
 | 
				
			||||||
 | 
					3212,paid-in capital in excess of par- preferred stock,特別股股票溢價,特别股股票溢价
 | 
				
			||||||
 | 
					3231,capital surplus from assets revaluation,資產重估增值準備,资产重估增值准备
 | 
				
			||||||
 | 
					3241,capital surplus from gain on disposal of assets,處分資產溢價公積,处分资产溢价公积
 | 
				
			||||||
 | 
					3251,capital surplus from business combination,合併公積,合并公积
 | 
				
			||||||
 | 
					3261,donated surplus,受贈公積,受赠公积
 | 
				
			||||||
 | 
					3281,additional paid-in capital from investee under equity method,權益法長期股權投資資本公積,权益法长期股权投资资本公积
 | 
				
			||||||
 | 
					3282,additional paid-in capital – treasury stock trans-actions,資本公積—庫藏股票交易,资本公积—库藏股票交易
 | 
				
			||||||
 | 
					3311,legal reserve,法定盈餘公積,法定盈余公积
 | 
				
			||||||
 | 
					3321,contingency reserve,意外損失準備,意外损失准备
 | 
				
			||||||
 | 
					3322,improvement and expansion reserve,改良擴充準備,改良扩充准备
 | 
				
			||||||
 | 
					3323,special reserve for redemption of liabilities,償債準備,偿债准备
 | 
				
			||||||
 | 
					3328,other special reserve,其他特別盈餘公積,其他特别盈余公积
 | 
				
			||||||
 | 
					3351,accumulated profit or loss,累積盈虧,累积盈亏
 | 
				
			||||||
 | 
					3352,prior period adjustments,前期損益調整,前期损益调整
 | 
				
			||||||
 | 
					3353,net income or loss for current period,本期損益,本期损益
 | 
				
			||||||
 | 
					3411,unrealized loss on market value decline of long-term equity investments,長期股權投資未實現跌價損失,长期股权投资未实现跌价损失
 | 
				
			||||||
 | 
					3421,cumulative translation adjustments,累積換算調整數,累积换算调整数
 | 
				
			||||||
 | 
					3431,net loss not recognized as pension costs,未認列為退休金成本之淨損失,未认列为退休金成本之净损失
 | 
				
			||||||
 | 
					3511,treasury stock,庫藏股,库藏股
 | 
				
			||||||
 | 
					3611,minority interest,少數股權,少数股权
 | 
				
			||||||
 | 
					4111,sales revenue,銷貨收入,销货收入
 | 
				
			||||||
 | 
					4112,installment sales revenue,分期付款銷貨收入,分期付款销货收入
 | 
				
			||||||
 | 
					4171,sales return,銷貨退回,销货退回
 | 
				
			||||||
 | 
					4191,sales discounts and allowances,銷貨折讓,销货折让
 | 
				
			||||||
 | 
					4611,service revenue,勞務收入,劳务收入
 | 
				
			||||||
 | 
					4711,agency revenue,業務收入,业务收入
 | 
				
			||||||
 | 
					4888,other operating revenue – other,其他營業收入—其他,其他营业收入—其他
 | 
				
			||||||
 | 
					5111,cost of goods sold,銷貨成本,销货成本
 | 
				
			||||||
 | 
					5112,installment cost of goods sold,分期付款銷貨成本,分期付款销货成本
 | 
				
			||||||
 | 
					5121,purchases,進貨,进货
 | 
				
			||||||
 | 
					5122,purchase expenses,進貨費用,进货费用
 | 
				
			||||||
 | 
					5123,purchase returns,進貨退出,进货退出
 | 
				
			||||||
 | 
					5124,charges on purchased merchandise,進貨折讓,进货折让
 | 
				
			||||||
 | 
					5131,material purchased,進料,进料
 | 
				
			||||||
 | 
					5132,charges on purchased material,進料費用,进料费用
 | 
				
			||||||
 | 
					5133,material purchase returns,進料退出,进料退出
 | 
				
			||||||
 | 
					5134,material purchase allowances,進料折讓,进料折让
 | 
				
			||||||
 | 
					5141,direct labor,直接人工,直接人工
 | 
				
			||||||
 | 
					5151,indirect labor,間接人工,间接人工
 | 
				
			||||||
 | 
					5152,"rent expense, rent",租金支出,租金支出
 | 
				
			||||||
 | 
					5153,office supplies (expense),文具用品,文具用品
 | 
				
			||||||
 | 
					5154,"travelling expense, travel",旅費,旅费
 | 
				
			||||||
 | 
					5155,"shipping expenses, freight",運費,运费
 | 
				
			||||||
 | 
					5156,postage (expenses),郵電費,邮电费
 | 
				
			||||||
 | 
					5157,repair (s) and maintenance (expense ),修繕費,修缮费
 | 
				
			||||||
 | 
					5158,packing expenses,包裝費,包装费
 | 
				
			||||||
 | 
					5161,utilities (expense),水電瓦斯費,水电瓦斯费
 | 
				
			||||||
 | 
					5162,insurance (expense),保險費,保险费
 | 
				
			||||||
 | 
					5163,manufacturing overhead – outsourced,加工費,加工费
 | 
				
			||||||
 | 
					5166,taxes,稅捐,税捐
 | 
				
			||||||
 | 
					5168,depreciation expense,折舊,折旧
 | 
				
			||||||
 | 
					5169,various amortization,各項耗竭及攤提,各项耗竭及摊提
 | 
				
			||||||
 | 
					5172,meal (expenses),伙食費,伙食费
 | 
				
			||||||
 | 
					5173,employee benefits/welfare,職工福利,职工福利
 | 
				
			||||||
 | 
					5176,training (expense),訓練費,训练费
 | 
				
			||||||
 | 
					5177,indirect materials,間接材料,间接材料
 | 
				
			||||||
 | 
					5188,other manufacturing expenses,其他製造費用,其他制造费用
 | 
				
			||||||
 | 
					5611,service costs,勞務成本,劳务成本
 | 
				
			||||||
 | 
					5711,agency costs,業務成本,业务成本
 | 
				
			||||||
 | 
					5888,other operating costs – other,其他營業成本—其他,其他营业成本—其他
 | 
				
			||||||
 | 
					6151,payroll expense,薪資支出,薪资支出
 | 
				
			||||||
 | 
					6152,"rent expense, rent",租金支出,租金支出
 | 
				
			||||||
 | 
					6153,office supplies (expense),文具用品,文具用品
 | 
				
			||||||
 | 
					6154,"travelling expense, travel",旅費,旅费
 | 
				
			||||||
 | 
					6155,"shipping expenses, freight",運費,运费
 | 
				
			||||||
 | 
					6156,postage (expenses),郵電費,邮电费
 | 
				
			||||||
 | 
					6157,repair (s) and maintenance (expense),修繕費,修缮费
 | 
				
			||||||
 | 
					6159,"advertisement expense, advertisement",廣告費,广告费
 | 
				
			||||||
 | 
					6161,utilities (expense),水電瓦斯費,水电瓦斯费
 | 
				
			||||||
 | 
					6162,insurance (expense),保險費,保险费
 | 
				
			||||||
 | 
					6164,entertainment (expense),交際費,交际费
 | 
				
			||||||
 | 
					6165,donation (expense),捐贈,捐赠
 | 
				
			||||||
 | 
					6166,taxes,稅捐,税捐
 | 
				
			||||||
 | 
					6167,loss on uncollectible accounts,呆帳損失,呆帐损失
 | 
				
			||||||
 | 
					6168,depreciation expense,折舊,折旧
 | 
				
			||||||
 | 
					6169,various amortization,各項耗竭及攤提,各项耗竭及摊提
 | 
				
			||||||
 | 
					6172,meal (expenses),伙食費,伙食费
 | 
				
			||||||
 | 
					6173,employee benefits/welfare,職工福利,职工福利
 | 
				
			||||||
 | 
					6175,commission (expense),佣金支出,佣金支出
 | 
				
			||||||
 | 
					6176,training (expense),訓練費,训练费
 | 
				
			||||||
 | 
					6188,other selling expenses,其他推銷費用,其他推销费用
 | 
				
			||||||
 | 
					6251,payroll expense,薪資支出,薪资支出
 | 
				
			||||||
 | 
					6252,"rent expense, rent",租金支出,租金支出
 | 
				
			||||||
 | 
					6253,office supplies,文具用品,文具用品
 | 
				
			||||||
 | 
					6254,"travelling expense, travel",旅費,旅费
 | 
				
			||||||
 | 
					6255,"shipping expenses,freight",運費,运费
 | 
				
			||||||
 | 
					6256,postage (expenses),郵電費,邮电费
 | 
				
			||||||
 | 
					6257,repair (s) and maintenance (expense),修繕費,修缮费
 | 
				
			||||||
 | 
					6259,"advertisement expense, advertisement",廣告費,广告费
 | 
				
			||||||
 | 
					6261,utilities (expense),水電瓦斯費,水电瓦斯费
 | 
				
			||||||
 | 
					6262,insurance (expense),保險費,保险费
 | 
				
			||||||
 | 
					6264,entertainment (expense),交際費,交际费
 | 
				
			||||||
 | 
					6265,donation (expense),捐贈,捐赠
 | 
				
			||||||
 | 
					6266,taxes,稅捐,税捐
 | 
				
			||||||
 | 
					6267,loss on uncollectible accounts,呆帳損失,呆帐损失
 | 
				
			||||||
 | 
					6268,depreciation expense,折舊,折旧
 | 
				
			||||||
 | 
					6269,various amortization,各項耗竭及攤提,各项耗竭及摊提
 | 
				
			||||||
 | 
					6271,loss on export sales,外銷損失,外销损失
 | 
				
			||||||
 | 
					6272,meal (expenses),伙食費,伙食费
 | 
				
			||||||
 | 
					6273,employee benefits/welfare,職工福利,职工福利
 | 
				
			||||||
 | 
					6274,research and development expense,研究發展費用,研究发展费用
 | 
				
			||||||
 | 
					6275,commission (expense),佣金支出,佣金支出
 | 
				
			||||||
 | 
					6276,training (expense),訓練費,训练费
 | 
				
			||||||
 | 
					6278,professional service fees,勞務費,劳务费
 | 
				
			||||||
 | 
					6288,other general and administrative expenses,其他管理及總務費用,其他管理及总务费用
 | 
				
			||||||
 | 
					6351,payroll expense,薪資支出,薪资支出
 | 
				
			||||||
 | 
					6352,"rent expense, rent",租金支出,租金支出
 | 
				
			||||||
 | 
					6353,office supplies,文具用品,文具用品
 | 
				
			||||||
 | 
					6354,"travelling expense, travel",旅費,旅费
 | 
				
			||||||
 | 
					6355,"shipping expenses, freight",運費,运费
 | 
				
			||||||
 | 
					6356,postage (expenses),郵電費,邮电费
 | 
				
			||||||
 | 
					6357,repair (s) and maintenance (expense),修繕費,修缮费
 | 
				
			||||||
 | 
					6361,utilities (expense),水電瓦斯費,水电瓦斯费
 | 
				
			||||||
 | 
					6362,insurance (expense),保險費,保险费
 | 
				
			||||||
 | 
					6364,entertainment (expense),交際費,交际费
 | 
				
			||||||
 | 
					6366,taxes,稅捐,税捐
 | 
				
			||||||
 | 
					6368,depreciation expense,折舊,折旧
 | 
				
			||||||
 | 
					6369,various amortization,各項耗竭及攤提,各项耗竭及摊提
 | 
				
			||||||
 | 
					6372,meal (expenses),伙食費,伙食费
 | 
				
			||||||
 | 
					6373,employee benefits/welfare,職工福利,职工福利
 | 
				
			||||||
 | 
					6376,training (expense),訓練費,训练费
 | 
				
			||||||
 | 
					6378,other research and development expenses,其他研究發展費用,其他研究发展费用
 | 
				
			||||||
 | 
					7111,interest revenue/income,利息收入,利息收入
 | 
				
			||||||
 | 
					7121,investment income recognized under equity method,權益法認列之投資收益,权益法认列之投资收益
 | 
				
			||||||
 | 
					7122,dividends income,股利收入,股利收入
 | 
				
			||||||
 | 
					7123,gain on market price recovery of short-term investment,短期投資市價回升利益,短期投资市价回升利益
 | 
				
			||||||
 | 
					7131,foreign exchange gain,兌換利益,兑换利益
 | 
				
			||||||
 | 
					7141,gain on disposal of investments,處分投資收益,处分投资收益
 | 
				
			||||||
 | 
					7151,gain on disposal of assets,處分資產溢價收入,处分资产溢价收入
 | 
				
			||||||
 | 
					7481,donation income,捐贈收入,捐赠收入
 | 
				
			||||||
 | 
					7482,rent revenue/income,租金收入,租金收入
 | 
				
			||||||
 | 
					7483,commission revenue/income,佣金收入,佣金收入
 | 
				
			||||||
 | 
					7484,revenue from sale of scraps,出售下腳及廢料收入,出售下脚及废料收入
 | 
				
			||||||
 | 
					7485,gain on physical inventory,存貨盤盈,存货盘盈
 | 
				
			||||||
 | 
					7486,gain from price recovery of inventory,存貨跌價回升利益,存货跌价回升利益
 | 
				
			||||||
 | 
					7487,gain on reversal of bad debts,壞帳轉回利益,坏帐转回利益
 | 
				
			||||||
 | 
					7488,other non-operating revenue – other items,其他營業外收入—其他,其他营业外收入—其他
 | 
				
			||||||
 | 
					7511,interest expense,利息費用,利息费用
 | 
				
			||||||
 | 
					7521,investment loss recognized under equity method,權益法認列之投資損失,权益法认列之投资损失
 | 
				
			||||||
 | 
					7523,unrealized loss on reduction of short-term investments to market,短期投資未實現跌價損失,短期投资未实现跌价损失
 | 
				
			||||||
 | 
					7531,foreign exchange loss,兌換損失,兑换损失
 | 
				
			||||||
 | 
					7541,loss on disposal of investments,處分投資損失,处分投资损失
 | 
				
			||||||
 | 
					7551,loss on disposal of assets,處分資產損失,处分资产损失
 | 
				
			||||||
 | 
					7881,loss on work stoppages,停工損失,停工损失
 | 
				
			||||||
 | 
					7882,casualty loss,災害損失,灾害损失
 | 
				
			||||||
 | 
					7885,loss on physical inventory,存貨盤損,存货盘损
 | 
				
			||||||
 | 
					7886,loss for market price decline and obsolete and slow-moving inventories,存貨跌價及呆滯損失,存货跌价及呆滞损失
 | 
				
			||||||
 | 
					7888,other non-operating expenses – other,其他營業外費用—其他,其他营业外费用—其他
 | 
				
			||||||
 | 
					8111,income tax expense ( or benefit),所得稅費用(或利益),所得税费用(或利益)
 | 
				
			||||||
 | 
					9111,income (loss) from operations of discontinued segment,停業部門損益—停業前營業損益,停业部门损益—停业前营业损益
 | 
				
			||||||
 | 
					9121,gain (loss) from disposal of discontinued segment,停業部門損益—處分損益,停业部门损益—处分损益
 | 
				
			||||||
 | 
					9211,extraordinary gain or loss,非常損益,非常损益
 | 
				
			||||||
 | 
					9311,cumulative effect of changes in accounting principles,會計原則變動累積影響數,会计原则变动累积影响数
 | 
				
			||||||
 | 
					9411,minority interest income,少數股權淨利,少数股权净利
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										10
									
								
								src/accounting/data/currencies.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/accounting/data/currencies.csv
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					code,name,l10n-zh_Hant,l10n-zh_Hans
 | 
				
			||||||
 | 
					TWD,New Taiwan dollar,新臺幣,新台币
 | 
				
			||||||
 | 
					USD,United States dollar,美元,美元
 | 
				
			||||||
 | 
					JPY,Japanese yen,日圓,日圆
 | 
				
			||||||
 | 
					CNY,Renminbi,人民幣,人民币
 | 
				
			||||||
 | 
					HKD,Hong Kong dollar,港元,港元
 | 
				
			||||||
 | 
					EUR,Euro,歐元,欧元
 | 
				
			||||||
 | 
					MOP,Macanese pataca,澳門元,澳门元
 | 
				
			||||||
 | 
					AUD,Australian dollar,澳洲元,澳大利亚元
 | 
				
			||||||
 | 
					ETH,Ethereum,以太坊,以太坊
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										96
									
								
								src/accounting/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/accounting/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import StringField, ValidationError
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ACCOUNT_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
 | 
					    lazy_gettext("Please select the account."))
 | 
				
			||||||
 | 
					"""The validator to check if the account code is empty."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CurrencyExists:
 | 
				
			||||||
 | 
					    """The validator to check if the account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if db.session.get(Currency, field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The currency does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountExists:
 | 
				
			||||||
 | 
					    """The validator to check if the account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if Account.find_by_code(field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IsDebitAccount:
 | 
				
			||||||
 | 
					    """The validator to check if the account is for debit line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message: str | LazyString):
 | 
				
			||||||
 | 
					        """Constructs the validator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param message: The error message.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__message: str | LazyString = message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if re.match(r"^(?:[1235689]|7[5678])", field.data) \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3351-") \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3353-"):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        raise ValidationError(self.__message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IsCreditAccount:
 | 
				
			||||||
 | 
					    """The validator to check if the account is for credit line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message: str | LazyString):
 | 
				
			||||||
 | 
					        """Constructs the validator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param message: The error message.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__message: str | LazyString = message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if re.match(r"^(?:[123489]|7[1234])", field.data) \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3351-") \
 | 
				
			||||||
 | 
					                and not field.data.startswith("3353-"):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        raise ValidationError(self.__message)
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/accounting/journal_entry/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/accounting/journal_entry/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import Flask, Blueprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def init_app(app: Flask, bp: Blueprint) -> None:
 | 
				
			||||||
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param app: The Flask application.
 | 
				
			||||||
 | 
					    :param bp: The blueprint of the accounting application.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    from .converters import JournalEntryConverter, JournalEntryTypeConverter, \
 | 
				
			||||||
 | 
					        DateConverter
 | 
				
			||||||
 | 
					    app.url_map.converters["journalEntry"] = JournalEntryConverter
 | 
				
			||||||
 | 
					    app.url_map.converters["journalEntryType"] = JournalEntryTypeConverter
 | 
				
			||||||
 | 
					    app.url_map.converters["date"] = DateConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .views import bp as journal_entry_bp
 | 
				
			||||||
 | 
					    bp.register_blueprint(journal_entry_bp, url_prefix="/journal-entries")
 | 
				
			||||||
							
								
								
									
										107
									
								
								src/accounting/journal_entry/converters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/accounting/journal_entry/converters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The path converters for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The journal entry converter to convert the journal entry ID from and to
 | 
				
			||||||
 | 
					    the corresponding journal entry in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> JournalEntry:
 | 
				
			||||||
 | 
					        """Converts a journal entry ID to a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry ID.
 | 
				
			||||||
 | 
					        :return: The corresponding journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        journal_entry: JournalEntry | None = JournalEntry.query\
 | 
				
			||||||
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
 | 
					            .filter(JournalEntry.id == value)\
 | 
				
			||||||
 | 
					            .options(selectinload(JournalEntry.line_items)
 | 
				
			||||||
 | 
					                     .selectinload(JournalEntryLineItem.offsets)
 | 
				
			||||||
 | 
					                     .selectinload(JournalEntryLineItem.journal_entry))\
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					        if journal_entry is None:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return journal_entry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Converts a journal entry to its ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry.
 | 
				
			||||||
 | 
					        :return: The ID.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(value.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryTypeConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The journal entry converter to convert the journal entry type ID from
 | 
				
			||||||
 | 
					    and to the corresponding journal entry type in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> JournalEntryType:
 | 
				
			||||||
 | 
					        """Converts a journal entry ID to a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry ID.
 | 
				
			||||||
 | 
					        :return: The corresponding journal entry type.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        type_dict: dict[str, JournalEntryType] \
 | 
				
			||||||
 | 
					            = {x.value: x for x in JournalEntryType}
 | 
				
			||||||
 | 
					        journal_entry_type: JournalEntryType | None = type_dict.get(value)
 | 
				
			||||||
 | 
					        if journal_entry_type is None:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return journal_entry_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: JournalEntryType) -> str:
 | 
				
			||||||
 | 
					        """Converts a journal entry type to its ID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The journal entry type.
 | 
				
			||||||
 | 
					        :return: The ID.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(value.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DateConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The date converter to convert the ISO date from and to the
 | 
				
			||||||
 | 
					    corresponding date in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> date:
 | 
				
			||||||
 | 
					        """Converts an ISO date to a date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The ISO date.
 | 
				
			||||||
 | 
					        :return: The corresponding date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return date.fromisoformat(value)
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: date) -> str:
 | 
				
			||||||
 | 
					        """Converts a date to its ISO date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The date.
 | 
				
			||||||
 | 
					        :return: The ISO date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return value.isoformat()
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/accounting/journal_entry/forms/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/accounting/journal_entry/forms/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from .reorder import sort_journal_entries_in, JournalEntryReorderForm
 | 
				
			||||||
 | 
					from .journal_entry import JournalEntryForm, CashReceiptJournalEntryForm, \
 | 
				
			||||||
 | 
					    CashDisbursementJournalEntryForm, TransferJournalEntryForm
 | 
				
			||||||
							
								
								
									
										294
									
								
								src/accounting/journal_entry/forms/currency.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								src/accounting/journal_entry/forms/currency.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The currency sub-forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import StringField, ValidationError, FieldList, IntegerField, \
 | 
				
			||||||
 | 
					    BooleanField, FormField
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.forms import CurrencyExists
 | 
				
			||||||
 | 
					from accounting.journal_entry.utils.offset_alias import offset_alias
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
 | 
					from .line_item import LineItemForm, CreditLineItemForm, DebitLineItemForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CURRENCY_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
 | 
					    lazy_gettext("Please select the currency."))
 | 
				
			||||||
 | 
					"""The validator to check if the currency code is empty."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SameCurrencyAsOriginalLineItems:
 | 
				
			||||||
 | 
					    """The validator to check if the currency is the same as the
 | 
				
			||||||
 | 
					    original line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, CurrencyForm)
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item_id: set[int] \
 | 
				
			||||||
 | 
					            = {x.original_line_item_id.data
 | 
				
			||||||
 | 
					               for x in form.line_items
 | 
				
			||||||
 | 
					               if x.original_line_item_id.data is not None}
 | 
				
			||||||
 | 
					        if len(original_line_item_id) == 0:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item_currency_codes: set[str] = set(db.session.scalars(
 | 
				
			||||||
 | 
					            sa.select(JournalEntryLineItem.currency_code)
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.id.in_(original_line_item_id))).all())
 | 
				
			||||||
 | 
					        for currency_code in original_line_item_currency_codes:
 | 
				
			||||||
 | 
					            if field.data != currency_code:
 | 
				
			||||||
 | 
					                raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                    "The currency must be the same as the"
 | 
				
			||||||
 | 
					                    " original line item."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class KeepCurrencyWhenHavingOffset:
 | 
				
			||||||
 | 
					    """The validator to check if the currency is the same when there is
 | 
				
			||||||
 | 
					    offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, CurrencyForm)
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        offset: sa.Alias = offset_alias()
 | 
				
			||||||
 | 
					        original_line_items: list[JournalEntryLineItem]\
 | 
				
			||||||
 | 
					            = JournalEntryLineItem.query\
 | 
				
			||||||
 | 
					            .join(offset, be(JournalEntryLineItem.id
 | 
				
			||||||
 | 
					                             == offset.c.original_line_item_id),
 | 
				
			||||||
 | 
					                  isouter=True)\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.id
 | 
				
			||||||
 | 
					                    .in_({x.id.data for x in form.line_items
 | 
				
			||||||
 | 
					                          if x.id.data is not None}))\
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.id,
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.currency_code)\
 | 
				
			||||||
 | 
					            .having(sa.func.count(offset.c.id) > 0).all()
 | 
				
			||||||
 | 
					        for original_line_item in original_line_items:
 | 
				
			||||||
 | 
					            if original_line_item.currency_code != field.data:
 | 
				
			||||||
 | 
					                raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                    "The currency must not be changed when there is offset."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedSomeLineItems:
 | 
				
			||||||
 | 
					    """The validator to check if there is any line item sub-form."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
				
			||||||
 | 
					        if len(field) == 0:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please add some line items."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IsBalanced:
 | 
				
			||||||
 | 
					    """The validator to check that the total amount of the debit and credit
 | 
				
			||||||
 | 
					    line items are equal."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: BooleanField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, TransferCurrencyForm)
 | 
				
			||||||
 | 
					        if len(form.debit) == 0 or len(form.credit) == 0:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if form.debit_total != form.credit_total:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The totals of the debit and credit amounts do not match."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CurrencyForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a currency in a journal entry."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
 | 
					    code = StringField()
 | 
				
			||||||
 | 
					    """The currency code."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField()
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def line_items(self) -> list[LineItemForm]:
 | 
				
			||||||
 | 
					        """Returns the line item sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line item sub-forms.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_item_forms: list[LineItemForm] = []
 | 
				
			||||||
 | 
					        if isinstance(self, CashReceiptCurrencyForm):
 | 
				
			||||||
 | 
					            line_item_forms.extend([x.form for x in self.credit])
 | 
				
			||||||
 | 
					        elif isinstance(self, CashDisbursementCurrencyForm):
 | 
				
			||||||
 | 
					            line_item_forms.extend([x.form for x in self.debit])
 | 
				
			||||||
 | 
					        elif isinstance(self, TransferCurrencyForm):
 | 
				
			||||||
 | 
					            line_item_forms.extend([x.form for x in self.debit])
 | 
				
			||||||
 | 
					            line_item_forms.extend([x.form for x in self.credit])
 | 
				
			||||||
 | 
					        return line_item_forms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_code_locked(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the currency code should not be changed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the currency code should not be changed, or False
 | 
				
			||||||
 | 
					            otherwise
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_item_forms: list[LineItemForm] = self.line_items
 | 
				
			||||||
 | 
					        original_line_item_id: set[int] \
 | 
				
			||||||
 | 
					            = {x.original_line_item_id.data for x in line_item_forms
 | 
				
			||||||
 | 
					               if x.original_line_item_id.data is not None}
 | 
				
			||||||
 | 
					        if len(original_line_item_id) > 0:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        line_item_id: set[int] = {x.id.data for x in line_item_forms
 | 
				
			||||||
 | 
					                                  if x.id.data is not None}
 | 
				
			||||||
 | 
					        select: sa.Select = sa.select(sa.func.count(JournalEntryLineItem.id))\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
 | 
					                    .in_(line_item_id))
 | 
				
			||||||
 | 
					        return db.session.scalar(select) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashReceiptCurrencyForm(CurrencyForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a currency in a
 | 
				
			||||||
 | 
					    cash receipt journal entry."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
 | 
					    code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
 | 
					                    CurrencyExists(),
 | 
				
			||||||
 | 
					                    SameCurrencyAsOriginalLineItems(),
 | 
				
			||||||
 | 
					                    KeepCurrencyWhenHavingOffset()])
 | 
				
			||||||
 | 
					    """The currency code."""
 | 
				
			||||||
 | 
					    credit = FieldList(FormField(CreditLineItemForm),
 | 
				
			||||||
 | 
					                       validators=[NeedSomeLineItems()])
 | 
				
			||||||
 | 
					    """The credit line items."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField()
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total amount of the credit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the credit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount.data for x in self.credit
 | 
				
			||||||
 | 
					                    if x.amount.data is not None])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns the credit line item errors, without the errors in their
 | 
				
			||||||
 | 
					        sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x for x in self.credit.errors
 | 
				
			||||||
 | 
					                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashDisbursementCurrencyForm(CurrencyForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a currency in a
 | 
				
			||||||
 | 
					    cash disbursement journal entry."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
 | 
					    code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
 | 
					                    CurrencyExists(),
 | 
				
			||||||
 | 
					                    SameCurrencyAsOriginalLineItems(),
 | 
				
			||||||
 | 
					                    KeepCurrencyWhenHavingOffset()])
 | 
				
			||||||
 | 
					    """The currency code."""
 | 
				
			||||||
 | 
					    debit = FieldList(FormField(DebitLineItemForm),
 | 
				
			||||||
 | 
					                      validators=[NeedSomeLineItems()])
 | 
				
			||||||
 | 
					    """The debit line items."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField()
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total amount of the debit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the debit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount.data for x in self.debit
 | 
				
			||||||
 | 
					                    if x.amount.data is not None])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns the debit line item errors, without the errors in their
 | 
				
			||||||
 | 
					        sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x for x in self.debit.errors
 | 
				
			||||||
 | 
					                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TransferCurrencyForm(CurrencyForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a currency in a transfer journal entry."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the journal entry."""
 | 
				
			||||||
 | 
					    code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[CURRENCY_REQUIRED,
 | 
				
			||||||
 | 
					                    CurrencyExists(),
 | 
				
			||||||
 | 
					                    SameCurrencyAsOriginalLineItems(),
 | 
				
			||||||
 | 
					                    KeepCurrencyWhenHavingOffset()])
 | 
				
			||||||
 | 
					    """The currency code."""
 | 
				
			||||||
 | 
					    debit = FieldList(FormField(DebitLineItemForm),
 | 
				
			||||||
 | 
					                      validators=[NeedSomeLineItems()])
 | 
				
			||||||
 | 
					    """The debit line items."""
 | 
				
			||||||
 | 
					    credit = FieldList(FormField(CreditLineItemForm),
 | 
				
			||||||
 | 
					                       validators=[NeedSomeLineItems()])
 | 
				
			||||||
 | 
					    """The credit line items."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField(validators=[IsBalanced()])
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total amount of the debit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the debit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount.data for x in self.debit
 | 
				
			||||||
 | 
					                    if x.amount.data is not None])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total amount of the credit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the credit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount.data for x in self.credit
 | 
				
			||||||
 | 
					                    if x.amount.data is not None])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns the debit line item errors, without the errors in their
 | 
				
			||||||
 | 
					        sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x for x in self.debit.errors
 | 
				
			||||||
 | 
					                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns the credit line item errors, without the errors in their
 | 
				
			||||||
 | 
					        sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x for x in self.credit.errors
 | 
				
			||||||
 | 
					                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
				
			||||||
							
								
								
									
										593
									
								
								src/accounting/journal_entry/forms/journal_entry.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										593
									
								
								src/accounting/journal_entry/forms/journal_entry.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,593 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The journal entry forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import datetime as dt
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import DateField, FieldList, FormField, TextAreaField, \
 | 
				
			||||||
 | 
					    BooleanField
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired, ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry, Account, JournalEntryLineItem, \
 | 
				
			||||||
 | 
					    JournalEntryCurrency
 | 
				
			||||||
 | 
					from accounting.journal_entry.utils.account_option import AccountOption
 | 
				
			||||||
 | 
					from accounting.journal_entry.utils.original_line_items import \
 | 
				
			||||||
 | 
					    get_selectable_original_line_items
 | 
				
			||||||
 | 
					from accounting.journal_entry.utils.description_editor import DescriptionEditor
 | 
				
			||||||
 | 
					from accounting.utils.random_id import new_id
 | 
				
			||||||
 | 
					from accounting.utils.strip_text import strip_multiline_text
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					from .currency import CurrencyForm, CashReceiptCurrencyForm, \
 | 
				
			||||||
 | 
					    CashDisbursementCurrencyForm, TransferCurrencyForm
 | 
				
			||||||
 | 
					from .line_item import LineItemForm, DebitLineItemForm, CreditLineItemForm
 | 
				
			||||||
 | 
					from .reorder import sort_journal_entries_in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DATE_REQUIRED: DataRequired = DataRequired(
 | 
				
			||||||
 | 
					    lazy_gettext("Please fill in the date."))
 | 
				
			||||||
 | 
					"""The validator to check if the date is empty."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotBeforeOriginalLineItems:
 | 
				
			||||||
 | 
					    """The validator to check if the date is not before the
 | 
				
			||||||
 | 
					    original line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        min_date: dt.date | None = form.min_date
 | 
				
			||||||
 | 
					        if min_date is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data < min_date:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The date cannot be earlier than the original line items."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotAfterOffsetItems:
 | 
				
			||||||
 | 
					    """The validator to check if the date is not after the offset items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: DateField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        max_date: dt.date | None = form.max_date
 | 
				
			||||||
 | 
					        if max_date is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data > max_date:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The date cannot be later than the offset items."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedSomeCurrencies:
 | 
				
			||||||
 | 
					    """The validator to check if there is any currency sub-form."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
				
			||||||
 | 
					        if len(field) == 0:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext("Please add some currencies."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CannotDeleteOriginalLineItemsWithOffset:
 | 
				
			||||||
 | 
					    """The validator to check the original line items with offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: FieldList) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, JournalEntryForm)
 | 
				
			||||||
 | 
					        if form.obj is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        existing_matched_original_line_item_id: set[int] \
 | 
				
			||||||
 | 
					            = {x.id for x in form.obj.line_items if len(x.offsets) > 0}
 | 
				
			||||||
 | 
					        line_item_id_in_form: set[int] \
 | 
				
			||||||
 | 
					            = {x.id.data for x in form.line_items if x.id.data is not None}
 | 
				
			||||||
 | 
					        for line_item_id in existing_matched_original_line_item_id:
 | 
				
			||||||
 | 
					            if line_item_id not in line_item_id_in_form:
 | 
				
			||||||
 | 
					                raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                    "Line items with offset cannot be deleted."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The base form to create or edit a journal entry."""
 | 
				
			||||||
 | 
					    date = DateField()
 | 
				
			||||||
 | 
					    """The date."""
 | 
				
			||||||
 | 
					    currencies = FieldList(FormField(CurrencyForm))
 | 
				
			||||||
 | 
					    """The line items categorized by their currencies."""
 | 
				
			||||||
 | 
					    note = TextAreaField()
 | 
				
			||||||
 | 
					    """The note."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        """Constructs a base journal entry form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param args: The arguments.
 | 
				
			||||||
 | 
					        :param kwargs: The keyword arguments.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.is_modified: bool = False
 | 
				
			||||||
 | 
					        """Whether the journal entry is modified during populate_obj()."""
 | 
				
			||||||
 | 
					        self.collector: t.Type[LineItemCollector] = LineItemCollector
 | 
				
			||||||
 | 
					        """The line item collector.  The default is the base abstract
 | 
				
			||||||
 | 
					        collector only to provide the correct type.  The subclass forms should
 | 
				
			||||||
 | 
					        provide their own collectors."""
 | 
				
			||||||
 | 
					        self.obj: JournalEntry | None = kwargs.get("obj")
 | 
				
			||||||
 | 
					        """The journal entry, when editing an existing one."""
 | 
				
			||||||
 | 
					        self._is_need_payable: bool = False
 | 
				
			||||||
 | 
					        """Whether we need the payable original line items."""
 | 
				
			||||||
 | 
					        self._is_need_receivable: bool = False
 | 
				
			||||||
 | 
					        """Whether we need the receivable original line items."""
 | 
				
			||||||
 | 
					        self.__original_line_item_options: list[JournalEntryLineItem] | None \
 | 
				
			||||||
 | 
					            = None
 | 
				
			||||||
 | 
					        """The options of the original line items."""
 | 
				
			||||||
 | 
					        self.__net_balance_exceeded: dict[int, LazyString] | None = None
 | 
				
			||||||
 | 
					        """The original line items whose net balances were exceeded by the
 | 
				
			||||||
 | 
					        amounts in the line item sub-forms."""
 | 
				
			||||||
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
 | 
					            line_item.journal_entry_form = self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def populate_obj(self, obj: JournalEntry) -> None:
 | 
				
			||||||
 | 
					        """Populates the form data into a journal entry object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The journal entry object.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        is_new: bool = obj.id is None
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            obj.id = new_id(JournalEntry)
 | 
				
			||||||
 | 
					        self.date: DateField
 | 
				
			||||||
 | 
					        self.__set_date(obj, self.date.data)
 | 
				
			||||||
 | 
					        obj.note = self.note.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        collector_cls: t.Type[LineItemCollector] = self.collector
 | 
				
			||||||
 | 
					        collector: collector_cls = collector_cls(self, obj)
 | 
				
			||||||
 | 
					        collector.collect()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        to_delete: set[int] = {x.id for x in obj.line_items
 | 
				
			||||||
 | 
					                               if x.id not in collector.to_keep}
 | 
				
			||||||
 | 
					        if len(to_delete) > 0:
 | 
				
			||||||
 | 
					            JournalEntryLineItem.query\
 | 
				
			||||||
 | 
					                .filter(JournalEntryLineItem.id.in_(to_delete)).delete()
 | 
				
			||||||
 | 
					            self.is_modified = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if is_new or db.session.is_modified(obj):
 | 
				
			||||||
 | 
					            self.is_modified = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
 | 
					            obj.created_by_id = current_user_pk
 | 
				
			||||||
 | 
					            obj.updated_by_id = current_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def line_items(self) -> list[LineItemForm]:
 | 
				
			||||||
 | 
					        """Collects and returns the line item sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line item sub-forms.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_items: list[LineItemForm] = []
 | 
				
			||||||
 | 
					        for currency in self.currencies:
 | 
				
			||||||
 | 
					            line_items.extend(currency.line_items)
 | 
				
			||||||
 | 
					        return line_items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __set_date(self, obj: JournalEntry, new_date: dt.date) -> None:
 | 
				
			||||||
 | 
					        """Sets the journal entry date and number.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The journal entry object.
 | 
				
			||||||
 | 
					        :param new_date: The new date.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if obj.date is None or obj.date != new_date:
 | 
				
			||||||
 | 
					            if obj.date is not None:
 | 
				
			||||||
 | 
					                sort_journal_entries_in(obj.date, obj.id)
 | 
				
			||||||
 | 
					            if self.max_date is not None and new_date == self.max_date:
 | 
				
			||||||
 | 
					                db_min_no: int | None = db.session.scalar(
 | 
				
			||||||
 | 
					                    sa.select(sa.func.min(JournalEntry.no))
 | 
				
			||||||
 | 
					                    .filter(JournalEntry.date == new_date))
 | 
				
			||||||
 | 
					                if db_min_no is None:
 | 
				
			||||||
 | 
					                    obj.date = new_date
 | 
				
			||||||
 | 
					                    obj.no = 1
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    obj.date = new_date
 | 
				
			||||||
 | 
					                    obj.no = db_min_no - 1
 | 
				
			||||||
 | 
					                    sort_journal_entries_in(new_date)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                sort_journal_entries_in(new_date, obj.id)
 | 
				
			||||||
 | 
					                count: int = JournalEntry.query\
 | 
				
			||||||
 | 
					                    .filter(JournalEntry.date == new_date).count()
 | 
				
			||||||
 | 
					                obj.date = new_date
 | 
				
			||||||
 | 
					                obj.no = count + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_account_options(self) -> list[AccountOption]:
 | 
				
			||||||
 | 
					        """The selectable debit accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        accounts: list[AccountOption] \
 | 
				
			||||||
 | 
					            = [AccountOption(x) for x in Account.selectable_debit()
 | 
				
			||||||
 | 
					               if not (x.code[0] == "2" and x.is_need_offset)]
 | 
				
			||||||
 | 
					        in_use: set[int] = set(db.session.scalars(
 | 
				
			||||||
 | 
					            sa.select(JournalEntryLineItem.account_id)
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.is_debit)
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.account_id)).all())
 | 
				
			||||||
 | 
					        for account in accounts:
 | 
				
			||||||
 | 
					            account.is_in_use = account.id in in_use
 | 
				
			||||||
 | 
					        return accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_account_options(self) -> list[AccountOption]:
 | 
				
			||||||
 | 
					        """The selectable credit accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The selectable credit accounts.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        accounts: list[AccountOption] \
 | 
				
			||||||
 | 
					            = [AccountOption(x) for x in Account.selectable_credit()
 | 
				
			||||||
 | 
					               if not (x.code[0] == "1" and x.is_need_offset)]
 | 
				
			||||||
 | 
					        in_use: set[int] = set(db.session.scalars(
 | 
				
			||||||
 | 
					            sa.select(JournalEntryLineItem.account_id)
 | 
				
			||||||
 | 
					            .filter(sa.not_(JournalEntryLineItem.is_debit))
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.account_id)).all())
 | 
				
			||||||
 | 
					        for account in accounts:
 | 
				
			||||||
 | 
					            account.is_in_use = account.id in in_use
 | 
				
			||||||
 | 
					        return accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currencies_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns the currency errors, without the errors in their sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency errors, without the errors in their sub-forms.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x for x in self.currencies.errors
 | 
				
			||||||
 | 
					                if isinstance(x, str) or isinstance(x, LazyString)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def description_editor(self) -> DescriptionEditor:
 | 
				
			||||||
 | 
					        """Returns the description editor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The description editor.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return DescriptionEditor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def original_line_item_options(self) -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					        """Returns the selectable original line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The selectable original line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.__original_line_item_options is None:
 | 
				
			||||||
 | 
					            self.__original_line_item_options \
 | 
				
			||||||
 | 
					                = get_selectable_original_line_items(
 | 
				
			||||||
 | 
					                    {x.id.data for x in self.line_items
 | 
				
			||||||
 | 
					                     if x.id.data is not None},
 | 
				
			||||||
 | 
					                    self._is_need_payable, self._is_need_receivable)
 | 
				
			||||||
 | 
					        return self.__original_line_item_options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def min_date(self) -> dt.date | None:
 | 
				
			||||||
 | 
					        """Returns the minimal available date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The minimal available date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        original_line_item_id: set[int] \
 | 
				
			||||||
 | 
					            = {x.original_line_item_id.data for x in self.line_items
 | 
				
			||||||
 | 
					               if x.original_line_item_id.data is not None}
 | 
				
			||||||
 | 
					        if len(original_line_item_id) == 0:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        select: sa.Select = sa.select(sa.func.max(JournalEntry.date))\
 | 
				
			||||||
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.id.in_(original_line_item_id))
 | 
				
			||||||
 | 
					        return db.session.scalar(select)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def max_date(self) -> dt.date | None:
 | 
				
			||||||
 | 
					        """Returns the maximum available date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The maximum available date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_item_id: set[int] = {x.id.data for x in self.line_items
 | 
				
			||||||
 | 
					                                  if x.id.data is not None}
 | 
				
			||||||
 | 
					        select: sa.Select = sa.select(sa.func.min(JournalEntry.date))\
 | 
				
			||||||
 | 
					            .join(JournalEntryLineItem)\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
 | 
					                    .in_(line_item_id))
 | 
				
			||||||
 | 
					        return db.session.scalar(select)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = t.TypeVar("T", bound=JournalEntryForm)
 | 
				
			||||||
 | 
					"""A journal entry form variant."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LineItemCollector(t.Generic[T], ABC):
 | 
				
			||||||
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, form: T, obj: JournalEntry):
 | 
				
			||||||
 | 
					        """Constructs the line item collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :param obj: The journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.form: T = form
 | 
				
			||||||
 | 
					        """The journal entry form."""
 | 
				
			||||||
 | 
					        self.__obj: JournalEntry = obj
 | 
				
			||||||
 | 
					        """The journal entry object."""
 | 
				
			||||||
 | 
					        self.__line_items: list[JournalEntryLineItem] = list(obj.line_items)
 | 
				
			||||||
 | 
					        """The existing line items."""
 | 
				
			||||||
 | 
					        self.__line_items_by_id: dict[int, JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in self.__line_items}
 | 
				
			||||||
 | 
					        """A dictionary from the line item ID to their line items."""
 | 
				
			||||||
 | 
					        self.__no_by_id: dict[int, int] \
 | 
				
			||||||
 | 
					            = {x.id: x.no for x in self.__line_items}
 | 
				
			||||||
 | 
					        """A dictionary from the line item number to their line items."""
 | 
				
			||||||
 | 
					        self.__currencies: list[JournalEntryCurrency] = obj.currencies
 | 
				
			||||||
 | 
					        """The currencies in the journal entry."""
 | 
				
			||||||
 | 
					        self._debit_no: int = 1
 | 
				
			||||||
 | 
					        """The number index for the debit line items."""
 | 
				
			||||||
 | 
					        self._credit_no: int = 1
 | 
				
			||||||
 | 
					        """The number index for the credit line items."""
 | 
				
			||||||
 | 
					        self.to_keep: set[int] = set()
 | 
				
			||||||
 | 
					        """The ID of the existing line items to keep."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def collect(self) -> set[int]:
 | 
				
			||||||
 | 
					        """Collects the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The ID of the line items to keep.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _add_line_item(self, form: LineItemForm, currency_code: str, no: int) \
 | 
				
			||||||
 | 
					            -> None:
 | 
				
			||||||
 | 
					        """Composes a line item from the form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The line item form.
 | 
				
			||||||
 | 
					        :param currency_code: The code of the currency.
 | 
				
			||||||
 | 
					        :param no: The number of the line item.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = self.__line_items_by_id.get(form.id.data)
 | 
				
			||||||
 | 
					        if line_item is not None:
 | 
				
			||||||
 | 
					            line_item.currency_code = currency_code
 | 
				
			||||||
 | 
					            form.populate_obj(line_item)
 | 
				
			||||||
 | 
					            line_item.no = no
 | 
				
			||||||
 | 
					            if db.session.is_modified(line_item):
 | 
				
			||||||
 | 
					                self.form.is_modified = True
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            line_item = JournalEntryLineItem()
 | 
				
			||||||
 | 
					            line_item.currency_code = currency_code
 | 
				
			||||||
 | 
					            form.populate_obj(line_item)
 | 
				
			||||||
 | 
					            line_item.no = no
 | 
				
			||||||
 | 
					            self.__obj.line_items.append(line_item)
 | 
				
			||||||
 | 
					            self.form.is_modified = True
 | 
				
			||||||
 | 
					        self.to_keep.add(line_item.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _make_cash_line_item(self, forms: list[LineItemForm], is_debit: bool,
 | 
				
			||||||
 | 
					                             currency_code: str, no: int) -> None:
 | 
				
			||||||
 | 
					        """Composes the cash line item at the other debit or credit of the
 | 
				
			||||||
 | 
					        cash journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param forms: The line item forms in the same currency.
 | 
				
			||||||
 | 
					        :param is_debit: True for a cash receipt journal entry, or False for a
 | 
				
			||||||
 | 
					            cash disbursement journal entry.
 | 
				
			||||||
 | 
					        :param currency_code: The code of the currency.
 | 
				
			||||||
 | 
					        :param no: The number of the line item.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        candidates: list[JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = [x for x in self.__line_items
 | 
				
			||||||
 | 
					               if x.is_debit == is_debit and x.currency_code == currency_code]
 | 
				
			||||||
 | 
					        line_item: JournalEntryLineItem
 | 
				
			||||||
 | 
					        if len(candidates) > 0:
 | 
				
			||||||
 | 
					            candidates.sort(key=lambda x: x.no)
 | 
				
			||||||
 | 
					            line_item = candidates[0]
 | 
				
			||||||
 | 
					            line_item.account_id = Account.cash().id
 | 
				
			||||||
 | 
					            line_item.description = None
 | 
				
			||||||
 | 
					            line_item.amount = sum([x.amount.data for x in forms])
 | 
				
			||||||
 | 
					            line_item.no = no
 | 
				
			||||||
 | 
					            if db.session.is_modified(line_item):
 | 
				
			||||||
 | 
					                self.form.is_modified = True
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            line_item = JournalEntryLineItem()
 | 
				
			||||||
 | 
					            line_item.id = new_id(JournalEntryLineItem)
 | 
				
			||||||
 | 
					            line_item.is_debit = is_debit
 | 
				
			||||||
 | 
					            line_item.currency_code = currency_code
 | 
				
			||||||
 | 
					            line_item.account_id = Account.cash().id
 | 
				
			||||||
 | 
					            line_item.description = None
 | 
				
			||||||
 | 
					            line_item.amount = sum([x.amount.data for x in forms])
 | 
				
			||||||
 | 
					            line_item.no = no
 | 
				
			||||||
 | 
					            self.__obj.line_items.append(line_item)
 | 
				
			||||||
 | 
					            self.form.is_modified = True
 | 
				
			||||||
 | 
					        self.to_keep.add(line_item.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _sort_line_item_forms(self, forms: list[LineItemForm]) -> None:
 | 
				
			||||||
 | 
					        """Sorts the line item sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param forms: The line item sub-forms.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        missing_no: int = 100 if len(self.__no_by_id) == 0 \
 | 
				
			||||||
 | 
					            else max(self.__no_by_id.values()) + 100
 | 
				
			||||||
 | 
					        ord_by_form: dict[LineItemForm, int] \
 | 
				
			||||||
 | 
					            = {forms[i]: i for i in range(len(forms))}
 | 
				
			||||||
 | 
					        recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
 | 
				
			||||||
 | 
					        missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
 | 
				
			||||||
 | 
					        forms.sort(key=lambda x: (x.no.data or missing_recv_no,
 | 
				
			||||||
 | 
					                                  missing_no if x.id.data is None else
 | 
				
			||||||
 | 
					                                  self.__no_by_id.get(x.id.data, missing_no),
 | 
				
			||||||
 | 
					                                  ord_by_form.get(x)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _sort_currency_forms(self, forms: list[CurrencyForm]) -> None:
 | 
				
			||||||
 | 
					        """Sorts the currency forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param forms: The currency forms.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        missing_no: int = len(self.__currencies) + 100
 | 
				
			||||||
 | 
					        no_by_code: dict[str, int] = {self.__currencies[i].code: i
 | 
				
			||||||
 | 
					                                      for i in range(len(self.__currencies))}
 | 
				
			||||||
 | 
					        ord_by_form: dict[CurrencyForm, int] \
 | 
				
			||||||
 | 
					            = {forms[i]: i for i in range(len(forms))}
 | 
				
			||||||
 | 
					        recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
 | 
				
			||||||
 | 
					        missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
 | 
				
			||||||
 | 
					        forms.sort(key=lambda x: (x.no.data or missing_recv_no,
 | 
				
			||||||
 | 
					                                  no_by_code.get(x.code.data, missing_no),
 | 
				
			||||||
 | 
					                                  ord_by_form.get(x)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashReceiptJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a cash receipt journal entry."""
 | 
				
			||||||
 | 
					    date = DateField(
 | 
				
			||||||
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
 | 
					                    NotAfterOffsetItems()])
 | 
				
			||||||
 | 
					    """The date."""
 | 
				
			||||||
 | 
					    currencies = FieldList(FormField(CashReceiptCurrencyForm), name="currency",
 | 
				
			||||||
 | 
					                           validators=[NeedSomeCurrencies()])
 | 
				
			||||||
 | 
					    """The line items categorized by their currencies."""
 | 
				
			||||||
 | 
					    note = TextAreaField(filters=[strip_multiline_text])
 | 
				
			||||||
 | 
					    """The note."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField(
 | 
				
			||||||
 | 
					        validators=[CannotDeleteOriginalLineItemsWithOffset()])
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self._is_need_receivable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Collector(LineItemCollector[CashReceiptJournalEntryForm]):
 | 
				
			||||||
 | 
					            """The line item collector for the cash receipt journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def collect(self) -> None:
 | 
				
			||||||
 | 
					                currencies: list[CashReceiptCurrencyForm] \
 | 
				
			||||||
 | 
					                    = [x.form for x in self.form.currencies]
 | 
				
			||||||
 | 
					                self._sort_currency_forms(currencies)
 | 
				
			||||||
 | 
					                for currency in currencies:
 | 
				
			||||||
 | 
					                    # The debit cash line item
 | 
				
			||||||
 | 
					                    self._make_cash_line_item(list(currency.credit), True,
 | 
				
			||||||
 | 
					                                              currency.code.data,
 | 
				
			||||||
 | 
					                                              self._debit_no)
 | 
				
			||||||
 | 
					                    self._debit_no = self._debit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # The credit forms
 | 
				
			||||||
 | 
					                    credit_forms: list[CreditLineItemForm] \
 | 
				
			||||||
 | 
					                        = [x.form for x in currency.credit]
 | 
				
			||||||
 | 
					                    self._sort_line_item_forms(credit_forms)
 | 
				
			||||||
 | 
					                    for credit_form in credit_forms:
 | 
				
			||||||
 | 
					                        self._add_line_item(credit_form, currency.code.data,
 | 
				
			||||||
 | 
					                                            self._credit_no)
 | 
				
			||||||
 | 
					                        self._credit_no = self._credit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.collector = Collector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashDisbursementJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a cash disbursement journal entry."""
 | 
				
			||||||
 | 
					    date = DateField(
 | 
				
			||||||
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
 | 
					                    NotAfterOffsetItems()])
 | 
				
			||||||
 | 
					    """The date."""
 | 
				
			||||||
 | 
					    currencies = FieldList(FormField(CashDisbursementCurrencyForm),
 | 
				
			||||||
 | 
					                           name="currency",
 | 
				
			||||||
 | 
					                           validators=[NeedSomeCurrencies()])
 | 
				
			||||||
 | 
					    """The line items categorized by their currencies."""
 | 
				
			||||||
 | 
					    note = TextAreaField(filters=[strip_multiline_text])
 | 
				
			||||||
 | 
					    """The note."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField(
 | 
				
			||||||
 | 
					        validators=[CannotDeleteOriginalLineItemsWithOffset()])
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self._is_need_payable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Collector(LineItemCollector[CashDisbursementJournalEntryForm]):
 | 
				
			||||||
 | 
					            """The line item collector for the cash disbursement journal
 | 
				
			||||||
 | 
					            entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def collect(self) -> None:
 | 
				
			||||||
 | 
					                currencies: list[CashDisbursementCurrencyForm] \
 | 
				
			||||||
 | 
					                    = [x.form for x in self.form.currencies]
 | 
				
			||||||
 | 
					                self._sort_currency_forms(currencies)
 | 
				
			||||||
 | 
					                for currency in currencies:
 | 
				
			||||||
 | 
					                    # The debit forms
 | 
				
			||||||
 | 
					                    debit_forms: list[DebitLineItemForm] \
 | 
				
			||||||
 | 
					                        = [x.form for x in currency.debit]
 | 
				
			||||||
 | 
					                    self._sort_line_item_forms(debit_forms)
 | 
				
			||||||
 | 
					                    for debit_form in debit_forms:
 | 
				
			||||||
 | 
					                        self._add_line_item(debit_form, currency.code.data,
 | 
				
			||||||
 | 
					                                            self._debit_no)
 | 
				
			||||||
 | 
					                        self._debit_no = self._debit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # The credit forms
 | 
				
			||||||
 | 
					                    self._make_cash_line_item(list(currency.debit), False,
 | 
				
			||||||
 | 
					                                              currency.code.data,
 | 
				
			||||||
 | 
					                                              self._credit_no)
 | 
				
			||||||
 | 
					                    self._credit_no = self._credit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.collector = Collector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TransferJournalEntryForm(JournalEntryForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a transfer journal entry."""
 | 
				
			||||||
 | 
					    date = DateField(
 | 
				
			||||||
 | 
					        validators=[DATE_REQUIRED,
 | 
				
			||||||
 | 
					                    NotBeforeOriginalLineItems(),
 | 
				
			||||||
 | 
					                    NotAfterOffsetItems()])
 | 
				
			||||||
 | 
					    """The date."""
 | 
				
			||||||
 | 
					    currencies = FieldList(FormField(TransferCurrencyForm), name="currency",
 | 
				
			||||||
 | 
					                           validators=[NeedSomeCurrencies()])
 | 
				
			||||||
 | 
					    """The line items categorized by their currencies."""
 | 
				
			||||||
 | 
					    note = TextAreaField(filters=[strip_multiline_text])
 | 
				
			||||||
 | 
					    """The note."""
 | 
				
			||||||
 | 
					    whole_form = BooleanField(
 | 
				
			||||||
 | 
					        validators=[CannotDeleteOriginalLineItemsWithOffset()])
 | 
				
			||||||
 | 
					    """The pseudo field for the whole form validators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self._is_need_payable = True
 | 
				
			||||||
 | 
					        self._is_need_receivable = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class Collector(LineItemCollector[TransferJournalEntryForm]):
 | 
				
			||||||
 | 
					            """The line item collector for the transfer journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def collect(self) -> None:
 | 
				
			||||||
 | 
					                currencies: list[TransferCurrencyForm] \
 | 
				
			||||||
 | 
					                    = [x.form for x in self.form.currencies]
 | 
				
			||||||
 | 
					                self._sort_currency_forms(currencies)
 | 
				
			||||||
 | 
					                for currency in currencies:
 | 
				
			||||||
 | 
					                    # The debit forms
 | 
				
			||||||
 | 
					                    debit_forms: list[DebitLineItemForm] \
 | 
				
			||||||
 | 
					                        = [x.form for x in currency.debit]
 | 
				
			||||||
 | 
					                    self._sort_line_item_forms(debit_forms)
 | 
				
			||||||
 | 
					                    for debit_form in debit_forms:
 | 
				
			||||||
 | 
					                        self._add_line_item(debit_form, currency.code.data,
 | 
				
			||||||
 | 
					                                            self._debit_no)
 | 
				
			||||||
 | 
					                        self._debit_no = self._debit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # The credit forms
 | 
				
			||||||
 | 
					                    credit_forms: list[CreditLineItemForm] \
 | 
				
			||||||
 | 
					                        = [x.form for x in currency.credit]
 | 
				
			||||||
 | 
					                    self._sort_line_item_forms(credit_forms)
 | 
				
			||||||
 | 
					                    for credit_form in credit_forms:
 | 
				
			||||||
 | 
					                        self._add_line_item(credit_form, currency.code.data,
 | 
				
			||||||
 | 
					                                            self._credit_no)
 | 
				
			||||||
 | 
					                        self._credit_no = self._credit_no + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.collector = Collector
 | 
				
			||||||
							
								
								
									
										499
									
								
								src/accounting/journal_entry/forms/line_item.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								src/accounting/journal_entry/forms/line_item.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,499 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The line item sub-forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					from wtforms import StringField, ValidationError, DecimalField, IntegerField
 | 
				
			||||||
 | 
					from wtforms.validators import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.forms import ACCOUNT_REQUIRED, AccountExists, IsDebitAccount, \
 | 
				
			||||||
 | 
					    IsCreditAccount
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.template_filters import format_amount
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.random_id import new_id
 | 
				
			||||||
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OriginalLineItemExists:
 | 
				
			||||||
 | 
					    """The validator to check if the original line item exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: IntegerField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if db.session.get(JournalEntryLineItem, field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The original line item does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OriginalLineItemOppositeDebitCredit:
 | 
				
			||||||
 | 
					    """The validator to check if the original line item is on the opposite
 | 
				
			||||||
 | 
					    debit or credit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: IntegerField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = db.session.get(JournalEntryLineItem, field.data)
 | 
				
			||||||
 | 
					        if original_line_item is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if isinstance(form, CreditLineItemForm) \
 | 
				
			||||||
 | 
					                and original_line_item.is_debit:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if isinstance(form, DebitLineItemForm) \
 | 
				
			||||||
 | 
					                and not original_line_item.is_debit:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					            "The original line item is on the same debit or credit."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OriginalLineItemNeedOffset:
 | 
				
			||||||
 | 
					    """The validator to check if the original line item needs offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: IntegerField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = db.session.get(JournalEntryLineItem, field.data)
 | 
				
			||||||
 | 
					        if original_line_item is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if not original_line_item.account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The original line item does not need offset."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OriginalLineItemNotOffset:
 | 
				
			||||||
 | 
					    """The validator to check if the original line item is not itself an
 | 
				
			||||||
 | 
					    offset item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: IntegerField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = db.session.get(JournalEntryLineItem, field.data)
 | 
				
			||||||
 | 
					        if original_line_item is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if original_line_item.original_line_item_id is not None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The original line item cannot be an offset item."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SameAccountAsOriginalLineItem:
 | 
				
			||||||
 | 
					    """The validator to check if the account is the same as the
 | 
				
			||||||
 | 
					    original line item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, LineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None or form.original_line_item_id.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = db.session.get(JournalEntryLineItem,
 | 
				
			||||||
 | 
					                             form.original_line_item_id.data)
 | 
				
			||||||
 | 
					        if original_line_item is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data != original_line_item.account_code:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account must be the same as the original line item."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class KeepAccountWhenHavingOffset:
 | 
				
			||||||
 | 
					    """The validator to check if the account is the same when having offset."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, LineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None or form.id.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        line_item: JournalEntryLineItem | None = db.session\
 | 
				
			||||||
 | 
					            .query(JournalEntryLineItem)\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.id == form.id.data)\
 | 
				
			||||||
 | 
					            .options(selectinload(JournalEntryLineItem.offsets)).first()
 | 
				
			||||||
 | 
					        if line_item is None or len(line_item.offsets) == 0:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data != line_item.account_code:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account must not be changed when there is offset."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartPayableFromDebit:
 | 
				
			||||||
 | 
					    """The validator to check that a payable line item does not start from
 | 
				
			||||||
 | 
					    debit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, DebitLineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None \
 | 
				
			||||||
 | 
					                or field.data[0] != "2" \
 | 
				
			||||||
 | 
					                or form.original_line_item_id.data is not None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "A payable line item cannot start from debit."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartReceivableFromCredit:
 | 
				
			||||||
 | 
					    """The validator to check that a receivable line item does not start
 | 
				
			||||||
 | 
					    from credit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, CreditLineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None \
 | 
				
			||||||
 | 
					                or field.data[0] != "1" \
 | 
				
			||||||
 | 
					                or form.original_line_item_id.data is not None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "A receivable line item cannot start from credit."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PositiveAmount:
 | 
				
			||||||
 | 
					    """The validator to check if the amount is positive."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: DecimalField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data <= 0:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please fill in a positive amount."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotExceedingOriginalLineItemNetBalance:
 | 
				
			||||||
 | 
					    """The validator to check if the amount exceeds the net balance of the
 | 
				
			||||||
 | 
					    original line item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: DecimalField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, LineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None or form.original_line_item_id.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        original_line_item: JournalEntryLineItem | None \
 | 
				
			||||||
 | 
					            = db.session.get(JournalEntryLineItem,
 | 
				
			||||||
 | 
					                             form.original_line_item_id.data)
 | 
				
			||||||
 | 
					        if original_line_item is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        is_debit: bool = isinstance(form, DebitLineItemForm)
 | 
				
			||||||
 | 
					        existing_line_item_id: set[int] = set()
 | 
				
			||||||
 | 
					        if form.journal_entry_form.obj is not None:
 | 
				
			||||||
 | 
					            existing_line_item_id \
 | 
				
			||||||
 | 
					                = {x.id for x in form.journal_entry_form.obj.line_items}
 | 
				
			||||||
 | 
					        offset_total_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (be(JournalEntryLineItem.is_debit == is_debit),
 | 
				
			||||||
 | 
					             JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
 | 
					        offset_total_but_form: Decimal | None = db.session.scalar(
 | 
				
			||||||
 | 
					            sa.select(offset_total_func)
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
 | 
					                       == original_line_item.id),
 | 
				
			||||||
 | 
					                    JournalEntryLineItem.id.not_in(existing_line_item_id)))
 | 
				
			||||||
 | 
					        if offset_total_but_form is None:
 | 
				
			||||||
 | 
					            offset_total_but_form = Decimal("0")
 | 
				
			||||||
 | 
					        offset_total_on_form: Decimal = sum(
 | 
				
			||||||
 | 
					            [x.amount.data for x in form.journal_entry_form.line_items
 | 
				
			||||||
 | 
					             if x.original_line_item_id.data == original_line_item.id
 | 
				
			||||||
 | 
					             and x.amount != field and x.amount.data is not None])
 | 
				
			||||||
 | 
					        net_balance: Decimal = original_line_item.amount \
 | 
				
			||||||
 | 
					            - offset_total_but_form - offset_total_on_form
 | 
				
			||||||
 | 
					        if field.data > net_balance:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The amount must not exceed the net balance %(balance)s of the"
 | 
				
			||||||
 | 
					                " original line item.", balance=format_amount(net_balance)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotLessThanOffsetTotal:
 | 
				
			||||||
 | 
					    """The validator to check if the amount is less than the offset total."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: DecimalField) -> None:
 | 
				
			||||||
 | 
					        assert isinstance(form, LineItemForm)
 | 
				
			||||||
 | 
					        if field.data is None or form.id.data is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        is_debit: bool = isinstance(form, DebitLineItemForm)
 | 
				
			||||||
 | 
					        select_offset_total: sa.Select = sa.select(sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit != is_debit,
 | 
				
			||||||
 | 
					             JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount)))\
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
 | 
					                       == form.id.data))
 | 
				
			||||||
 | 
					        offset_total: Decimal | None = db.session.scalar(select_offset_total)
 | 
				
			||||||
 | 
					        if offset_total is not None and field.data < offset_total:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The amount must not be less than the offset total %(total)s.",
 | 
				
			||||||
 | 
					                total=format_amount(offset_total)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LineItemForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The base form to create or edit a line item."""
 | 
				
			||||||
 | 
					    id = IntegerField()
 | 
				
			||||||
 | 
					    """The existing line item ID."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the currency."""
 | 
				
			||||||
 | 
					    original_line_item_id = IntegerField()
 | 
				
			||||||
 | 
					    """The Id of the original line item."""
 | 
				
			||||||
 | 
					    account_code = StringField()
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description = StringField()
 | 
				
			||||||
 | 
					    """The description."""
 | 
				
			||||||
 | 
					    amount = DecimalField()
 | 
				
			||||||
 | 
					    """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        """Constructs a base line item form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param args: The arguments.
 | 
				
			||||||
 | 
					        :param kwargs: The keyword arguments.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        from .journal_entry import JournalEntryForm
 | 
				
			||||||
 | 
					        self.journal_entry_form: JournalEntryForm | None = None
 | 
				
			||||||
 | 
					        """The source journal entry form."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_text(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the text representation of the account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The text representation of the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.account_code.data is None:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(self.account_code.data)
 | 
				
			||||||
 | 
					        if account is None:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					        return str(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __original_line_item(self) -> JournalEntryLineItem | None:
 | 
				
			||||||
 | 
					        """Returns the original line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The original line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not hasattr(self, "____original_line_item"):
 | 
				
			||||||
 | 
					            def get_line_item() -> JournalEntryLineItem | None:
 | 
				
			||||||
 | 
					                if self.original_line_item_id.data is None:
 | 
				
			||||||
 | 
					                    return None
 | 
				
			||||||
 | 
					                return db.session.get(JournalEntryLineItem,
 | 
				
			||||||
 | 
					                                      self.original_line_item_id.data)
 | 
				
			||||||
 | 
					            setattr(self, "____original_line_item", get_line_item())
 | 
				
			||||||
 | 
					        return getattr(self, "____original_line_item")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def original_line_item_date(self) -> date | None:
 | 
				
			||||||
 | 
					        """Returns the text representation of the original line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The text representation of the original line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return None if self.__original_line_item is None \
 | 
				
			||||||
 | 
					            else self.__original_line_item.journal_entry.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def original_line_item_text(self) -> str | None:
 | 
				
			||||||
 | 
					        """Returns the text representation of the original line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The text representation of the original line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return None if self.__original_line_item is None \
 | 
				
			||||||
 | 
					            else str(self.__original_line_item)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_need_offset(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the line item needs offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the line item needs offset, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.account_code.data is None:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        if self.account_code.data[0] == "1":
 | 
				
			||||||
 | 
					            if isinstance(self, CreditLineItemForm):
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        elif self.account_code.data[0] == "2":
 | 
				
			||||||
 | 
					            if isinstance(self, DebitLineItemForm):
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(self.account_code.data)
 | 
				
			||||||
 | 
					        return account is not None and account.is_need_offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def offsets(self) -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					        """Returns the offsets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The offsets.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not hasattr(self, "__offsets"):
 | 
				
			||||||
 | 
					            def get_offsets() -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					                if not self.is_need_offset or self.id.data is None:
 | 
				
			||||||
 | 
					                    return []
 | 
				
			||||||
 | 
					                return JournalEntryLineItem.query\
 | 
				
			||||||
 | 
					                    .filter(JournalEntryLineItem.original_line_item_id
 | 
				
			||||||
 | 
					                            == self.id.data)\
 | 
				
			||||||
 | 
					                    .options(selectinload(JournalEntryLineItem.journal_entry),
 | 
				
			||||||
 | 
					                             selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
 | 
					                             selectinload(JournalEntryLineItem.offsets)
 | 
				
			||||||
 | 
					                             .selectinload(
 | 
				
			||||||
 | 
					                                 JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					            setattr(self, "__offsets", get_offsets())
 | 
				
			||||||
 | 
					        return getattr(self, "__offsets")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def offset_total(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Returns the total amount of the offsets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the offsets.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not hasattr(self, "__offset_total"):
 | 
				
			||||||
 | 
					            def get_offset_total():
 | 
				
			||||||
 | 
					                if not self.is_need_offset or self.id.data is None:
 | 
				
			||||||
 | 
					                    return None
 | 
				
			||||||
 | 
					                is_debit: bool = isinstance(self, DebitLineItemForm)
 | 
				
			||||||
 | 
					                return sum([x.amount if x.is_debit != is_debit else -x.amount
 | 
				
			||||||
 | 
					                            for x in self.offsets])
 | 
				
			||||||
 | 
					            setattr(self, "__offset_total", get_offset_total())
 | 
				
			||||||
 | 
					        return getattr(self, "__offset_total")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def net_balance(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Returns the net balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The net balance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.is_need_offset or self.id.data is None \
 | 
				
			||||||
 | 
					                or self.amount.data is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        return self.amount.data - self.offset_total
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def all_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns all the errors of the form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: All the errors of the form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_errors: list[str | LazyString] = []
 | 
				
			||||||
 | 
					        for key in self.errors:
 | 
				
			||||||
 | 
					            if key != "csrf_token":
 | 
				
			||||||
 | 
					                all_errors.extend(self.errors[key])
 | 
				
			||||||
 | 
					        return all_errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DebitLineItemForm(LineItemForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a debit line item."""
 | 
				
			||||||
 | 
					    id = IntegerField()
 | 
				
			||||||
 | 
					    """The existing line item ID."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the currency."""
 | 
				
			||||||
 | 
					    original_line_item_id = IntegerField(
 | 
				
			||||||
 | 
					        validators=[Optional(),
 | 
				
			||||||
 | 
					                    OriginalLineItemExists(),
 | 
				
			||||||
 | 
					                    OriginalLineItemOppositeDebitCredit(),
 | 
				
			||||||
 | 
					                    OriginalLineItemNeedOffset(),
 | 
				
			||||||
 | 
					                    OriginalLineItemNotOffset()])
 | 
				
			||||||
 | 
					    """The ID of the original line item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsDebitAccount(lazy_gettext(
 | 
				
			||||||
 | 
					                "This account is not for debit line items.")),
 | 
				
			||||||
 | 
					            SameAccountAsOriginalLineItem(),
 | 
				
			||||||
 | 
					            KeepAccountWhenHavingOffset(),
 | 
				
			||||||
 | 
					            NotStartPayableFromDebit()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description = StringField(filters=[strip_text])
 | 
				
			||||||
 | 
					    """The description."""
 | 
				
			||||||
 | 
					    amount = DecimalField(
 | 
				
			||||||
 | 
					        validators=[PositiveAmount(),
 | 
				
			||||||
 | 
					                    NotExceedingOriginalLineItemNetBalance(),
 | 
				
			||||||
 | 
					                    NotLessThanOffsetTotal()])
 | 
				
			||||||
 | 
					    """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def populate_obj(self, obj: JournalEntryLineItem) -> None:
 | 
				
			||||||
 | 
					        """Populates the form data into a line item object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The line item object.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        is_new: bool = obj.id is None
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            obj.id = new_id(JournalEntryLineItem)
 | 
				
			||||||
 | 
					        obj.original_line_item_id = self.original_line_item_id.data
 | 
				
			||||||
 | 
					        obj.account_id = Account.find_by_code(self.account_code.data).id
 | 
				
			||||||
 | 
					        obj.description = self.description.data
 | 
				
			||||||
 | 
					        obj.is_debit = True
 | 
				
			||||||
 | 
					        obj.amount = self.amount.data
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
 | 
					            obj.created_by_id = current_user_pk
 | 
				
			||||||
 | 
					            obj.updated_by_id = current_user_pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreditLineItemForm(LineItemForm):
 | 
				
			||||||
 | 
					    """The form to create or edit a credit line item."""
 | 
				
			||||||
 | 
					    id = IntegerField()
 | 
				
			||||||
 | 
					    """The existing line item ID."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order in the currency."""
 | 
				
			||||||
 | 
					    original_line_item_id = IntegerField(
 | 
				
			||||||
 | 
					        validators=[Optional(),
 | 
				
			||||||
 | 
					                    OriginalLineItemExists(),
 | 
				
			||||||
 | 
					                    OriginalLineItemOppositeDebitCredit(),
 | 
				
			||||||
 | 
					                    OriginalLineItemNeedOffset(),
 | 
				
			||||||
 | 
					                    OriginalLineItemNotOffset()])
 | 
				
			||||||
 | 
					    """The ID of the original line item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsCreditAccount(lazy_gettext(
 | 
				
			||||||
 | 
					                "This account is not for credit line items.")),
 | 
				
			||||||
 | 
					            SameAccountAsOriginalLineItem(),
 | 
				
			||||||
 | 
					            KeepAccountWhenHavingOffset(),
 | 
				
			||||||
 | 
					            NotStartReceivableFromCredit()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description = StringField(filters=[strip_text])
 | 
				
			||||||
 | 
					    """The description."""
 | 
				
			||||||
 | 
					    amount = DecimalField(
 | 
				
			||||||
 | 
					        validators=[PositiveAmount(),
 | 
				
			||||||
 | 
					                    NotExceedingOriginalLineItemNetBalance(),
 | 
				
			||||||
 | 
					                    NotLessThanOffsetTotal()])
 | 
				
			||||||
 | 
					    """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def populate_obj(self, obj: JournalEntryLineItem) -> None:
 | 
				
			||||||
 | 
					        """Populates the form data into a line item object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The line item object.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        is_new: bool = obj.id is None
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            obj.id = new_id(JournalEntryLineItem)
 | 
				
			||||||
 | 
					        obj.original_line_item_id = self.original_line_item_id.data
 | 
				
			||||||
 | 
					        obj.account_id = Account.find_by_code(self.account_code.data).id
 | 
				
			||||||
 | 
					        obj.description = self.description.data
 | 
				
			||||||
 | 
					        obj.is_debit = False
 | 
				
			||||||
 | 
					        obj.amount = self.amount.data
 | 
				
			||||||
 | 
					        if is_new:
 | 
				
			||||||
 | 
					            current_user_pk: int = get_current_user_pk()
 | 
				
			||||||
 | 
					            obj.created_by_id = current_user_pk
 | 
				
			||||||
 | 
					            obj.updated_by_id = current_user_pk
 | 
				
			||||||
							
								
								
									
										95
									
								
								src/accounting/journal_entry/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/accounting/journal_entry/forms/reorder.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The reorder forms for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sort_journal_entries_in(journal_entry_date: date,
 | 
				
			||||||
 | 
					                            exclude: int | None = None) -> None:
 | 
				
			||||||
 | 
					    """Sorts the journal entries under a date after changing the date or
 | 
				
			||||||
 | 
					    deleting a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date of the journal entry.
 | 
				
			||||||
 | 
					    :param exclude: The journal entry ID to exclude.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					        = [JournalEntry.date == journal_entry_date]
 | 
				
			||||||
 | 
					    if exclude is not None:
 | 
				
			||||||
 | 
					        conditions.append(JournalEntry.id != exclude)
 | 
				
			||||||
 | 
					    journal_entries: list[JournalEntry] = JournalEntry.query\
 | 
				
			||||||
 | 
					        .filter(*conditions)\
 | 
				
			||||||
 | 
					        .order_by(JournalEntry.no).all()
 | 
				
			||||||
 | 
					    for i in range(len(journal_entries)):
 | 
				
			||||||
 | 
					        if journal_entries[i].no != i + 1:
 | 
				
			||||||
 | 
					            journal_entries[i].no = i + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryReorderForm:
 | 
				
			||||||
 | 
					    """The form to reorder the journal entries."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, journal_entry_date: date):
 | 
				
			||||||
 | 
					        """Constructs the form to reorder the journal entries in a day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.date: date = journal_entry_date
 | 
				
			||||||
 | 
					        self.is_modified: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save_order(self) -> None:
 | 
				
			||||||
 | 
					        """Saves the order of the account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        journal_entries: list[JournalEntry] = JournalEntry.query\
 | 
				
			||||||
 | 
					            .filter(JournalEntry.date == self.date).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Collects the specified order.
 | 
				
			||||||
 | 
					        orders: dict[JournalEntry, int] = {}
 | 
				
			||||||
 | 
					        for journal_entry in journal_entries:
 | 
				
			||||||
 | 
					            if f"{journal_entry.id}-no" in request.form:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    orders[journal_entry] \
 | 
				
			||||||
 | 
					                        = int(request.form[f"{journal_entry.id}-no"])
 | 
				
			||||||
 | 
					                except ValueError:
 | 
				
			||||||
 | 
					                    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Missing and invalid orders are appended to the end.
 | 
				
			||||||
 | 
					        missing: list[JournalEntry] \
 | 
				
			||||||
 | 
					            = [x for x in journal_entries if x not in orders]
 | 
				
			||||||
 | 
					        if len(missing) > 0:
 | 
				
			||||||
 | 
					            next_no: int = 1 if len(orders) == 0 else max(orders.values()) + 1
 | 
				
			||||||
 | 
					            for journal_entry in missing:
 | 
				
			||||||
 | 
					                orders[journal_entry] = next_no
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Sort by the specified order first, and their original order.
 | 
				
			||||||
 | 
					        journal_entries.sort(key=lambda x: (orders[x], x.no))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update the orders.
 | 
				
			||||||
 | 
					        with db.session.no_autoflush:
 | 
				
			||||||
 | 
					            for i in range(len(journal_entries)):
 | 
				
			||||||
 | 
					                if journal_entries[i].no != i + 1:
 | 
				
			||||||
 | 
					                    journal_entries[i].no = i + 1
 | 
				
			||||||
 | 
					                    self.is_modified = True
 | 
				
			||||||
							
								
								
									
										82
									
								
								src/accounting/journal_entry/template_filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/accounting/journal_entry/template_filters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The template filters for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					from html import escape
 | 
				
			||||||
 | 
					from urllib.parse import ParseResult, urlparse, parse_qsl, urlencode, \
 | 
				
			||||||
 | 
					    urlunparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def with_type(uri: str) -> str:
 | 
				
			||||||
 | 
					    """Adds the journal entry type to the URI, if it is specified.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param uri: The URI.
 | 
				
			||||||
 | 
					    :return: The result URL, optionally with the journal entry type added.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if "as" not in request.args:
 | 
				
			||||||
 | 
					        return uri
 | 
				
			||||||
 | 
					    uri_p: ParseResult = urlparse(uri)
 | 
				
			||||||
 | 
					    params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
				
			||||||
 | 
					    params = [x for x in params if x[0] != "as"]
 | 
				
			||||||
 | 
					    params.append(("as", request.args["as"]))
 | 
				
			||||||
 | 
					    parts: list[str] = list(uri_p)
 | 
				
			||||||
 | 
					    parts[4] = urlencode(params)
 | 
				
			||||||
 | 
					    return urlunparse(parts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def to_transfer(uri: str) -> str:
 | 
				
			||||||
 | 
					    """Adds the transfer journal entry type to the URI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param uri: The URI.
 | 
				
			||||||
 | 
					    :return: The result URL, with the transfer journal entry type added.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    uri_p: ParseResult = urlparse(uri)
 | 
				
			||||||
 | 
					    params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
				
			||||||
 | 
					    params = [x for x in params if x[0] != "as"]
 | 
				
			||||||
 | 
					    params.append(("as", "transfer"))
 | 
				
			||||||
 | 
					    parts: list[str] = list(uri_p)
 | 
				
			||||||
 | 
					    parts[4] = urlencode(params)
 | 
				
			||||||
 | 
					    return urlunparse(parts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_amount_input(value: Decimal | None) -> str:
 | 
				
			||||||
 | 
					    """Format an amount for an input value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The amount.
 | 
				
			||||||
 | 
					    :return: The formatted amount text for an input value.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if value is None:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    whole: int = int(value)
 | 
				
			||||||
 | 
					    frac: Decimal = (value - whole).normalize()
 | 
				
			||||||
 | 
					    return str(whole) + str(frac)[1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def text2html(value: str) -> str:
 | 
				
			||||||
 | 
					    """Converts plain text into HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The plain text.
 | 
				
			||||||
 | 
					    :return: The HTML.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    s: str = escape(value)
 | 
				
			||||||
 | 
					    s = s.replace("\n", "<br>")
 | 
				
			||||||
 | 
					    s = s.replace("  ", "  ")
 | 
				
			||||||
 | 
					    return s
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/accounting/journal_entry/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/accounting/journal_entry/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The utilities for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
							
								
								
									
										49
									
								
								src/accounting/journal_entry/utils/account_option.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/accounting/journal_entry/utils/account_option.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The account option for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from accounting.models import Account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountOption:
 | 
				
			||||||
 | 
					    """An account option."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, account: Account):
 | 
				
			||||||
 | 
					        """Constructs an account option.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.id: str = account.id
 | 
				
			||||||
 | 
					        """The account ID."""
 | 
				
			||||||
 | 
					        self.code: str = account.code
 | 
				
			||||||
 | 
					        """The account code."""
 | 
				
			||||||
 | 
					        self.query_values: list[str] = account.query_values
 | 
				
			||||||
 | 
					        """The values to be queried."""
 | 
				
			||||||
 | 
					        self.__str: str = str(account)
 | 
				
			||||||
 | 
					        """The string representation of the account option."""
 | 
				
			||||||
 | 
					        self.is_in_use: bool = False
 | 
				
			||||||
 | 
					        """True if this account is in use, or False otherwise."""
 | 
				
			||||||
 | 
					        self.is_need_offset: bool = account.is_need_offset
 | 
				
			||||||
 | 
					        """True if this account needs offset, or False otherwise."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the string representation of the account option.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The string representation of the account option.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__str
 | 
				
			||||||
							
								
								
									
										344
									
								
								src/accounting/journal_entry/utils/description_editor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								src/accounting/journal_entry/utils/description_editor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,344 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The description editor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import Account, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.options import options, Recurring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionAccount:
 | 
				
			||||||
 | 
					    """An account for a description tag."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, account: Account, freq: int):
 | 
				
			||||||
 | 
					        """Constructs an account for a description tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param freq: The frequency of the tag with the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.id: int = account.id
 | 
				
			||||||
 | 
					        """The account ID."""
 | 
				
			||||||
 | 
					        self.code: str = account.code
 | 
				
			||||||
 | 
					        """The account code."""
 | 
				
			||||||
 | 
					        self.freq: int = freq
 | 
				
			||||||
 | 
					        """The frequency of the tag with the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the string representation of the account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The string representation of the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return str(self.account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_freq(self, freq: int) -> None:
 | 
				
			||||||
 | 
					        """Adds the frequency of an account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param freq: The frequency of the tag name with the account.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.freq = self.freq + freq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionTag:
 | 
				
			||||||
 | 
					    """A description tag."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name: str):
 | 
				
			||||||
 | 
					        """Constructs a description tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param name: The tag name.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.name: str = name
 | 
				
			||||||
 | 
					        """The tag name."""
 | 
				
			||||||
 | 
					        self.__account_dict: dict[int, DescriptionAccount] = {}
 | 
				
			||||||
 | 
					        """The accounts that come with the tag, in the order of their
 | 
				
			||||||
 | 
					        frequency."""
 | 
				
			||||||
 | 
					        self.freq: int = 0
 | 
				
			||||||
 | 
					        """The frequency of the tag."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the string representation of the tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The string representation of the tag.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_account(self, account: Account, freq: int):
 | 
				
			||||||
 | 
					        """Adds an account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The associated account.
 | 
				
			||||||
 | 
					        :param freq: The frequency of the tag name with the account.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__account_dict[account.id] = DescriptionAccount(account, freq)
 | 
				
			||||||
 | 
					        self.freq = self.freq + freq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def accounts(self) -> list[DescriptionAccount]:
 | 
				
			||||||
 | 
					        """Returns the accounts by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The accounts by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sorted(self.__account_dict.values(), key=lambda x: -x.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_codes(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [x.code for x in self.accounts]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionType:
 | 
				
			||||||
 | 
					    """A description type"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, type_id: t.Literal["general", "travel", "bus"]):
 | 
				
			||||||
 | 
					        """Constructs a description type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param type_id: The type ID, either "general", "travel", or "bus".
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.id: t.Literal["general", "travel", "bus"] = type_id
 | 
				
			||||||
 | 
					        """The type ID."""
 | 
				
			||||||
 | 
					        self.__tag_dict: dict[str, DescriptionTag] = {}
 | 
				
			||||||
 | 
					        """A dictionary from the tag name to their corresponding tag."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_tag(self, name: str, account: Account, freq: int) -> None:
 | 
				
			||||||
 | 
					        """Adds a tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param name: The tag name.
 | 
				
			||||||
 | 
					        :param account: The associated account.
 | 
				
			||||||
 | 
					        :param freq: The frequency of the tag name with the account.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if name not in self.__tag_dict:
 | 
				
			||||||
 | 
					            self.__tag_dict[name] = DescriptionTag(name)
 | 
				
			||||||
 | 
					        self.__tag_dict[name].add_account(account, freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def tags(self) -> list[DescriptionTag]:
 | 
				
			||||||
 | 
					        """Returns the tags by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The tags by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sorted(self.__tag_dict.values(), key=lambda x: -x.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionRecurring:
 | 
				
			||||||
 | 
					    """A recurring transaction."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name: str, account: Account, description_template: str):
 | 
				
			||||||
 | 
					        """Constructs a recurring transaction.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param name: The name.
 | 
				
			||||||
 | 
					        :param description_template: The description template.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.name: str = name
 | 
				
			||||||
 | 
					        self.account: DescriptionAccount = DescriptionAccount(account, 0)
 | 
				
			||||||
 | 
					        self.description_template: str = description_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_codes(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account codes by the order of their frequencies.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.account.code]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionDebitCredit:
 | 
				
			||||||
 | 
					    """The description on debit or credit."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, debit_credit: t.Literal["debit", "credit"]):
 | 
				
			||||||
 | 
					        """Constructs the description on debit or credit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param debit_credit: Either "debit" or "credit".
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.debit_credit: t.Literal["debit", "credit"] = debit_credit
 | 
				
			||||||
 | 
					        """Either debit or credit."""
 | 
				
			||||||
 | 
					        self.general: DescriptionType = DescriptionType("general")
 | 
				
			||||||
 | 
					        """The general tags."""
 | 
				
			||||||
 | 
					        self.travel: DescriptionType = DescriptionType("travel")
 | 
				
			||||||
 | 
					        """The travel tags."""
 | 
				
			||||||
 | 
					        self.bus: DescriptionType = DescriptionType("bus")
 | 
				
			||||||
 | 
					        """The bus tags."""
 | 
				
			||||||
 | 
					        self.__type_dict: dict[t.Literal["general", "travel", "bus"],
 | 
				
			||||||
 | 
					                               DescriptionType] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in {self.general, self.travel, self.bus}}
 | 
				
			||||||
 | 
					        """A dictionary from the type ID to the corresponding tags."""
 | 
				
			||||||
 | 
					        self.recurring: list[DescriptionRecurring] = []
 | 
				
			||||||
 | 
					        """The recurring transactions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_tag(self, tag_type: t.Literal["general", "travel", "bus"],
 | 
				
			||||||
 | 
					                name: str, account: Account, freq: int) -> None:
 | 
				
			||||||
 | 
					        """Adds a tag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param tag_type: The tag type, either "general", "travel", or "bus".
 | 
				
			||||||
 | 
					        :param name: The name.
 | 
				
			||||||
 | 
					        :param account: The associated account.
 | 
				
			||||||
 | 
					        :param freq: The frequency of the tag name with the account.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__type_dict[tag_type].add_tag(name, account, freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def accounts(self) -> list[DescriptionAccount]:
 | 
				
			||||||
 | 
					        """Returns the suggested accounts of all tags in the description editor
 | 
				
			||||||
 | 
					        in debit or credit, in their frequency order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The suggested accounts of all tags, in their frequency order.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        accounts: dict[int, DescriptionAccount] = {}
 | 
				
			||||||
 | 
					        freq: dict[int, int] = {}
 | 
				
			||||||
 | 
					        for tag_type in self.__type_dict.values():
 | 
				
			||||||
 | 
					            for tag in tag_type.tags:
 | 
				
			||||||
 | 
					                for account in tag.accounts:
 | 
				
			||||||
 | 
					                    accounts[account.id] = account
 | 
				
			||||||
 | 
					                    if account.id not in freq:
 | 
				
			||||||
 | 
					                        freq[account.id] = 0
 | 
				
			||||||
 | 
					                    freq[account.id] \
 | 
				
			||||||
 | 
					                        = freq[account.id] + account.freq
 | 
				
			||||||
 | 
					        for recurring in self.recurring:
 | 
				
			||||||
 | 
					            accounts[recurring.account.id] = recurring.account
 | 
				
			||||||
 | 
					            if recurring.account.id not in freq:
 | 
				
			||||||
 | 
					                freq[recurring.account.id] = 0
 | 
				
			||||||
 | 
					        return [accounts[y] for y in sorted(freq.keys(),
 | 
				
			||||||
 | 
					                                            key=lambda x: -freq[x])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DescriptionEditor:
 | 
				
			||||||
 | 
					    """The description editor."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        """Constructs the description editor."""
 | 
				
			||||||
 | 
					        self.debit: DescriptionDebitCredit = DescriptionDebitCredit("debit")
 | 
				
			||||||
 | 
					        """The debit tags."""
 | 
				
			||||||
 | 
					        self.credit: DescriptionDebitCredit = DescriptionDebitCredit("credit")
 | 
				
			||||||
 | 
					        """The credit tags."""
 | 
				
			||||||
 | 
					        self.__init_tags()
 | 
				
			||||||
 | 
					        self.__init_recurring()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_tags(self):
 | 
				
			||||||
 | 
					        """Initializes the tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        debit_credit: sa.Label = sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, "debit"),
 | 
				
			||||||
 | 
					            else_="credit").label("debit_credit")
 | 
				
			||||||
 | 
					        tag_type: sa.Label = sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.description.like("_%—_%—_%→_%"), "bus"),
 | 
				
			||||||
 | 
					            (sa.or_(JournalEntryLineItem.description.like("_%—_%→_%"),
 | 
				
			||||||
 | 
					                    JournalEntryLineItem.description.like("_%—_%↔_%")),
 | 
				
			||||||
 | 
					             "travel"),
 | 
				
			||||||
 | 
					            else_="general").label("tag_type")
 | 
				
			||||||
 | 
					        tag: sa.Label = get_prefix(JournalEntryLineItem.description, "—")\
 | 
				
			||||||
 | 
					            .label("tag")
 | 
				
			||||||
 | 
					        select: sa.Select = sa.Select(debit_credit, tag_type, tag,
 | 
				
			||||||
 | 
					                                      JournalEntryLineItem.account_id,
 | 
				
			||||||
 | 
					                                      sa.func.count().label("freq"))\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.description.is_not(None),
 | 
				
			||||||
 | 
					                    JournalEntryLineItem.description.like("_%—_%"),
 | 
				
			||||||
 | 
					                    JournalEntryLineItem.original_line_item_id.is_(None))\
 | 
				
			||||||
 | 
					            .group_by(debit_credit, tag_type, tag,
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.account_id)
 | 
				
			||||||
 | 
					        result: list[sa.Row] = db.session.execute(select).all()
 | 
				
			||||||
 | 
					        accounts: dict[int, Account] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in Account.query
 | 
				
			||||||
 | 
					               .filter(Account.id.in_({x.account_id for x in result})).all()}
 | 
				
			||||||
 | 
					        debit_credit_dict: dict[t.Literal["debit", "credit"],
 | 
				
			||||||
 | 
					                                DescriptionDebitCredit] \
 | 
				
			||||||
 | 
					            = {x.debit_credit: x for x in {self.debit, self.credit}}
 | 
				
			||||||
 | 
					        for row in result:
 | 
				
			||||||
 | 
					            debit_credit_dict[row.debit_credit].add_tag(
 | 
				
			||||||
 | 
					                row.tag_type, row.tag, accounts[row.account_id], row.freq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init_recurring(self) -> None:
 | 
				
			||||||
 | 
					        """Initializes the recurring transactions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        recurring: Recurring = options.recurring
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = self.__get_accounts(recurring.codes)
 | 
				
			||||||
 | 
					        self.debit.recurring \
 | 
				
			||||||
 | 
					            = [DescriptionRecurring(x.name, accounts[x.account_code],
 | 
				
			||||||
 | 
					                                    x.description_template)
 | 
				
			||||||
 | 
					               for x in recurring.expenses]
 | 
				
			||||||
 | 
					        self.credit.recurring \
 | 
				
			||||||
 | 
					            = [DescriptionRecurring(x.name, accounts[x.account_code],
 | 
				
			||||||
 | 
					                                    x.description_template)
 | 
				
			||||||
 | 
					               for x in recurring.incomes]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_accounts(codes: set[str]) -> dict[str, Account]:
 | 
				
			||||||
 | 
					        """Finds and returns the accounts by codes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param codes: The account codes.
 | 
				
			||||||
 | 
					        :return: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if len(codes) == 0:
 | 
				
			||||||
 | 
					            return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def get_condition(code0: str) -> sa.BinaryExpression:
 | 
				
			||||||
 | 
					            m: re.Match = re.match(r"^(\d{4})-(\d{3})$", code0)
 | 
				
			||||||
 | 
					            assert m is not None,\
 | 
				
			||||||
 | 
					                f"Malformed account code \"{code0}\" for regular transactions."
 | 
				
			||||||
 | 
					            return sa.and_(Account.base_code == m.group(1),
 | 
				
			||||||
 | 
					                           Account.no == int(m.group(2)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [get_condition(x) for x in codes]
 | 
				
			||||||
 | 
					        accounts: dict[str, Account] \
 | 
				
			||||||
 | 
					            = {x.code: x for x in
 | 
				
			||||||
 | 
					               Account.query.filter(sa.or_(*conditions)).all()}
 | 
				
			||||||
 | 
					        for code in codes:
 | 
				
			||||||
 | 
					            assert code in accounts,\
 | 
				
			||||||
 | 
					                f"Unknown account \"{code}\" for regular transactions."
 | 
				
			||||||
 | 
					        return accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_prefix(string: str | sa.Column, separator: str | sa.Column) \
 | 
				
			||||||
 | 
					        -> sa.Function:
 | 
				
			||||||
 | 
					    """Returns the SQL function to find the prefix of a string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param string: The string.
 | 
				
			||||||
 | 
					    :param separator: The separator.
 | 
				
			||||||
 | 
					    :return: The position of the substring, starting from 1.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return sa.func.substr(string, 0, get_position(string, separator))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_position(string: str | sa.Column, substring: str | sa.Column) \
 | 
				
			||||||
 | 
					        -> sa.Function:
 | 
				
			||||||
 | 
					    """Returns the SQL function to find the position of a substring.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param string: The string.
 | 
				
			||||||
 | 
					    :param substring: The substring.
 | 
				
			||||||
 | 
					    :return: The position of the substring, starting from 1.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if db.engine.name == "postgresql":
 | 
				
			||||||
 | 
					        return sa.func.strpos(string, substring)
 | 
				
			||||||
 | 
					    return sa.func.instr(string, substring)
 | 
				
			||||||
							
								
								
									
										39
									
								
								src/accounting/journal_entry/utils/offset_alias.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/accounting/journal_entry/utils/offset_alias.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The SQLAlchemy alias for the offset items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntryLineItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def offset_alias() -> sa.Alias:
 | 
				
			||||||
 | 
					    """Returns the SQLAlchemy alias for the offset items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The SQLAlchemy alias for the offset items.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def as_from(model_cls: t.Any) -> sa.FromClause:
 | 
				
			||||||
 | 
					        return model_cls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def as_alias(alias: t.Any) -> sa.Alias:
 | 
				
			||||||
 | 
					        return alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return as_alias(sa.alias(as_from(JournalEntryLineItem), name="offset"))
 | 
				
			||||||
							
								
								
									
										336
									
								
								src/accounting/journal_entry/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/accounting/journal_entry/utils/operators.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The operators for different journal entry types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import render_template, request, abort
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					from accounting.journal_entry.forms import JournalEntryForm, \
 | 
				
			||||||
 | 
					    CashReceiptJournalEntryForm, CashDisbursementJournalEntryForm, \
 | 
				
			||||||
 | 
					    TransferJournalEntryForm
 | 
				
			||||||
 | 
					from accounting.journal_entry.forms.line_item import LineItemForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryOperator(ABC):
 | 
				
			||||||
 | 
					    """The base journal entry operator."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = -1
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_create_template(self, form: FlaskForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: FlaskForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def _line_item_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the line item sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the line item sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/include/form-line-item.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            debit_credit="DEBIT_CREDIT",
 | 
				
			||||||
 | 
					            line_item_index="LINE_ITEM_INDEX",
 | 
				
			||||||
 | 
					            form=LineItemForm())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashReceiptJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A cash receipt journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 2
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CashReceiptJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: CashReceiptJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/receipt/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.CASH_RECEIPT,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/receipt/detail.html",
 | 
				
			||||||
 | 
					                               obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: CashReceiptJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/receipt/edit.html",
 | 
				
			||||||
 | 
					                               journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					                               currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					                               line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return journal_entry.is_cash_receipt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/receipt/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            credit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CashDisbursementJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A cash disbursement journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 1
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CashDisbursementJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: CashDisbursementJournalEntryForm) \
 | 
				
			||||||
 | 
					            -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.CASH_DISBURSEMENT,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/detail.html",
 | 
				
			||||||
 | 
					            obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: CashDisbursementJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/edit.html",
 | 
				
			||||||
 | 
					            journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return journal_entry.is_cash_disbursement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/disbursement/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            debit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TransferJournalEntry(JournalEntryOperator):
 | 
				
			||||||
 | 
					    """A transfer journal entry."""
 | 
				
			||||||
 | 
					    CHECK_ORDER: int = 3
 | 
				
			||||||
 | 
					    """The order when checking the journal entry operator."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def form(self) -> t.Type[JournalEntryForm]:
 | 
				
			||||||
 | 
					        """Returns the form class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form class.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return TransferJournalEntryForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_create_template(self, form: TransferJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to create a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param form: The journal entry form.
 | 
				
			||||||
 | 
					        :return: the form to create a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/transfer/create.html",
 | 
				
			||||||
 | 
					            form=form,
 | 
				
			||||||
 | 
					            journal_entry_type=JournalEntryType.TRANSFER,
 | 
				
			||||||
 | 
					            currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					            line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_detail_template(self, journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the detail page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: the detail page.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/transfer/detail.html",
 | 
				
			||||||
 | 
					                               obj=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_edit_template(self, journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                             form: TransferJournalEntryForm) -> str:
 | 
				
			||||||
 | 
					        """Renders the template for the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :param form: The form.
 | 
				
			||||||
 | 
					        :return: the form to edit a journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template("accounting/journal-entry/transfer/edit.html",
 | 
				
			||||||
 | 
					                               journal_entry=journal_entry, form=form,
 | 
				
			||||||
 | 
					                               currency_template=self.__currency_template,
 | 
				
			||||||
 | 
					                               line_item_template=self._line_item_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_my_type(self, journal_entry: JournalEntry) -> bool:
 | 
				
			||||||
 | 
					        """Checks and returns whether the journal entry belongs to the type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					        :return: True if the journal entry belongs to the type, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __currency_template(self) -> str:
 | 
				
			||||||
 | 
					        """Renders and returns the template for the currency sub-form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template for the currency sub-form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/journal-entry/transfer/include/form-currency.html",
 | 
				
			||||||
 | 
					            currency_index="CURRENCY_INDEX",
 | 
				
			||||||
 | 
					            currency_code_data=default_currency_code(),
 | 
				
			||||||
 | 
					            debit_total="-", credit_total="-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					JOURNAL_ENTRY_TYPE_TO_OP: dict[JournalEntryType, JournalEntryOperator] \
 | 
				
			||||||
 | 
					    = {JournalEntryType.CASH_RECEIPT: CashReceiptJournalEntry(),
 | 
				
			||||||
 | 
					       JournalEntryType.CASH_DISBURSEMENT: CashDisbursementJournalEntry(),
 | 
				
			||||||
 | 
					       JournalEntryType.TRANSFER: TransferJournalEntry()}
 | 
				
			||||||
 | 
					"""The map from the journal entry types to their operators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_journal_entry_op(journal_entry: JournalEntry,
 | 
				
			||||||
 | 
					                         is_check_as: bool = False) -> JournalEntryOperator:
 | 
				
			||||||
 | 
					    """Returns the journal entry operator that may be specified in the "as"
 | 
				
			||||||
 | 
					    query parameter.  If it is not specified, check the journal entry type from
 | 
				
			||||||
 | 
					    the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :param is_check_as: True to check the "as" parameter, or False otherwise.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if is_check_as and "as" in request.args:
 | 
				
			||||||
 | 
					        type_dict: dict[str, JournalEntryType] \
 | 
				
			||||||
 | 
					            = {x.value: x for x in JournalEntryType}
 | 
				
			||||||
 | 
					        if request.args["as"] not in type_dict:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return JOURNAL_ENTRY_TYPE_TO_OP[type_dict[request.args["as"]]]
 | 
				
			||||||
 | 
					    for journal_entry_type in sorted(JOURNAL_ENTRY_TYPE_TO_OP.values(),
 | 
				
			||||||
 | 
					                                     key=lambda x: x.CHECK_ORDER):
 | 
				
			||||||
 | 
					        if journal_entry_type.is_my_type(journal_entry):
 | 
				
			||||||
 | 
					            return journal_entry_type
 | 
				
			||||||
							
								
								
									
										84
									
								
								src/accounting/journal_entry/utils/original_line_items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/accounting/journal_entry/utils/original_line_items.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The selectable original line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import Account, JournalEntry, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from .offset_alias import offset_alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_selectable_original_line_items(
 | 
				
			||||||
 | 
					        line_item_id_on_form: set[int], is_payable: bool,
 | 
				
			||||||
 | 
					        is_receivable: bool) -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					    """Queries and returns the selectable original line items, with their net
 | 
				
			||||||
 | 
					    balances.  The offset amounts of the form is excluded.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param line_item_id_on_form: The ID of the line items on the form.
 | 
				
			||||||
 | 
					    :param is_payable: True to check the payable original line items, or False
 | 
				
			||||||
 | 
					        otherwise.
 | 
				
			||||||
 | 
					    :param is_receivable: True to check the receivable original line items, or
 | 
				
			||||||
 | 
					        False otherwise.
 | 
				
			||||||
 | 
					    :return: The selectable original line items, with their net balances.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    assert is_payable or is_receivable
 | 
				
			||||||
 | 
					    offset: sa.Alias = offset_alias()
 | 
				
			||||||
 | 
					    net_balance: sa.Label = (JournalEntryLineItem.amount + sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					        (offset.c.id.in_(line_item_id_on_form), 0),
 | 
				
			||||||
 | 
					        (be(offset.c.is_debit == JournalEntryLineItem.is_debit),
 | 
				
			||||||
 | 
					         offset.c.amount),
 | 
				
			||||||
 | 
					        else_=-offset.c.amount))).label("net_balance")
 | 
				
			||||||
 | 
					    conditions: list[sa.BinaryExpression] = [Account.is_need_offset]
 | 
				
			||||||
 | 
					    sub_conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
 | 
					    if is_payable:
 | 
				
			||||||
 | 
					        sub_conditions.append(sa.and_(Account.base_code.startswith("2"),
 | 
				
			||||||
 | 
					                                      sa.not_(JournalEntryLineItem.is_debit)))
 | 
				
			||||||
 | 
					    if is_receivable:
 | 
				
			||||||
 | 
					        sub_conditions.append(sa.and_(Account.base_code.startswith("1"),
 | 
				
			||||||
 | 
					                                      JournalEntryLineItem.is_debit))
 | 
				
			||||||
 | 
					    conditions.append(sa.or_(*sub_conditions))
 | 
				
			||||||
 | 
					    select_net_balances: sa.Select \
 | 
				
			||||||
 | 
					        = sa.select(JournalEntryLineItem.id, net_balance)\
 | 
				
			||||||
 | 
					        .join(Account)\
 | 
				
			||||||
 | 
					        .join(offset, be(JournalEntryLineItem.id
 | 
				
			||||||
 | 
					                         == offset.c.original_line_item_id),
 | 
				
			||||||
 | 
					              isouter=True)\
 | 
				
			||||||
 | 
					        .filter(*conditions)\
 | 
				
			||||||
 | 
					        .group_by(JournalEntryLineItem.id)\
 | 
				
			||||||
 | 
					        .having(sa.or_(sa.func.count(offset.c.id) == 0, net_balance != 0))
 | 
				
			||||||
 | 
					    net_balances: dict[int, Decimal] \
 | 
				
			||||||
 | 
					        = {x.id: x.net_balance
 | 
				
			||||||
 | 
					           for x in db.session.execute(select_net_balances).all()}
 | 
				
			||||||
 | 
					    line_items: list[JournalEntryLineItem] = JournalEntryLineItem.query\
 | 
				
			||||||
 | 
					        .filter(JournalEntryLineItem.id.in_({x for x in net_balances}))\
 | 
				
			||||||
 | 
					        .join(JournalEntry)\
 | 
				
			||||||
 | 
					        .order_by(JournalEntry.date, JournalEntryLineItem.is_debit,
 | 
				
			||||||
 | 
					                  JournalEntryLineItem.no)\
 | 
				
			||||||
 | 
					        .options(selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
 | 
					                 selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
 | 
					                 selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					    for line_item in line_items:
 | 
				
			||||||
 | 
					        line_item.net_balance = line_item.amount \
 | 
				
			||||||
 | 
					            if net_balances[line_item.id] is None \
 | 
				
			||||||
 | 
					            else net_balances[line_item.id]
 | 
				
			||||||
 | 
					    return line_items
 | 
				
			||||||
							
								
								
									
										238
									
								
								src/accounting/journal_entry/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/accounting/journal_entry/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The views for the journal entry management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from urllib.parse import parse_qsl, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import Blueprint, render_template, session, redirect, request, \
 | 
				
			||||||
 | 
					    flash, url_for
 | 
				
			||||||
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next, or_next
 | 
				
			||||||
 | 
					from accounting.utils.permission import has_permission, can_view, can_edit
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					from accounting.utils.user import get_current_user_pk
 | 
				
			||||||
 | 
					from .forms import sort_journal_entries_in, JournalEntryReorderForm
 | 
				
			||||||
 | 
					from .template_filters import with_type, to_transfer, format_amount_input, \
 | 
				
			||||||
 | 
					    text2html
 | 
				
			||||||
 | 
					from .utils.operators import JournalEntryOperator, JOURNAL_ENTRY_TYPE_TO_OP, \
 | 
				
			||||||
 | 
					    get_journal_entry_op
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp: Blueprint = Blueprint("journal-entry", __name__)
 | 
				
			||||||
 | 
					"""The view blueprint for the journal entry management."""
 | 
				
			||||||
 | 
					bp.add_app_template_filter(with_type, "accounting_journal_entry_with_type")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(to_transfer, "accounting_journal_entry_to_transfer")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(format_amount_input,
 | 
				
			||||||
 | 
					                           "accounting_journal_entry_format_amount_input")
 | 
				
			||||||
 | 
					bp.add_app_template_filter(text2html, "accounting_journal_entry_text2html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/create/<journalEntryType:journal_entry_type>", endpoint="create")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def show_add_journal_entry_form(journal_entry_type: JournalEntryType) -> str:
 | 
				
			||||||
 | 
					    """Shows the form to add a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_type: The journal entry type.
 | 
				
			||||||
 | 
					    :return: The form to add a journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
 | 
				
			||||||
 | 
					    form: journal_entry_op.form
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(
 | 
				
			||||||
 | 
					            ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form()
 | 
				
			||||||
 | 
					        form.date.data = date.today()
 | 
				
			||||||
 | 
					    return journal_entry_op.render_create_template(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/store/<journalEntryType:journal_entry_type>", endpoint="store")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def add_journal_entry(journal_entry_type: JournalEntryType) -> redirect:
 | 
				
			||||||
 | 
					    """Adds a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_type: The journal entry type.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry detail on success, or the
 | 
				
			||||||
 | 
					        journal entry creation form on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = JOURNAL_ENTRY_TYPE_TO_OP[journal_entry_type]
 | 
				
			||||||
 | 
					    form: journal_entry_op.form = journal_entry_op.form(request.form)
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(with_type(
 | 
				
			||||||
 | 
					            url_for("accounting.journal-entry.create",
 | 
				
			||||||
 | 
					                    journal_entry_type=journal_entry_type))))
 | 
				
			||||||
 | 
					    journal_entry: JournalEntry = JournalEntry()
 | 
				
			||||||
 | 
					    form.populate_obj(journal_entry)
 | 
				
			||||||
 | 
					    db.session.add(journal_entry)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is added successfully.")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/<journalEntry:journal_entry>", endpoint="detail")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def show_journal_entry_detail(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Shows the journal entry detail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The detail.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry)
 | 
				
			||||||
 | 
					    return journal_entry_op.render_detail_template(journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/<journalEntry:journal_entry>/edit", endpoint="edit")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def show_journal_entry_edit_form(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Shows the form to edit a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The form to edit the journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry, is_check_as=True)
 | 
				
			||||||
 | 
					    form: journal_entry_op.form
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(
 | 
				
			||||||
 | 
					            ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.obj = journal_entry
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = journal_entry_op.form(obj=journal_entry)
 | 
				
			||||||
 | 
					    return journal_entry_op.render_edit_template(journal_entry, form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/<journalEntry:journal_entry>/update", endpoint="update")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def update_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			||||||
 | 
					    """Updates a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry detail on success, or the
 | 
				
			||||||
 | 
					        journal entry edit form on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entry_op: JournalEntryOperator \
 | 
				
			||||||
 | 
					        = get_journal_entry_op(journal_entry, is_check_as=True)
 | 
				
			||||||
 | 
					    form: journal_entry_op.form = journal_entry_op.form(request.form)
 | 
				
			||||||
 | 
					    form.obj = journal_entry
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(with_type(
 | 
				
			||||||
 | 
					            url_for("accounting.journal-entry.edit",
 | 
				
			||||||
 | 
					                    journal_entry=journal_entry))))
 | 
				
			||||||
 | 
					    with db.session.no_autoflush:
 | 
				
			||||||
 | 
					        form.populate_obj(journal_entry)
 | 
				
			||||||
 | 
					    if not form.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The journal entry was not modified.")),
 | 
				
			||||||
 | 
					              "success")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					    journal_entry.updated_by_id = get_current_user_pk()
 | 
				
			||||||
 | 
					    journal_entry.updated_at = sa.func.now()
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is updated successfully.")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/<journalEntry:journal_entry>/delete", endpoint="delete")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def delete_journal_entry(journal_entry: JournalEntry) -> redirect:
 | 
				
			||||||
 | 
					    """Deletes a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The redirection to the journal entry list on success, or the
 | 
				
			||||||
 | 
					        journal entry detail on error.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not journal_entry.can_delete:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The journal entry cannot be deleted.")), "error")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(__get_detail_uri(journal_entry)))
 | 
				
			||||||
 | 
					    journal_entry.delete()
 | 
				
			||||||
 | 
					    sort_journal_entries_in(journal_entry.date, journal_entry.id)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The journal entry is deleted successfully.")),
 | 
				
			||||||
 | 
					          "success")
 | 
				
			||||||
 | 
					    return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("/dates/<date:journal_entry_date>", endpoint="order")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def show_journal_entry_order(journal_entry_date: date) -> str:
 | 
				
			||||||
 | 
					    """Shows the order of the journal entries in a same date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					    :return: The order of the journal entries in the date.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    journal_entries: list[JournalEntry] = JournalEntry.query \
 | 
				
			||||||
 | 
					        .filter(JournalEntry.date == journal_entry_date) \
 | 
				
			||||||
 | 
					        .order_by(JournalEntry.no).all()
 | 
				
			||||||
 | 
					    return render_template("accounting/journal-entry/order.html",
 | 
				
			||||||
 | 
					                           date=journal_entry_date, list=journal_entries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("/dates/<date:journal_entry_date>", endpoint="sort")
 | 
				
			||||||
 | 
					@has_permission(can_edit)
 | 
				
			||||||
 | 
					def sort_journal_entries(journal_entry_date: date) -> redirect:
 | 
				
			||||||
 | 
					    """Reorders the journal entries in a date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry_date: The date.
 | 
				
			||||||
 | 
					    :return: The redirection to the incoming account or the account list.  The
 | 
				
			||||||
 | 
					        reordering operation does not fail.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form: JournalEntryReorderForm = JournalEntryReorderForm(journal_entry_date)
 | 
				
			||||||
 | 
					    form.save_order()
 | 
				
			||||||
 | 
					    if not form.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The order was not modified.")), "success")
 | 
				
			||||||
 | 
					        return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The order is updated successfully.")), "success")
 | 
				
			||||||
 | 
					    return redirect(or_next(__get_default_page_uri()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_detail_uri(journal_entry: JournalEntry) -> str:
 | 
				
			||||||
 | 
					    """Returns the detail URI of a journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param journal_entry: The journal entry.
 | 
				
			||||||
 | 
					    :return: The detail URI of the journal entry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.journal-entry.detail",
 | 
				
			||||||
 | 
					                   journal_entry=journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_default_page_uri() -> str:
 | 
				
			||||||
 | 
					    """Returns the URI for the default page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The URI for the default page.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return url_for("accounting.report.default")
 | 
				
			||||||
@@ -17,15 +17,20 @@
 | 
				
			|||||||
"""The data models.
 | 
					"""The data models.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import typing as t
 | 
					import typing as t
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from flask import current_app
 | 
					from babel import Locale
 | 
				
			||||||
from flask_babel import get_locale
 | 
					from flask_babel import get_locale, get_babel
 | 
				
			||||||
from sqlalchemy import text
 | 
					from sqlalchemy import text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounting.database import db
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
from accounting.utils.user import user_cls, user_pk_column
 | 
					from accounting.utils.user import user_cls, user_pk_column
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,7 +53,7 @@ class BaseAccount(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The string representation of the base account.
 | 
					        :return: The string representation of the base account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return F"{self.code} {self.title}"
 | 
					        return f"{self.code} {self.title.title()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def title(self) -> str:
 | 
					    def title(self) -> str:
 | 
				
			||||||
@@ -56,11 +61,11 @@ class BaseAccount(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -109,8 +114,8 @@ class Account(db.Model):
 | 
				
			|||||||
    """The account number under the base account."""
 | 
					    """The account number under the base account."""
 | 
				
			||||||
    title_l10n = db.Column("title", db.String, nullable=False)
 | 
					    title_l10n = db.Column("title", db.String, nullable=False)
 | 
				
			||||||
    """The title."""
 | 
					    """The title."""
 | 
				
			||||||
    is_offset_needed = db.Column(db.Boolean, nullable=False, default=False)
 | 
					    is_need_offset = db.Column(db.Boolean, nullable=False, default=False)
 | 
				
			||||||
    """Whether the entries of this account need offsets."""
 | 
					    """Whether the journal entry line items of this account need offset."""
 | 
				
			||||||
    created_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
					    created_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
                           server_default=db.func.now())
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
    """The time of creation."""
 | 
					    """The time of creation."""
 | 
				
			||||||
@@ -134,18 +139,15 @@ class Account(db.Model):
 | 
				
			|||||||
    l10n = db.relationship("AccountL10n", back_populates="account",
 | 
					    l10n = db.relationship("AccountL10n", back_populates="account",
 | 
				
			||||||
                           lazy=False)
 | 
					                           lazy=False)
 | 
				
			||||||
    """The localized titles."""
 | 
					    """The localized titles."""
 | 
				
			||||||
 | 
					    line_items = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
 | 
					                                 back_populates="account")
 | 
				
			||||||
 | 
					    """The journal entry line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    __CASH = "1111-001"
 | 
					    CASH_CODE: str = "1111-001"
 | 
				
			||||||
    """The code of the cash account,"""
 | 
					    """The code of the cash account,"""
 | 
				
			||||||
    __RECEIVABLE = "1141-001"
 | 
					    ACCUMULATED_CHANGE_CODE: str = "3351-001"
 | 
				
			||||||
    """The code of the receivable account,"""
 | 
					 | 
				
			||||||
    __PAYABLE = "2141-001"
 | 
					 | 
				
			||||||
    """The code of the payable account,"""
 | 
					 | 
				
			||||||
    __ACCUMULATED_CHANGE = "3351-001"
 | 
					 | 
				
			||||||
    """The code of the accumulated-change account,"""
 | 
					    """The code of the accumulated-change account,"""
 | 
				
			||||||
    __BROUGHT_FORWARD = "3352-001"
 | 
					    NET_CHANGE_CODE: str = "3353-001"
 | 
				
			||||||
    """The code of the brought-forward account,"""
 | 
					 | 
				
			||||||
    __NET_CHANGE = "3353-001"
 | 
					 | 
				
			||||||
    """The code of the net-change account,"""
 | 
					    """The code of the net-change account,"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
@@ -153,7 +155,7 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The string representation of this account.
 | 
					        :return: The string representation of this account.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return F"{self.base_code}-{self.no:03d} {self.title}"
 | 
					        return f"{self.base_code}-{self.no:03d} {self.title.title()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def code(self) -> str:
 | 
					    def code(self) -> str:
 | 
				
			||||||
@@ -161,7 +163,7 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The code.
 | 
					        :return: The code.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return F"{self.base_code}-{self.no:03d}"
 | 
					        return f"{self.base_code}-{self.no:03d}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def title(self) -> str:
 | 
					    def title(self) -> str:
 | 
				
			||||||
@@ -169,11 +171,11 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The title in the current locale.
 | 
					        :return: The title in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.title_l10n
 | 
					            return self.title_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.title
 | 
					                return l10n.title
 | 
				
			||||||
        return self.title_l10n
 | 
					        return self.title_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -187,15 +189,71 @@ class Account(db.Model):
 | 
				
			|||||||
        if self.title_l10n is None:
 | 
					        if self.title_l10n is None:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.title_l10n = value
 | 
					            self.title_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.title = value
 | 
					                l10n.title = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(AccountL10n(locale=current_locale, title=value))
 | 
					        self.l10n.append(AccountL10n(locale=str(current_locale), title=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_real(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the account is a real account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the account is a real account, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.base_code[0] in {"1", "2", "3"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_nominal(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the account is a nominal account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the account is a nominal account, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return not self.is_real
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def query_values(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the values to be queried.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values to be queried.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.code, self.title_l10n] + [x.title for x in self.l10n]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_modified(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether a product account was modified.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if modified, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if db.session.is_modified(self):
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
 | 
					            if db.session.is_modified(l10n):
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the account can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the account can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.code in {"1111-001", "3351-001", "3353-001"}:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self) -> None:
 | 
				
			||||||
 | 
					        """Deletes this account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        AccountL10n.query.filter(AccountL10n.account == self).delete()
 | 
				
			||||||
 | 
					        cls: t.Type[t.Self] = self.__class__
 | 
				
			||||||
 | 
					        cls.query.filter(cls.id == self.id).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def find_by_code(cls, code: str) -> t.Self | None:
 | 
					    def find_by_code(cls, code: str) -> t.Self | None:
 | 
				
			||||||
@@ -204,20 +262,22 @@ class Account(db.Model):
 | 
				
			|||||||
        :param code: The code.
 | 
					        :param code: The code.
 | 
				
			||||||
        :return: The account, or None if this account does not exist.
 | 
					        :return: The account, or None if this account does not exist.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        m = re.match("^([1-9]{4})-([0-9]{3})$", code)
 | 
					        m = re.match(r"^([1-9]{4})-(\d{3})$", code)
 | 
				
			||||||
        if m is None:
 | 
					        if m is None:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
        return cls.query.filter(cls.base_code == m.group(1),
 | 
					        return cls.query.filter(cls.base_code == m.group(1),
 | 
				
			||||||
                                cls.no == int(m.group(2))).first()
 | 
					                                cls.no == int(m.group(2))).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def debit(cls) -> list[t.Self]:
 | 
					    def selectable_debit(cls) -> list[t.Self]:
 | 
				
			||||||
        """Returns the debit accounts.
 | 
					        """Returns the selectable debit accounts.
 | 
				
			||||||
 | 
					        Payable line items can not start from debit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The debit accounts.
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
					        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
				
			||||||
                                       cls.base_code.startswith("2"),
 | 
					                                       sa.and_(cls.base_code.startswith("2"),
 | 
				
			||||||
 | 
					                                               sa.not_(cls.is_need_offset)),
 | 
				
			||||||
                                       cls.base_code.startswith("3"),
 | 
					                                       cls.base_code.startswith("3"),
 | 
				
			||||||
                                       cls.base_code.startswith("5"),
 | 
					                                       cls.base_code.startswith("5"),
 | 
				
			||||||
                                       cls.base_code.startswith("6"),
 | 
					                                       cls.base_code.startswith("6"),
 | 
				
			||||||
@@ -232,12 +292,14 @@ class Account(db.Model):
 | 
				
			|||||||
            .order_by(cls.base_code, cls.no).all()
 | 
					            .order_by(cls.base_code, cls.no).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def credit(cls) -> list[t.Self]:
 | 
					    def selectable_credit(cls) -> list[t.Self]:
 | 
				
			||||||
        """Returns the debit accounts.
 | 
					        """Returns the selectable debit accounts.
 | 
				
			||||||
 | 
					        Receivable line items can not start from credit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The debit accounts.
 | 
					        :return: The selectable debit accounts.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.query.filter(sa.or_(cls.base_code.startswith("1"),
 | 
					        return cls.query.filter(sa.or_(sa.and_(cls.base_code.startswith("1"),
 | 
				
			||||||
 | 
					                                               sa.not_(cls.is_need_offset)),
 | 
				
			||||||
                                       cls.base_code.startswith("2"),
 | 
					                                       cls.base_code.startswith("2"),
 | 
				
			||||||
                                       cls.base_code.startswith("3"),
 | 
					                                       cls.base_code.startswith("3"),
 | 
				
			||||||
                                       cls.base_code.startswith("4"),
 | 
					                                       cls.base_code.startswith("4"),
 | 
				
			||||||
@@ -257,23 +319,7 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The cash account
 | 
					        :return: The cash account
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.find_by_code(cls.__CASH)
 | 
					        return cls.find_by_code(cls.CASH_CODE)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def receivable(cls) -> t.Self:
 | 
					 | 
				
			||||||
        """Returns the receivable account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The receivable account
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return cls.find_by_code(cls.__RECEIVABLE)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def payable(cls) -> t.Self:
 | 
					 | 
				
			||||||
        """Returns the payable account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The payable account
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return cls.find_by_code(cls.__PAYABLE)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def accumulated_change(cls) -> t.Self:
 | 
					    def accumulated_change(cls) -> t.Self:
 | 
				
			||||||
@@ -281,45 +327,7 @@ class Account(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The accumulated-change account
 | 
					        :return: The accumulated-change account
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return cls.find_by_code(cls.__ACCUMULATED_CHANGE)
 | 
					        return cls.find_by_code(cls.ACCUMULATED_CHANGE_CODE)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def brought_forward(cls) -> t.Self:
 | 
					 | 
				
			||||||
        """Returns the brought-forward account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The brought-forward account
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return cls.find_by_code(cls.__BROUGHT_FORWARD)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod
 | 
					 | 
				
			||||||
    def net_change(cls) -> t.Self:
 | 
					 | 
				
			||||||
        """Returns the net-change account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: The net-change account
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return cls.find_by_code(cls.__NET_CHANGE)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def is_modified(self) -> bool:
 | 
					 | 
				
			||||||
        """Returns whether a product account was modified.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: True if modified, or False otherwise.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if db.session.is_modified(self):
 | 
					 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        for l10n in self.l10n:
 | 
					 | 
				
			||||||
            if db.session.is_modified(l10n):
 | 
					 | 
				
			||||||
                return True
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def delete(self) -> None:
 | 
					 | 
				
			||||||
        """Deletes this account.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        :return: None.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        AccountL10n.query.filter(AccountL10n.account == self).delete()
 | 
					 | 
				
			||||||
        cls: t.Type[t.Self] = self.__class__
 | 
					 | 
				
			||||||
        cls.query.filter(cls.id == self.id).delete()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountL10n(db.Model):
 | 
					class AccountL10n(db.Model):
 | 
				
			||||||
@@ -370,13 +378,16 @@ class Currency(db.Model):
 | 
				
			|||||||
    l10n = db.relationship("CurrencyL10n", back_populates="currency",
 | 
					    l10n = db.relationship("CurrencyL10n", back_populates="currency",
 | 
				
			||||||
                           lazy=False)
 | 
					                           lazy=False)
 | 
				
			||||||
    """The localized names."""
 | 
					    """The localized names."""
 | 
				
			||||||
 | 
					    line_items = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
 | 
					                                 back_populates="currency")
 | 
				
			||||||
 | 
					    """The journal entry line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self) -> str:
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
        """Returns the string representation of the currency.
 | 
					        """Returns the string representation of the currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :return: The string representation of the currency.
 | 
					        :return: The string representation of the currency.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return F"{self.name} ({self.code})"
 | 
					        return f"{self.name.title()} ({self.code})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def name(self) -> str:
 | 
					    def name(self) -> str:
 | 
				
			||||||
@@ -384,11 +395,11 @@ class Currency(db.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        :return: The name in the current locale.
 | 
					        :return: The name in the current locale.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            return self.name_l10n
 | 
					            return self.name_l10n
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                return l10n.name
 | 
					                return l10n.name
 | 
				
			||||||
        return self.name_l10n
 | 
					        return self.name_l10n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -402,15 +413,15 @@ class Currency(db.Model):
 | 
				
			|||||||
        if self.name_l10n is None:
 | 
					        if self.name_l10n is None:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        current_locale = str(get_locale())
 | 
					        current_locale: Locale = get_locale()
 | 
				
			||||||
        if current_locale == current_app.config["BABEL_DEFAULT_LOCALE"]:
 | 
					        if current_locale == get_babel().instance.default_locale:
 | 
				
			||||||
            self.name_l10n = value
 | 
					            self.name_l10n = value
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        for l10n in self.l10n:
 | 
					        for l10n in self.l10n:
 | 
				
			||||||
            if l10n.locale == current_locale:
 | 
					            if l10n.locale == str(current_locale):
 | 
				
			||||||
                l10n.name = value
 | 
					                l10n.name = value
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
        self.l10n.append(CurrencyL10n(locale=current_locale, name=value))
 | 
					        self.l10n.append(CurrencyL10n(locale=str(current_locale), name=value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_modified(self) -> bool:
 | 
					    def is_modified(self) -> bool:
 | 
				
			||||||
@@ -425,6 +436,17 @@ class Currency(db.Model):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the currency can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the currency can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					        if self.code == default_currency_code():
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(self.line_items) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Deletes the currency.
 | 
					        """Deletes the currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -450,3 +472,343 @@ class CurrencyL10n(db.Model):
 | 
				
			|||||||
    """The locale."""
 | 
					    """The locale."""
 | 
				
			||||||
    name = db.Column(db.String, nullable=False)
 | 
					    name = db.Column(db.String, nullable=False)
 | 
				
			||||||
    """The localized name."""
 | 
					    """The localized name."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryCurrency:
 | 
				
			||||||
 | 
					    """A currency in a journal entry."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, code: str, debit: list[JournalEntryLineItem],
 | 
				
			||||||
 | 
					                 credit: list[JournalEntryLineItem]):
 | 
				
			||||||
 | 
					        """Constructs the currency in the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param code: The currency code.
 | 
				
			||||||
 | 
					        :param debit: The debit line items.
 | 
				
			||||||
 | 
					        :param credit: The credit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.code: str = code
 | 
				
			||||||
 | 
					        """The currency code."""
 | 
				
			||||||
 | 
					        self.debit: list[JournalEntryLineItem] = debit
 | 
				
			||||||
 | 
					        """The debit line items."""
 | 
				
			||||||
 | 
					        self.credit: list[JournalEntryLineItem] = credit
 | 
				
			||||||
 | 
					        """The credit line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the currency name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency name.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return db.session.get(Currency, self.code).name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit_total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total amount of the debit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the debit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount for x in self.debit])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit_total(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the total amount of the credit line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total amount of the credit line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount for x in self.credit])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntry(db.Model):
 | 
				
			||||||
 | 
					    """A journal entry."""
 | 
				
			||||||
 | 
					    __tablename__ = "accounting_journal_entries"
 | 
				
			||||||
 | 
					    """The table name."""
 | 
				
			||||||
 | 
					    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
				
			||||||
 | 
					                   autoincrement=False)
 | 
				
			||||||
 | 
					    """The journal entry ID."""
 | 
				
			||||||
 | 
					    date = db.Column(db.Date, nullable=False)
 | 
				
			||||||
 | 
					    """The date."""
 | 
				
			||||||
 | 
					    no = db.Column(db.Integer, nullable=False, default=text("1"))
 | 
				
			||||||
 | 
					    """The account number under the date."""
 | 
				
			||||||
 | 
					    note = db.Column(db.String)
 | 
				
			||||||
 | 
					    """The note."""
 | 
				
			||||||
 | 
					    created_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of creation."""
 | 
				
			||||||
 | 
					    created_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the creator."""
 | 
				
			||||||
 | 
					    created_by = db.relationship(user_cls, foreign_keys=created_by_id)
 | 
				
			||||||
 | 
					    """The creator."""
 | 
				
			||||||
 | 
					    updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of last update."""
 | 
				
			||||||
 | 
					    updated_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the updator."""
 | 
				
			||||||
 | 
					    updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
 | 
				
			||||||
 | 
					    """The updator."""
 | 
				
			||||||
 | 
					    line_items = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
 | 
					                                 back_populates="journal_entry")
 | 
				
			||||||
 | 
					    """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the string representation of this journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The string representation of this journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.is_cash_disbursement:
 | 
				
			||||||
 | 
					            return gettext("Cash Disbursement Journal Entry#%(id)s",
 | 
				
			||||||
 | 
					                           id=self.id)
 | 
				
			||||||
 | 
					        if self.is_cash_receipt:
 | 
				
			||||||
 | 
					            return gettext("Cash Receipt Journal Entry#%(id)s", id=self.id)
 | 
				
			||||||
 | 
					        return gettext("Transfer Journal Entry#%(id)s", id=self.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currencies(self) -> list[JournalEntryCurrency]:
 | 
				
			||||||
 | 
					        """Returns the line items categorized by their currencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency categories.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        line_items: list[JournalEntryLineItem] = sorted(self.line_items,
 | 
				
			||||||
 | 
					                                                        key=lambda x: x.no)
 | 
				
			||||||
 | 
					        codes: list[str] = []
 | 
				
			||||||
 | 
					        by_currency: dict[str, list[JournalEntryLineItem]] = {}
 | 
				
			||||||
 | 
					        for line_item in line_items:
 | 
				
			||||||
 | 
					            if line_item.currency_code not in by_currency:
 | 
				
			||||||
 | 
					                codes.append(line_item.currency_code)
 | 
				
			||||||
 | 
					                by_currency[line_item.currency_code] = []
 | 
				
			||||||
 | 
					            by_currency[line_item.currency_code].append(line_item)
 | 
				
			||||||
 | 
					        return [JournalEntryCurrency(code=x,
 | 
				
			||||||
 | 
					                                     debit=[y for y in by_currency[x]
 | 
				
			||||||
 | 
					                                            if y.is_debit],
 | 
				
			||||||
 | 
					                                     credit=[y for y in by_currency[x]
 | 
				
			||||||
 | 
					                                             if not y.is_debit])
 | 
				
			||||||
 | 
					                for x in codes]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_cash_receipt(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether this is a cash receipt journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if this is a cash receipt journal entry, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for currency in self.currencies:
 | 
				
			||||||
 | 
					            if len(currency.debit) > 1:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            if currency.debit[0].account.code != Account.CASH_CODE:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_cash_disbursement(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether this is a cash disbursement journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if this is a cash disbursement journal entry, or False
 | 
				
			||||||
 | 
					            otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for currency in self.currencies:
 | 
				
			||||||
 | 
					            if len(currency.credit) > 1:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					            if currency.credit[0].account.code != Account.CASH_CODE:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def can_delete(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the journal entry can be deleted.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the journal entry can be deleted, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
 | 
					            if len(line_item.offsets) > 0:
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self) -> None:
 | 
				
			||||||
 | 
					        """Deletes the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        JournalEntryLineItem.query\
 | 
				
			||||||
 | 
					            .filter(JournalEntryLineItem.journal_entry_id == self.id).delete()
 | 
				
			||||||
 | 
					        db.session.delete(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class JournalEntryLineItem(db.Model):
 | 
				
			||||||
 | 
					    """A line item in the journal entry."""
 | 
				
			||||||
 | 
					    __tablename__ = "accounting_journal_entry_line_items"
 | 
				
			||||||
 | 
					    """The table name."""
 | 
				
			||||||
 | 
					    id = db.Column(db.Integer, nullable=False, primary_key=True,
 | 
				
			||||||
 | 
					                   autoincrement=False)
 | 
				
			||||||
 | 
					    """The line item ID."""
 | 
				
			||||||
 | 
					    journal_entry_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                                 db.ForeignKey(JournalEntry.id,
 | 
				
			||||||
 | 
					                                               onupdate="CASCADE",
 | 
				
			||||||
 | 
					                                               ondelete="CASCADE"),
 | 
				
			||||||
 | 
					                                 nullable=False)
 | 
				
			||||||
 | 
					    """The journal entry ID."""
 | 
				
			||||||
 | 
					    journal_entry = db.relationship(JournalEntry, back_populates="line_items")
 | 
				
			||||||
 | 
					    """The journal entry."""
 | 
				
			||||||
 | 
					    is_debit = db.Column(db.Boolean, nullable=False)
 | 
				
			||||||
 | 
					    """True for a debit line item, or False for a credit line item."""
 | 
				
			||||||
 | 
					    no = db.Column(db.Integer, nullable=False)
 | 
				
			||||||
 | 
					    """The line item number under the journal entry and debit or credit."""
 | 
				
			||||||
 | 
					    original_line_item_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                                      db.ForeignKey(id, onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                                      nullable=True)
 | 
				
			||||||
 | 
					    """The ID of the original line item."""
 | 
				
			||||||
 | 
					    original_line_item = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
 | 
					                                         back_populates="offsets",
 | 
				
			||||||
 | 
					                                         remote_side=id, passive_deletes=True)
 | 
				
			||||||
 | 
					    """The original line item."""
 | 
				
			||||||
 | 
					    offsets = db.relationship("JournalEntryLineItem",
 | 
				
			||||||
 | 
					                              back_populates="original_line_item")
 | 
				
			||||||
 | 
					    """The offset items."""
 | 
				
			||||||
 | 
					    currency_code = db.Column(db.String,
 | 
				
			||||||
 | 
					                              db.ForeignKey(Currency.code, onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The currency code."""
 | 
				
			||||||
 | 
					    currency = db.relationship(Currency, back_populates="line_items")
 | 
				
			||||||
 | 
					    """The currency."""
 | 
				
			||||||
 | 
					    account_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                           db.ForeignKey(Account.id,
 | 
				
			||||||
 | 
					                                         onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                           nullable=False)
 | 
				
			||||||
 | 
					    """The account ID."""
 | 
				
			||||||
 | 
					    account = db.relationship(Account, back_populates="line_items", lazy=False)
 | 
				
			||||||
 | 
					    """The account."""
 | 
				
			||||||
 | 
					    description = db.Column(db.String, nullable=True)
 | 
				
			||||||
 | 
					    """The description."""
 | 
				
			||||||
 | 
					    amount = db.Column(db.Numeric(14, 2), nullable=False)
 | 
				
			||||||
 | 
					    """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the string representation of the line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The string representation of the line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not hasattr(self, "__str"):
 | 
				
			||||||
 | 
					            from accounting.template_filters import format_date, format_amount
 | 
				
			||||||
 | 
					            setattr(self, "__str",
 | 
				
			||||||
 | 
					                    gettext("%(date)s %(description)s %(amount)s",
 | 
				
			||||||
 | 
					                            date=format_date(self.journal_entry.date),
 | 
				
			||||||
 | 
					                            description="" if self.description is None
 | 
				
			||||||
 | 
					                            else self.description,
 | 
				
			||||||
 | 
					                            amount=format_amount(self.amount)))
 | 
				
			||||||
 | 
					        return getattr(self, "__str")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_code(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the account code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account code.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.account.code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def debit(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Returns the debit amount.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The debit amount, or None if this is not a debit line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.amount if self.is_debit else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_need_offset(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the line item needs offset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if the line item needs offset, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.account.is_need_offset:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        if self.account.base_code[0] == "1" and not self.is_debit:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        if self.account.base_code[0] == "2" and self.is_debit:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def credit(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Returns the credit amount.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The credit amount, or None if this is not a credit line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return None if self.is_debit else self.amount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def net_balance(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the net balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The net balance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not hasattr(self, "__net_balance"):
 | 
				
			||||||
 | 
					            setattr(self, "__net_balance", self.amount + sum(
 | 
				
			||||||
 | 
					                [x.amount if x.is_debit == self.is_debit else -x.amount
 | 
				
			||||||
 | 
					                 for x in self.offsets]))
 | 
				
			||||||
 | 
					        return getattr(self, "__net_balance")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @net_balance.setter
 | 
				
			||||||
 | 
					    def net_balance(self, net_balance: Decimal) -> None:
 | 
				
			||||||
 | 
					        """Sets the net balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param net_balance: The net balance.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        setattr(self, "__net_balance", net_balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def query_values(self) -> list[str]:
 | 
				
			||||||
 | 
					        """Returns the values to be queried.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values to be queried.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        def format_amount(value: Decimal) -> str:
 | 
				
			||||||
 | 
					            whole: int = int(value)
 | 
				
			||||||
 | 
					            frac: Decimal = (value - whole).normalize()
 | 
				
			||||||
 | 
					            return str(whole) + str(abs(frac))[1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        journal_entry_day: date = self.journal_entry.date
 | 
				
			||||||
 | 
					        description: str = "" if self.description is None else self.description
 | 
				
			||||||
 | 
					        return [description,
 | 
				
			||||||
 | 
					                str(journal_entry_day.year),
 | 
				
			||||||
 | 
					                "{}/{}".format(journal_entry_day.year,
 | 
				
			||||||
 | 
					                               journal_entry_day.month),
 | 
				
			||||||
 | 
					                "{}/{}".format(journal_entry_day.month,
 | 
				
			||||||
 | 
					                               journal_entry_day.day),
 | 
				
			||||||
 | 
					                "{}/{}/{}".format(journal_entry_day.year,
 | 
				
			||||||
 | 
					                                  journal_entry_day.month,
 | 
				
			||||||
 | 
					                                  journal_entry_day.day),
 | 
				
			||||||
 | 
					                format_amount(self.amount)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Option(db.Model):
 | 
				
			||||||
 | 
					    """An option."""
 | 
				
			||||||
 | 
					    __tablename__ = "accounting_options"
 | 
				
			||||||
 | 
					    """The table name."""
 | 
				
			||||||
 | 
					    name = db.Column(db.String, nullable=False, primary_key=True)
 | 
				
			||||||
 | 
					    """The name."""
 | 
				
			||||||
 | 
					    value = db.Column(db.Text, nullable=False)
 | 
				
			||||||
 | 
					    """The option value."""
 | 
				
			||||||
 | 
					    created_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of creation."""
 | 
				
			||||||
 | 
					    created_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the creator."""
 | 
				
			||||||
 | 
					    created_by = db.relationship(user_cls, foreign_keys=created_by_id)
 | 
				
			||||||
 | 
					    """The creator."""
 | 
				
			||||||
 | 
					    updated_at = db.Column(db.DateTime(timezone=True), nullable=False,
 | 
				
			||||||
 | 
					                           server_default=db.func.now())
 | 
				
			||||||
 | 
					    """The time of last update."""
 | 
				
			||||||
 | 
					    updated_by_id = db.Column(db.Integer,
 | 
				
			||||||
 | 
					                              db.ForeignKey(user_pk_column,
 | 
				
			||||||
 | 
					                                            onupdate="CASCADE"),
 | 
				
			||||||
 | 
					                              nullable=False)
 | 
				
			||||||
 | 
					    """The ID of the updator."""
 | 
				
			||||||
 | 
					    updated_by = db.relationship(user_cls, foreign_keys=updated_by_id)
 | 
				
			||||||
 | 
					    """The updator."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
# The Mia! Accounting Flask Project.
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/25
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  Copyright (c) 2023 imacat.
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
@@ -14,26 +14,17 @@
 | 
				
			|||||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
#  See the License for the specific language governing permissions and
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
#  limitations under the License.
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The option management.
 | 
				
			||||||
"""The database instance factory for the base account management.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This is to overcome the problem that the database instance needs to be
 | 
					 | 
				
			||||||
initialized at compile time, but as a submodule it is only available at run
 | 
					 | 
				
			||||||
time.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					from flask import Blueprint
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
db: SQLAlchemy = SQLAlchemy()
 | 
					 | 
				
			||||||
"""The database instance."""
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def set_db(new_db: SQLAlchemy) -> None:
 | 
					def init_app(bp: Blueprint) -> None:
 | 
				
			||||||
    """Sets the database instance.
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param new_db: The database instance.
 | 
					    :param bp: The blueprint of the accounting application.
 | 
				
			||||||
    :return: None.
 | 
					    :return: None.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    global db
 | 
					    from .views import bp as option_bp
 | 
				
			||||||
    db = new_db
 | 
					    bp.register_blueprint(option_bp, url_prefix="/options")
 | 
				
			||||||
							
								
								
									
										269
									
								
								src/accounting/option/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/accounting/option/forms.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,269 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The forms for the option management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import render_template
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					from flask_wtf import FlaskForm
 | 
				
			||||||
 | 
					from wtforms import StringField, FieldList, FormField, IntegerField
 | 
				
			||||||
 | 
					from wtforms.validators import DataRequired, ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.forms import ACCOUNT_REQUIRED, CurrencyExists, AccountExists, \
 | 
				
			||||||
 | 
					    IsDebitAccount, IsCreditAccount
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.models import Account
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import Options
 | 
				
			||||||
 | 
					from accounting.utils.strip_text import strip_text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CurrentAccountExists:
 | 
				
			||||||
 | 
					    """The validator to check that the current account exists."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if Account.find_by_code(field.data) is None:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "The account does not exist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountNotCurrent:
 | 
				
			||||||
 | 
					    """The validator to check that the account is a current account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if field.data[:2] not in {"11", "12", "21", "22"}:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "This is not a current account."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartPayableFromExpense:
 | 
				
			||||||
 | 
					    """The validator to check that a payable line item does not start from
 | 
				
			||||||
 | 
					    expense."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data[0] != "2":
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "You cannot select a payable account as expense."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotStartReceivableFromIncome:
 | 
				
			||||||
 | 
					    """The validator to check that a receivable line item does not start
 | 
				
			||||||
 | 
					    from income."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __call__(self, form: FlaskForm, field: StringField) -> None:
 | 
				
			||||||
 | 
					        if field.data is None or field.data[0] != "1":
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(field.data)
 | 
				
			||||||
 | 
					        if account is not None and account.is_need_offset:
 | 
				
			||||||
 | 
					            raise ValidationError(lazy_gettext(
 | 
				
			||||||
 | 
					                "You cannot select a receivable account as income."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringItemForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The base sub-form to add or update the recurring item."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField()
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField()
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField()
 | 
				
			||||||
 | 
					    """The description template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_text(self) -> str | None:
 | 
				
			||||||
 | 
					        """Returns the account text.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account text.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.account_code.data is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(self.account_code.data)
 | 
				
			||||||
 | 
					        return None if account is None else str(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def all_errors(self) -> list[str | LazyString]:
 | 
				
			||||||
 | 
					        """Returns all the errors of the form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: All the errors of the form.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_errors: list[str | LazyString] = []
 | 
				
			||||||
 | 
					        for key in self.errors:
 | 
				
			||||||
 | 
					            if key != "csrf_token":
 | 
				
			||||||
 | 
					                all_errors.extend(self.errors[key])
 | 
				
			||||||
 | 
					        return all_errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringExpenseForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form to add or update the recurring expenses."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the name."))])
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsDebitAccount(lazy_gettext("This account is not for expense.")),
 | 
				
			||||||
 | 
					            NotStartPayableFromExpense()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please fill in the description template."))])
 | 
				
			||||||
 | 
					    """The template for the line item description."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringIncomeForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form to add or update the recurring incomes."""
 | 
				
			||||||
 | 
					    no = IntegerField()
 | 
				
			||||||
 | 
					    """The order number of this recurring item."""
 | 
				
			||||||
 | 
					    name = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[DataRequired(lazy_gettext("Please fill in the name."))])
 | 
				
			||||||
 | 
					    """The name of the recurring item."""
 | 
				
			||||||
 | 
					    account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            ACCOUNT_REQUIRED,
 | 
				
			||||||
 | 
					            AccountExists(),
 | 
				
			||||||
 | 
					            IsCreditAccount(lazy_gettext("This account is not for income.")),
 | 
				
			||||||
 | 
					            NotStartReceivableFromIncome()])
 | 
				
			||||||
 | 
					    """The account code."""
 | 
				
			||||||
 | 
					    description_template = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please fill in the description template."))])
 | 
				
			||||||
 | 
					    """The description template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecurringForm(RecurringItemForm):
 | 
				
			||||||
 | 
					    """The sub-form for the recurring expenses and incomes."""
 | 
				
			||||||
 | 
					    expenses = FieldList(FormField(RecurringExpenseForm), name="expense")
 | 
				
			||||||
 | 
					    """The recurring expenses."""
 | 
				
			||||||
 | 
					    incomes = FieldList(FormField(RecurringIncomeForm), name="income")
 | 
				
			||||||
 | 
					    """The recurring incomes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def item_template(self) -> str:
 | 
				
			||||||
 | 
					        """Returns the template of a recurring item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The template of a recurring item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return render_template(
 | 
				
			||||||
 | 
					            "accounting/option/include/form-recurring-item.html",
 | 
				
			||||||
 | 
					            expense_income="EXPENSE_INCOME",
 | 
				
			||||||
 | 
					            item_index="ITEM_INDEX",
 | 
				
			||||||
 | 
					            form=RecurringItemForm())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def expense_accounts(self) -> list[Account]:
 | 
				
			||||||
 | 
					        """The expense accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return Account.selectable_debit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def income_accounts(self) -> list[Account]:
 | 
				
			||||||
 | 
					        """The income accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return Account.selectable_credit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def as_data(self) -> dict[str, list[tuple[str, str, str]]]:
 | 
				
			||||||
 | 
					        """Returns the form data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The form data.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        def as_tuple(item: RecurringItemForm) -> tuple[str, str, str]:
 | 
				
			||||||
 | 
					            return (item.name.data, item.account_code.data,
 | 
				
			||||||
 | 
					                    item.description_template.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses: list[RecurringItemForm] = [x.form for x in self.expenses]
 | 
				
			||||||
 | 
					        self.__sort_item_forms(expenses)
 | 
				
			||||||
 | 
					        incomes: list[RecurringItemForm] = [x.form for x in self.incomes]
 | 
				
			||||||
 | 
					        self.__sort_item_forms(incomes)
 | 
				
			||||||
 | 
					        return {"expense": [as_tuple(x) for x in expenses],
 | 
				
			||||||
 | 
					                "income": [as_tuple(x) for x in incomes]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __sort_item_forms(forms: list[RecurringItemForm]) -> None:
 | 
				
			||||||
 | 
					        """Sorts the recurring item sub-forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param forms: The recurring item sub-forms.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        ord_by_form: dict[RecurringItemForm, int] \
 | 
				
			||||||
 | 
					            = {forms[i]: i for i in range(len(forms))}
 | 
				
			||||||
 | 
					        recv_no: set[int] = {x.no.data for x in forms if x.no.data is not None}
 | 
				
			||||||
 | 
					        missing_recv_no: int = 100 if len(recv_no) == 0 else max(recv_no) + 100
 | 
				
			||||||
 | 
					        forms.sort(key=lambda x: (x.no.data or missing_recv_no,
 | 
				
			||||||
 | 
					                                  ord_by_form.get(x)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OptionForm(FlaskForm):
 | 
				
			||||||
 | 
					    """The form to update the options."""
 | 
				
			||||||
 | 
					    default_currency_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext("Please select the default currency.")),
 | 
				
			||||||
 | 
					            CurrencyExists()])
 | 
				
			||||||
 | 
					    """The default currency code."""
 | 
				
			||||||
 | 
					    default_ie_account_code = StringField(
 | 
				
			||||||
 | 
					        filters=[strip_text],
 | 
				
			||||||
 | 
					        validators=[
 | 
				
			||||||
 | 
					            DataRequired(lazy_gettext(
 | 
				
			||||||
 | 
					                "Please select the default account"
 | 
				
			||||||
 | 
					                " for the income and expenses log.")),
 | 
				
			||||||
 | 
					            CurrentAccountExists(),
 | 
				
			||||||
 | 
					            AccountNotCurrent()])
 | 
				
			||||||
 | 
					    """The default account code for the income and expenses log."""
 | 
				
			||||||
 | 
					    recurring = FormField(RecurringForm)
 | 
				
			||||||
 | 
					    """The recurring expenses and incomes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def populate_obj(self, obj: Options) -> None:
 | 
				
			||||||
 | 
					        """Populates the form data into a currency object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param obj: The currency object.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        obj.default_currency_code = self.default_currency_code.data
 | 
				
			||||||
 | 
					        obj.default_ie_account_code = self.default_ie_account_code.data
 | 
				
			||||||
 | 
					        obj.recurring_data = self.recurring.form.as_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def current_accounts(self) -> list[CurrentAccount]:
 | 
				
			||||||
 | 
					        """Returns the current accounts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The current accounts.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return CurrentAccount.accounts()
 | 
				
			||||||
							
								
								
									
										83
									
								
								src/accounting/option/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/accounting/option/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The views for the option management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from urllib.parse import parse_qsl, urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Blueprint, render_template, redirect, session, request, \
 | 
				
			||||||
 | 
					    flash, url_for
 | 
				
			||||||
 | 
					from werkzeug.datastructures import ImmutableMultiDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import lazy_gettext
 | 
				
			||||||
 | 
					from accounting.utils.cast import s
 | 
				
			||||||
 | 
					from accounting.utils.flash_errors import flash_form_errors
 | 
				
			||||||
 | 
					from accounting.utils.next_uri import inherit_next
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					from accounting.utils.permission import has_permission, can_admin
 | 
				
			||||||
 | 
					from .forms import OptionForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp: Blueprint = Blueprint("option", __name__)
 | 
				
			||||||
 | 
					"""The view blueprint for the currency management."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("", endpoint="detail")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def show_options() -> str:
 | 
				
			||||||
 | 
					    """Shows the options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The options.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return render_template("accounting/option/detail.html", obj=options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("edit", endpoint="edit")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def show_option_form() -> str:
 | 
				
			||||||
 | 
					    """Shows the option form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The option form.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form: OptionForm
 | 
				
			||||||
 | 
					    if "form" in session:
 | 
				
			||||||
 | 
					        form = OptionForm(ImmutableMultiDict(parse_qsl(session["form"])))
 | 
				
			||||||
 | 
					        del session["form"]
 | 
				
			||||||
 | 
					        form.validate()
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = OptionForm(obj=options)
 | 
				
			||||||
 | 
					    return render_template("accounting/option/form.html", form=form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.post("update", endpoint="update")
 | 
				
			||||||
 | 
					@has_permission(can_admin)
 | 
				
			||||||
 | 
					def update_options() -> redirect:
 | 
				
			||||||
 | 
					    """Updates the options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The redirection to the option form.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    form = OptionForm(request.form)
 | 
				
			||||||
 | 
					    if not form.validate():
 | 
				
			||||||
 | 
					        flash_form_errors(form)
 | 
				
			||||||
 | 
					        session["form"] = urlencode(list(request.form.items()))
 | 
				
			||||||
 | 
					        return redirect(inherit_next(url_for("accounting.option.edit")))
 | 
				
			||||||
 | 
					    form.populate_obj(options)
 | 
				
			||||||
 | 
					    if not options.is_modified:
 | 
				
			||||||
 | 
					        flash(s(lazy_gettext("The settings were not modified.")), "success")
 | 
				
			||||||
 | 
					        return redirect(inherit_next(url_for("accounting.option.detail")))
 | 
				
			||||||
 | 
					    options.commit()
 | 
				
			||||||
 | 
					    flash(s(lazy_gettext("The settings are saved successfully.")), "success")
 | 
				
			||||||
 | 
					    return redirect(inherit_next(url_for("accounting.option.detail")))
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/accounting/report/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/accounting/report/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The report management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import Flask, Blueprint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def init_app(app: Flask, bp: Blueprint) -> None:
 | 
				
			||||||
 | 
					    """Initialize the application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param app: The Flask application.
 | 
				
			||||||
 | 
					    :param bp: The blueprint of the accounting application.
 | 
				
			||||||
 | 
					    :return: None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    from .converters import PeriodConverter, IncomeExpensesAccountConverter
 | 
				
			||||||
 | 
					    app.url_map.converters["period"] = PeriodConverter
 | 
				
			||||||
 | 
					    app.url_map.converters["ieAccount"] = IncomeExpensesAccountConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from .views import bp as report_bp
 | 
				
			||||||
 | 
					    bp.register_blueprint(report_bp, url_prefix="/reports")
 | 
				
			||||||
							
								
								
									
										79
									
								
								src/accounting/report/converters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/accounting/report/converters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The path converters for the report management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from werkzeug.routing import BaseConverter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import Account
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from .period import Period, get_period
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PeriodConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The supplier converter to convert the period specification from and to
 | 
				
			||||||
 | 
					    the corresponding period in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> Period:
 | 
				
			||||||
 | 
					        """Converts a period specification to a period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The period specification.
 | 
				
			||||||
 | 
					        :return: The corresponding period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return get_period(value)
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: Period) -> str:
 | 
				
			||||||
 | 
					        """Converts a period to its specification.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The period.
 | 
				
			||||||
 | 
					        :return: Its specification.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return value.spec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IncomeExpensesAccountConverter(BaseConverter):
 | 
				
			||||||
 | 
					    """The supplier converter to convert the income and expenses log pseudo
 | 
				
			||||||
 | 
					    account code from and to the corresponding pseudo account in the routes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_python(self, value: str) -> CurrentAccount:
 | 
				
			||||||
 | 
					        """Converts an account code to an account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The account code.
 | 
				
			||||||
 | 
					        :return: The corresponding account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if value == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return CurrentAccount.current_assets_and_liabilities()
 | 
				
			||||||
 | 
					        if not re.match("^[12][12]", value):
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        account: Account | None = Account.find_by_code(value)
 | 
				
			||||||
 | 
					        if account is None:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					        return CurrentAccount(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_url(self, value: CurrentAccount) -> str:
 | 
				
			||||||
 | 
					        """Converts an account to account code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param value: The account.
 | 
				
			||||||
 | 
					        :return: Its code.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return value.code
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/accounting/report/period/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/accounting/report/period/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The period utility.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from .chooser import PeriodChooser
 | 
				
			||||||
 | 
					from .parser import get_period
 | 
				
			||||||
 | 
					from .period import Period
 | 
				
			||||||
							
								
								
									
										97
									
								
								src/accounting/report/period/chooser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/accounting/report/period/chooser.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The period chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This file is largely taken from the NanoParma ERP project, first written in
 | 
				
			||||||
 | 
					2021/9/16 by imacat (imacat@nanoparma.com).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import JournalEntry
 | 
				
			||||||
 | 
					from .period import Period
 | 
				
			||||||
 | 
					from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
 | 
				
			||||||
 | 
					    LastYear, Today, Yesterday, AllTime, TemplatePeriod, YearPeriod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PeriodChooser:
 | 
				
			||||||
 | 
					    """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, get_url: t.Callable[[Period], str]):
 | 
				
			||||||
 | 
					        """Constructs a period chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param get_url: The callback to return the URL of the current report in
 | 
				
			||||||
 | 
					            a period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__get_url: t.Callable[[Period], str] = get_url
 | 
				
			||||||
 | 
					        """The callback to return the URL of the current report in a period."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Shortcut periods
 | 
				
			||||||
 | 
					        self.this_month_url: str = get_url(ThisMonth())
 | 
				
			||||||
 | 
					        """The URL for this month."""
 | 
				
			||||||
 | 
					        self.last_month_url: str = get_url(LastMonth())
 | 
				
			||||||
 | 
					        """The URL for last month."""
 | 
				
			||||||
 | 
					        self.since_last_month_url: str = get_url(SinceLastMonth())
 | 
				
			||||||
 | 
					        """The URL since last mint."""
 | 
				
			||||||
 | 
					        self.this_year_url: str = get_url(ThisYear())
 | 
				
			||||||
 | 
					        """The URL for this year."""
 | 
				
			||||||
 | 
					        self.last_year_url: str = get_url(LastYear())
 | 
				
			||||||
 | 
					        """The URL for last year."""
 | 
				
			||||||
 | 
					        self.today_url: str = get_url(Today())
 | 
				
			||||||
 | 
					        """The URL for today."""
 | 
				
			||||||
 | 
					        self.yesterday_url: str = get_url(Yesterday())
 | 
				
			||||||
 | 
					        """The URL for yesterday."""
 | 
				
			||||||
 | 
					        self.all_url: str = get_url(AllTime())
 | 
				
			||||||
 | 
					        """The URL for all period."""
 | 
				
			||||||
 | 
					        self.url_template: str = get_url(TemplatePeriod())
 | 
				
			||||||
 | 
					        """The URL template."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        first: JournalEntry | None \
 | 
				
			||||||
 | 
					            = JournalEntry.query.order_by(JournalEntry.date).first()
 | 
				
			||||||
 | 
					        start: date | None = None if first is None else first.date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Attributes
 | 
				
			||||||
 | 
					        self.data_start: date | None = start
 | 
				
			||||||
 | 
					        """The start of the data."""
 | 
				
			||||||
 | 
					        self.has_data: bool = start is not None
 | 
				
			||||||
 | 
					        """Whether there is any data."""
 | 
				
			||||||
 | 
					        self.has_last_month: bool = False
 | 
				
			||||||
 | 
					        """Where there is data in last month."""
 | 
				
			||||||
 | 
					        self.has_last_year: bool = False
 | 
				
			||||||
 | 
					        """Whether there is data in last year."""
 | 
				
			||||||
 | 
					        self.has_yesterday: bool = False
 | 
				
			||||||
 | 
					        """Whether there is data in yesterday."""
 | 
				
			||||||
 | 
					        self.available_years: list[int] = []
 | 
				
			||||||
 | 
					        """The available years."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.has_data:
 | 
				
			||||||
 | 
					            today: date = date.today()
 | 
				
			||||||
 | 
					            self.has_last_month = start < date(today.year, today.month, 1)
 | 
				
			||||||
 | 
					            self.has_last_year = start.year < today.year
 | 
				
			||||||
 | 
					            self.has_yesterday = start < today
 | 
				
			||||||
 | 
					            if start.year < today.year - 1:
 | 
				
			||||||
 | 
					                self.available_years \
 | 
				
			||||||
 | 
					                    = reversed(range(start.year, today.year - 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def year_url(self, year: int) -> str:
 | 
				
			||||||
 | 
					        """Returns the period URL of a year.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param year: The year
 | 
				
			||||||
 | 
					        :return: The period URL of the year.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__get_url(YearPeriod(year))
 | 
				
			||||||
							
								
								
									
										179
									
								
								src/accounting/report/period/description.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/accounting/report/period/description.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The period description composer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_desc(start: date | None, end: date | None) -> str:
 | 
				
			||||||
 | 
					    """Returns the period description.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period description.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start is None and end is None:
 | 
				
			||||||
 | 
					        return gettext("for all time")
 | 
				
			||||||
 | 
					    if start is None:
 | 
				
			||||||
 | 
					        return __get_until_desc(end)
 | 
				
			||||||
 | 
					    if end is None:
 | 
				
			||||||
 | 
					        return __get_since_desc(start)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return __get_year_desc(start, end)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return __get_month_desc(start, end)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    return __get_day_desc(start, end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_since_desc(start: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the description without the end day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :return: The description without the end day.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_start_desc() -> str:
 | 
				
			||||||
 | 
					        """Returns the description of the start day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The description of the start day.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if start.month == 1 and start.day == 1:
 | 
				
			||||||
 | 
					            return str(start.year)
 | 
				
			||||||
 | 
					        if start.day == 1:
 | 
				
			||||||
 | 
					            return __format_month(start)
 | 
				
			||||||
 | 
					        return __format_day(start)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return gettext("since %(start)s", start=get_start_desc())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_until_desc(end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the description without the start day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The description without the start day.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_end_desc() -> str:
 | 
				
			||||||
 | 
					        """Returns the description of the end day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The description of the end day.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if end.month == 12 and end.day == 31:
 | 
				
			||||||
 | 
					            return str(end.year)
 | 
				
			||||||
 | 
					        if (end + timedelta(days=1)).day == 1:
 | 
				
			||||||
 | 
					            return __format_month(end)
 | 
				
			||||||
 | 
					        return __format_day(end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return gettext("until %(end)s", end=get_end_desc())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_year_desc(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the description as a year range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The description as a year range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is not a year range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start.month != 1 or start.day != 1 \
 | 
				
			||||||
 | 
					            or end.month != 12 or end.day != 31:
 | 
				
			||||||
 | 
					        raise ValueError
 | 
				
			||||||
 | 
					    start_text: str = str(start.year)
 | 
				
			||||||
 | 
					    if start.year == end.year:
 | 
				
			||||||
 | 
					        return __get_in_desc(start_text)
 | 
				
			||||||
 | 
					    return __get_from_to_desc(start_text, str(end.year))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_month_desc(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the description as a month range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The description as a month range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is not a month range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start.day != 1 or (end + timedelta(days=1)).day != 1:
 | 
				
			||||||
 | 
					        raise ValueError
 | 
				
			||||||
 | 
					    start_text: str = __format_month(start)
 | 
				
			||||||
 | 
					    if start.year == end.year and start.month == end.month:
 | 
				
			||||||
 | 
					        return __get_in_desc(start_text)
 | 
				
			||||||
 | 
					    if start.year == end.year:
 | 
				
			||||||
 | 
					        return __get_from_to_desc(start_text, str(end.month))
 | 
				
			||||||
 | 
					    return __get_from_to_desc(start_text, __format_month(end))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_day_desc(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the description as a day range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The description as a day range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is a month or year range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    start_text: str = __format_day(start)
 | 
				
			||||||
 | 
					    if start == end:
 | 
				
			||||||
 | 
					        return __get_in_desc(start_text)
 | 
				
			||||||
 | 
					    if start.year == end.year and start.month == end.month:
 | 
				
			||||||
 | 
					        return __get_from_to_desc(start_text, str(end.day))
 | 
				
			||||||
 | 
					    if start.year == end.year:
 | 
				
			||||||
 | 
					        end_month_day: str = f"{end.month}/{end.day}"
 | 
				
			||||||
 | 
					        return __get_from_to_desc(start_text, end_month_day)
 | 
				
			||||||
 | 
					    return __get_from_to_desc(start_text, __format_day(end))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __format_month(month: date) -> str:
 | 
				
			||||||
 | 
					    """Formats a month.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param month: The month.
 | 
				
			||||||
 | 
					    :return: The formatted month.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return f"{month.year}/{month.month}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __format_day(day: date) -> str:
 | 
				
			||||||
 | 
					    """Formats a day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param day: The day.
 | 
				
			||||||
 | 
					    :return: The formatted day.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return f"{day.year}/{day.month}/{day.day}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_in_desc(period: str) -> str:
 | 
				
			||||||
 | 
					    """Returns the description of a whole year, month, or day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param period: The time period.
 | 
				
			||||||
 | 
					    :return: The description of a whole year, month, or day.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return gettext("in %(period)s", period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_from_to_desc(start: str, end: str) -> str:
 | 
				
			||||||
 | 
					    """Returns the description of a separated start and end.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start.
 | 
				
			||||||
 | 
					    :param end: The end.
 | 
				
			||||||
 | 
					    :return: The description of the separated start and end.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return gettext("in %(start)s-%(end)s", start=start, end=end)
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/accounting/report/period/month_end.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/accounting/report/period/month_end.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The utility to return the end of a month.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import calendar
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def month_end(day: date) -> date:
 | 
				
			||||||
 | 
					    """Returns the end day of month for a date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param day: The date.
 | 
				
			||||||
 | 
					    :return: The end day of the month of that day.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    last_day: int = calendar.monthrange(day.year, day.month)[1]
 | 
				
			||||||
 | 
					    return date(day.year, day.month, last_day)
 | 
				
			||||||
							
								
								
									
										119
									
								
								src/accounting/report/period/parser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/accounting/report/period/parser.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The period specification parser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import calendar
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .period import Period
 | 
				
			||||||
 | 
					from .shortcuts import ThisMonth, LastMonth, SinceLastMonth, ThisYear, \
 | 
				
			||||||
 | 
					    LastYear, Today, Yesterday, AllTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DATE_SPEC_RE: str = r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?"
 | 
				
			||||||
 | 
					"""The regular expression of a date specification."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_period(spec: str | None = None) -> Period:
 | 
				
			||||||
 | 
					    """Returns a period instance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param spec: The period specification, or omit for the default.
 | 
				
			||||||
 | 
					    :return: The period instance.
 | 
				
			||||||
 | 
					    :raise ValueError: When the period specification is invalid.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if spec is None:
 | 
				
			||||||
 | 
					        return ThisMonth()
 | 
				
			||||||
 | 
					    named_periods: dict[str, t.Type[t.Callable[[], Period]]] = {
 | 
				
			||||||
 | 
					        "this-month": lambda: ThisMonth(),
 | 
				
			||||||
 | 
					        "last-month": lambda: LastMonth(),
 | 
				
			||||||
 | 
					        "since-last-month": lambda: SinceLastMonth(),
 | 
				
			||||||
 | 
					        "this-year": lambda: ThisYear(),
 | 
				
			||||||
 | 
					        "last-year": lambda: LastYear(),
 | 
				
			||||||
 | 
					        "today": lambda: Today(),
 | 
				
			||||||
 | 
					        "yesterday": lambda: Yesterday(),
 | 
				
			||||||
 | 
					        "all-time": lambda: AllTime(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if spec in named_periods:
 | 
				
			||||||
 | 
					        return named_periods[spec]()
 | 
				
			||||||
 | 
					    start, end = __parse_spec(spec)
 | 
				
			||||||
 | 
					    if start is not None and end is not None and start > end:
 | 
				
			||||||
 | 
					        raise ValueError
 | 
				
			||||||
 | 
					    return Period(start, end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __parse_spec(text: str) -> tuple[date | None, date | None]:
 | 
				
			||||||
 | 
					    """Parses the period specification.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param text: The period specification.
 | 
				
			||||||
 | 
					    :return: The start and end day of the period.  The start and end day
 | 
				
			||||||
 | 
					        may be None.
 | 
				
			||||||
 | 
					    :raise ValueError: When the date is invalid.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if text == "-":
 | 
				
			||||||
 | 
					        return None, None
 | 
				
			||||||
 | 
					    m = re.match(f"^{DATE_SPEC_RE}$", text)
 | 
				
			||||||
 | 
					    if m is not None:
 | 
				
			||||||
 | 
					        return __get_start(m[1], m[2], m[3]), \
 | 
				
			||||||
 | 
					               __get_end(m[1], m[2], m[3])
 | 
				
			||||||
 | 
					    m = re.match(f"^{DATE_SPEC_RE}-$", text)
 | 
				
			||||||
 | 
					    if m is not None:
 | 
				
			||||||
 | 
					        return __get_start(m[1], m[2], m[3]), None
 | 
				
			||||||
 | 
					    m = re.match(f"-{DATE_SPEC_RE}$", text)
 | 
				
			||||||
 | 
					    if m is not None:
 | 
				
			||||||
 | 
					        return None, __get_end(m[1], m[2], m[3])
 | 
				
			||||||
 | 
					    m = re.match(f"^{DATE_SPEC_RE}-{DATE_SPEC_RE}$", text)
 | 
				
			||||||
 | 
					    if m is not None:
 | 
				
			||||||
 | 
					        return __get_start(m[1], m[2], m[3]), \
 | 
				
			||||||
 | 
					               __get_end(m[4], m[5], m[6])
 | 
				
			||||||
 | 
					    raise ValueError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_start(year: str, month: str | None, day: str | None) -> date:
 | 
				
			||||||
 | 
					    """Returns the start of the period from the date representation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param year: The year.
 | 
				
			||||||
 | 
					    :param month: The month, if any.
 | 
				
			||||||
 | 
					    :param day: The day, if any.
 | 
				
			||||||
 | 
					    :return: The start of the period.
 | 
				
			||||||
 | 
					    :raise ValueError: When the date is invalid.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if day is not None:
 | 
				
			||||||
 | 
					        return date(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					    if month is not None:
 | 
				
			||||||
 | 
					        return date(int(year), int(month), 1)
 | 
				
			||||||
 | 
					    return date(int(year), 1, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_end(year: str, month: str | None, day: str | None) -> date:
 | 
				
			||||||
 | 
					    """Returns the end of the period from the date representation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param year: The year.
 | 
				
			||||||
 | 
					    :param month: The month, if any.
 | 
				
			||||||
 | 
					    :param day: The day, if any.
 | 
				
			||||||
 | 
					    :return: The end of the period.
 | 
				
			||||||
 | 
					    :raise ValueError: When the date is invalid.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if day is not None:
 | 
				
			||||||
 | 
					        return date(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					    if month is not None:
 | 
				
			||||||
 | 
					        year_n: int = int(year)
 | 
				
			||||||
 | 
					        month_n: int = int(month)
 | 
				
			||||||
 | 
					        day_n: int = calendar.monthrange(year_n, month_n)[1]
 | 
				
			||||||
 | 
					        return date(year_n, month_n, day_n)
 | 
				
			||||||
 | 
					    return date(int(year), 12, 31)
 | 
				
			||||||
							
								
								
									
										129
									
								
								src/accounting/report/period/period.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/accounting/report/period/period.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The date period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This file is largely taken from the NanoParma ERP project, first written in
 | 
				
			||||||
 | 
					2021/9/16 by imacat (imacat@nanoparma.com).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from datetime import date, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .description import get_desc
 | 
				
			||||||
 | 
					from .month_end import month_end
 | 
				
			||||||
 | 
					from .specification import get_spec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Period:
 | 
				
			||||||
 | 
					    """A date period."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, start: date | None, end: date | None):
 | 
				
			||||||
 | 
					        """Constructs a new date period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param start: The start date, or None from the very beginning.
 | 
				
			||||||
 | 
					        :param end: The end date, or None till no end.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.start: date | None = start
 | 
				
			||||||
 | 
					        """The start of the period."""
 | 
				
			||||||
 | 
					        self.end: date | None = end
 | 
				
			||||||
 | 
					        """The end of the period."""
 | 
				
			||||||
 | 
					        self.is_default: bool = False
 | 
				
			||||||
 | 
					        """Whether the is the default period."""
 | 
				
			||||||
 | 
					        self.is_this_month: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is this month."""
 | 
				
			||||||
 | 
					        self.is_last_month: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is last month."""
 | 
				
			||||||
 | 
					        self.is_since_last_month: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is since last month."""
 | 
				
			||||||
 | 
					        self.is_this_year: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is this year."""
 | 
				
			||||||
 | 
					        self.is_last_year: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is last year."""
 | 
				
			||||||
 | 
					        self.is_today: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is today."""
 | 
				
			||||||
 | 
					        self.is_yesterday: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is yesterday."""
 | 
				
			||||||
 | 
					        self.is_all: bool = start is None and end is None
 | 
				
			||||||
 | 
					        """Whether the period is all time."""
 | 
				
			||||||
 | 
					        self.spec: str = ""
 | 
				
			||||||
 | 
					        """The period specification."""
 | 
				
			||||||
 | 
					        self.desc: str = ""
 | 
				
			||||||
 | 
					        """The text description."""
 | 
				
			||||||
 | 
					        self.is_a_month: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is a whole month."""
 | 
				
			||||||
 | 
					        self.is_type_month: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is for the month chooser."""
 | 
				
			||||||
 | 
					        self.is_a_year: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is a whole year."""
 | 
				
			||||||
 | 
					        self.is_a_day: bool = False
 | 
				
			||||||
 | 
					        """Whether the period is a single day."""
 | 
				
			||||||
 | 
					        self._set_properties()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        """Sets the following properties.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        * self.spec
 | 
				
			||||||
 | 
					        * self.desc
 | 
				
			||||||
 | 
					        * self.is_a_month
 | 
				
			||||||
 | 
					        * self.is_type_month
 | 
				
			||||||
 | 
					        * self.is_a_year
 | 
				
			||||||
 | 
					        * self.is_a_day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Override this method to set the properties in the subclasses, to skip
 | 
				
			||||||
 | 
					        the calculation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.spec = get_spec(self.start, self.end)
 | 
				
			||||||
 | 
					        self.desc = get_desc(self.start, self.end)
 | 
				
			||||||
 | 
					        if self.start is None or self.end is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        self.is_a_month = self.start.day == 1 \
 | 
				
			||||||
 | 
					            and self.end == month_end(self.start)
 | 
				
			||||||
 | 
					        self.is_type_month = self.is_a_month
 | 
				
			||||||
 | 
					        self.is_a_year = self.start == date(self.start.year, 1, 1) \
 | 
				
			||||||
 | 
					            and self.end == date(self.start.year, 12, 31)
 | 
				
			||||||
 | 
					        self.is_a_day = self.start == self.end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_year(self, year: int) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether the period is the specific year period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param year: The year.
 | 
				
			||||||
 | 
					        :return: True if the period is the year period, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.is_a_year:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return self.start.year == year
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_type_arbitrary(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether this period is an arbitrary period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if this is an arbitrary period, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return not self.is_type_month and not self.is_a_year \
 | 
				
			||||||
 | 
					            and not self.is_a_day
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def before(self) -> t.Self | None:
 | 
				
			||||||
 | 
					        """Returns the period before this period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The period before this period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.start is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        return Period(None, self.start - timedelta(days=1))
 | 
				
			||||||
							
								
								
									
										168
									
								
								src/accounting/report/period/shortcuts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/accounting/report/period/shortcuts.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The named shortcut periods.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from .month_end import month_end
 | 
				
			||||||
 | 
					from .period import Period
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ThisMonth(Period):
 | 
				
			||||||
 | 
					    """The period of this month."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        today: date = date.today()
 | 
				
			||||||
 | 
					        this_month_start: date = date(today.year, today.month, 1)
 | 
				
			||||||
 | 
					        super().__init__(this_month_start, month_end(today))
 | 
				
			||||||
 | 
					        self.is_default = True
 | 
				
			||||||
 | 
					        self.is_this_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "this-month"
 | 
				
			||||||
 | 
					        self.desc = gettext("This Month")
 | 
				
			||||||
 | 
					        self.is_a_month = True
 | 
				
			||||||
 | 
					        self.is_type_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LastMonth(Period):
 | 
				
			||||||
 | 
					    """The period of this month."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        today: date = date.today()
 | 
				
			||||||
 | 
					        year: int = today.year
 | 
				
			||||||
 | 
					        month: int = today.month - 1
 | 
				
			||||||
 | 
					        if month < 1:
 | 
				
			||||||
 | 
					            year = year - 1
 | 
				
			||||||
 | 
					            month = 12
 | 
				
			||||||
 | 
					        start: date = date(year, month, 1)
 | 
				
			||||||
 | 
					        super().__init__(start, month_end(start))
 | 
				
			||||||
 | 
					        self.is_last_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "last-month"
 | 
				
			||||||
 | 
					        self.desc = gettext("Last Month")
 | 
				
			||||||
 | 
					        self.is_a_month = True
 | 
				
			||||||
 | 
					        self.is_type_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SinceLastMonth(Period):
 | 
				
			||||||
 | 
					    """The period of this month."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        today: date = date.today()
 | 
				
			||||||
 | 
					        year: int = today.year
 | 
				
			||||||
 | 
					        month: int = today.month - 1
 | 
				
			||||||
 | 
					        if month < 1:
 | 
				
			||||||
 | 
					            year = year - 1
 | 
				
			||||||
 | 
					            month = 12
 | 
				
			||||||
 | 
					        start: date = date(year, month, 1)
 | 
				
			||||||
 | 
					        super().__init__(start, None)
 | 
				
			||||||
 | 
					        self.is_since_last_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "since-last-month"
 | 
				
			||||||
 | 
					        self.desc = gettext("Since Last Month")
 | 
				
			||||||
 | 
					        self.is_type_month = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ThisYear(Period):
 | 
				
			||||||
 | 
					    """The period of this year."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        year: int = date.today().year
 | 
				
			||||||
 | 
					        start: date = date(year, 1, 1)
 | 
				
			||||||
 | 
					        end: date = date(year, 12, 31)
 | 
				
			||||||
 | 
					        super().__init__(start, end)
 | 
				
			||||||
 | 
					        self.is_this_year = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "this-year"
 | 
				
			||||||
 | 
					        self.desc = gettext("This Year")
 | 
				
			||||||
 | 
					        self.is_a_year = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LastYear(Period):
 | 
				
			||||||
 | 
					    """The period of last year."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        year: int = date.today().year
 | 
				
			||||||
 | 
					        start: date = date(year - 1, 1, 1)
 | 
				
			||||||
 | 
					        end: date = date(year - 1, 12, 31)
 | 
				
			||||||
 | 
					        super().__init__(start, end)
 | 
				
			||||||
 | 
					        self.is_last_year = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "last-year"
 | 
				
			||||||
 | 
					        self.desc = gettext("Last Year")
 | 
				
			||||||
 | 
					        self.is_a_year = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Today(Period):
 | 
				
			||||||
 | 
					    """The period of today."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        today: date = date.today()
 | 
				
			||||||
 | 
					        super().__init__(today, today)
 | 
				
			||||||
 | 
					        self.is_today = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "today"
 | 
				
			||||||
 | 
					        self.desc = gettext("Today")
 | 
				
			||||||
 | 
					        self.is_a_day = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Yesterday(Period):
 | 
				
			||||||
 | 
					    """The period of yesterday."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        yesterday: date = date.today() - timedelta(days=1)
 | 
				
			||||||
 | 
					        super().__init__(yesterday, yesterday)
 | 
				
			||||||
 | 
					        self.is_yesterday = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "yesterday"
 | 
				
			||||||
 | 
					        self.desc = gettext("Yesterday")
 | 
				
			||||||
 | 
					        self.is_a_day = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AllTime(Period):
 | 
				
			||||||
 | 
					    """The period of all time."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        super().__init__(None, None)
 | 
				
			||||||
 | 
					        self.is_all = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "all-time"
 | 
				
			||||||
 | 
					        self.desc = gettext("All")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TemplatePeriod(Period):
 | 
				
			||||||
 | 
					    """The period template."""
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        super().__init__(None, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_properties(self) -> None:
 | 
				
			||||||
 | 
					        self.spec = "PERIOD"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class YearPeriod(Period):
 | 
				
			||||||
 | 
					    """A year period."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, year: int):
 | 
				
			||||||
 | 
					        """Constructs a year period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param year: The year.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        start: date = date(year, 1, 1)
 | 
				
			||||||
 | 
					        end: date = date(year, 12, 31)
 | 
				
			||||||
 | 
					        super().__init__(start, end)
 | 
				
			||||||
							
								
								
									
										120
									
								
								src/accounting/report/period/specification.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/accounting/report/period/specification.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The period specification composer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_spec(start: date | None, end: date | None) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period specification.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start is None and end is None:
 | 
				
			||||||
 | 
					        return "-"
 | 
				
			||||||
 | 
					    if end is None:
 | 
				
			||||||
 | 
					        return __get_since_spec(start)
 | 
				
			||||||
 | 
					    if start is None:
 | 
				
			||||||
 | 
					        return __get_until_spec(end)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return __get_year_spec(start, end)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return __get_month_spec(start, end)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    return __get_day_spec(start, end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_since_spec(start: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification without the end day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :return: The period specification without the end day
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start.month == 1 and start.day == 1:
 | 
				
			||||||
 | 
					        return start.strftime("%Y-")
 | 
				
			||||||
 | 
					    if start.day == 1:
 | 
				
			||||||
 | 
					        return start.strftime("%Y-%m-")
 | 
				
			||||||
 | 
					    return start.strftime("%Y-%m-%d-")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_until_spec(end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification without the start day.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period specification without the start day
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if end.month == 12 and end.day == 31:
 | 
				
			||||||
 | 
					        return end.strftime("-%Y")
 | 
				
			||||||
 | 
					    if (end + timedelta(days=1)).day == 1:
 | 
				
			||||||
 | 
					        return end.strftime("-%Y-%m")
 | 
				
			||||||
 | 
					    return end.strftime("-%Y-%m-%d")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_year_spec(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification as a year range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period specification as a year range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is not a year range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start.month != 1 or start.day != 1 \
 | 
				
			||||||
 | 
					            or end.month != 12 or end.day != 31:
 | 
				
			||||||
 | 
					        raise ValueError
 | 
				
			||||||
 | 
					    start_spec: str = start.strftime("%Y")
 | 
				
			||||||
 | 
					    if start.year == end.year:
 | 
				
			||||||
 | 
					        return start_spec
 | 
				
			||||||
 | 
					    end_spec: str = end.strftime("%Y")
 | 
				
			||||||
 | 
					    return f"{start_spec}-{end_spec}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_month_spec(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification as a month range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period specification as a month range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is not a month range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start.day != 1 or (end + timedelta(days=1)).day != 1:
 | 
				
			||||||
 | 
					        raise ValueError
 | 
				
			||||||
 | 
					    start_spec: str = start.strftime("%Y-%m")
 | 
				
			||||||
 | 
					    if start.year == end.year and start.month == end.month:
 | 
				
			||||||
 | 
					        return start_spec
 | 
				
			||||||
 | 
					    end_spec: str = end.strftime("%Y-%m")
 | 
				
			||||||
 | 
					    return f"{start_spec}-{end_spec}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_day_spec(start: date, end: date) -> str:
 | 
				
			||||||
 | 
					    """Returns the period specification as a day range.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start of the period.
 | 
				
			||||||
 | 
					    :param end: The end of the period.
 | 
				
			||||||
 | 
					    :return: The period specification as a day range.
 | 
				
			||||||
 | 
					    :raise ValueError: The period is a month or year range.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    start_spec: str = start.strftime("%Y-%m-%d")
 | 
				
			||||||
 | 
					    if start == end:
 | 
				
			||||||
 | 
					        return start_spec
 | 
				
			||||||
 | 
					    end_spec: str = end.strftime("%Y-%m-%d")
 | 
				
			||||||
 | 
					    return f"{start_spec}-{end_spec}"
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/accounting/report/reports/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/accounting/report/reports/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The reports.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from .balance_sheet import BalanceSheet
 | 
				
			||||||
 | 
					from .income_expenses import IncomeExpenses
 | 
				
			||||||
 | 
					from .income_statement import IncomeStatement
 | 
				
			||||||
 | 
					from .journal import Journal
 | 
				
			||||||
 | 
					from .ledger import Ledger
 | 
				
			||||||
 | 
					from .search import Search
 | 
				
			||||||
 | 
					from .trial_balance import TrialBalance
 | 
				
			||||||
							
								
								
									
										474
									
								
								src/accounting/report/reports/balance_sheet.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								src/accounting/report/reports/balance_sheet.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,474 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import render_template, Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import ledger_url, balance_sheet_url, \
 | 
				
			||||||
 | 
					    income_statement_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportAccount:
 | 
				
			||||||
 | 
					    """An account in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, account: Account, amount: Decimal, url: str):
 | 
				
			||||||
 | 
					        """Constructs an account in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        :param url: The URL to the ledger of the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.amount: Decimal = amount
 | 
				
			||||||
 | 
					        """The amount of the account."""
 | 
				
			||||||
 | 
					        self.url: str = url
 | 
				
			||||||
 | 
					        """The URL to the ledger of the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Subsection:
 | 
				
			||||||
 | 
					    """A subsection."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: BaseAccount):
 | 
				
			||||||
 | 
					        """Constructs a subsection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: BaseAccount = title
 | 
				
			||||||
 | 
					        """The title account."""
 | 
				
			||||||
 | 
					        self.accounts: list[ReportAccount] = []
 | 
				
			||||||
 | 
					        """The accounts in the subsection."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total of the subsection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total of the subsection.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount for x in self.accounts])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Section:
 | 
				
			||||||
 | 
					    """A section."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: BaseAccount):
 | 
				
			||||||
 | 
					        """Constructs a section.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: BaseAccount = title
 | 
				
			||||||
 | 
					        """The title account."""
 | 
				
			||||||
 | 
					        self.subsections: list[Subsection] = []
 | 
				
			||||||
 | 
					        """The subsections in the section."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total of the section.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total of the section.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.total for x in self.subsections])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountCollector:
 | 
				
			||||||
 | 
					    """The balance sheet account collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, period: Period):
 | 
				
			||||||
 | 
					        """Constructs the balance sheet account collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.accounts: list[ReportAccount] = self.__query_balances()
 | 
				
			||||||
 | 
					        """The balance sheet accounts."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_balances(self) -> list[ReportAccount]:
 | 
				
			||||||
 | 
					        """Queries and returns the balances.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The balances.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        sub_conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [Account.base_code.startswith(x) for x in {"1", "2", "3"}]
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
 | 
					               sa.or_(*sub_conditions)]
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
 | 
					        select_balance: sa.Select \
 | 
				
			||||||
 | 
					            = sa.select(Account.id, Account.base_code, Account.no,
 | 
				
			||||||
 | 
					                        balance_func)\
 | 
				
			||||||
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
 | 
					            .filter(*conditions)\
 | 
				
			||||||
 | 
					            .group_by(Account.id, Account.base_code, Account.no)\
 | 
				
			||||||
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
 | 
					        account_balances: list[sa.Row] \
 | 
				
			||||||
 | 
					            = db.session.execute(select_balance).all()
 | 
				
			||||||
 | 
					        self.__all_accounts: list[Account] = Account.query\
 | 
				
			||||||
 | 
					            .filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
 | 
				
			||||||
 | 
					                           Account.base_code == "3351",
 | 
				
			||||||
 | 
					                           Account.base_code == "3353")).all()
 | 
				
			||||||
 | 
					        account_by_id: dict[int, Account] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in self.__all_accounts}
 | 
				
			||||||
 | 
					        self.accounts: list[ReportAccount] \
 | 
				
			||||||
 | 
					            = [ReportAccount(account=account_by_id[x.id],
 | 
				
			||||||
 | 
					                             amount=x.balance,
 | 
				
			||||||
 | 
					                             url=ledger_url(self.__currency,
 | 
				
			||||||
 | 
					                                            account_by_id[x.id],
 | 
				
			||||||
 | 
					                                            self.__period))
 | 
				
			||||||
 | 
					               for x in account_balances]
 | 
				
			||||||
 | 
					        self.__add_accumulated()
 | 
				
			||||||
 | 
					        self.__add_current_period()
 | 
				
			||||||
 | 
					        self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no))
 | 
				
			||||||
 | 
					        for balance in self.accounts:
 | 
				
			||||||
 | 
					            if not balance.account.base_code.startswith("1"):
 | 
				
			||||||
 | 
					                balance.amount = -balance.amount
 | 
				
			||||||
 | 
					        return self.accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __add_accumulated(self) -> None:
 | 
				
			||||||
 | 
					        """Adds the accumulated profit or loss to the balances.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__add_owner_s_equity(Account.ACCUMULATED_CHANGE_CODE,
 | 
				
			||||||
 | 
					                                  self.__query_accumulated(),
 | 
				
			||||||
 | 
					                                  self.__period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_accumulated(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Queries and returns the accumulated profit or loss.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The accumulated profit or loss.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.__period.start is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
 | 
					               JournalEntry.date < self.__period.start]
 | 
				
			||||||
 | 
					        return self.__query_balance(conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __add_current_period(self) -> None:
 | 
				
			||||||
 | 
					        """Adds the accumulated profit or loss to the balances.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__add_owner_s_equity(Account.NET_CHANGE_CODE,
 | 
				
			||||||
 | 
					                                  self.__query_current_period(),
 | 
				
			||||||
 | 
					                                  self.__period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_current_period(self) -> Decimal | None:
 | 
				
			||||||
 | 
					        """Queries and returns the net income or loss for current period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The net income or loss for current period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        return self.__query_balance(conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __query_balance(conditions: list[sa.BinaryExpression])\
 | 
				
			||||||
 | 
					            -> Decimal:
 | 
				
			||||||
 | 
					        """Queries the balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param conditions: The SQL conditions for the balance.
 | 
				
			||||||
 | 
					        :return: The balance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions.extend([sa.not_(Account.base_code.startswith(x))
 | 
				
			||||||
 | 
					                           for x in {"1", "2", "3"}])
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
 | 
					        select_balance: sa.Select = sa.select(balance_func)\
 | 
				
			||||||
 | 
					            .join(JournalEntry).join(Account).filter(*conditions)
 | 
				
			||||||
 | 
					        return db.session.scalar(select_balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __add_owner_s_equity(self, code: str, amount: Decimal | None,
 | 
				
			||||||
 | 
					                             period: Period) -> None:
 | 
				
			||||||
 | 
					        """Adds an owner's equity balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param code: The code of the account to add.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if amount is None:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        url: str = income_statement_url(self.__currency, period)
 | 
				
			||||||
 | 
					        # There is an existing balance.
 | 
				
			||||||
 | 
					        account_balance_by_code: dict[str, ReportAccount] \
 | 
				
			||||||
 | 
					            = {x.account.code: x for x in self.accounts}
 | 
				
			||||||
 | 
					        if code in account_balance_by_code:
 | 
				
			||||||
 | 
					            balance: ReportAccount = account_balance_by_code[code]
 | 
				
			||||||
 | 
					            balance.amount = balance.amount + amount
 | 
				
			||||||
 | 
					            balance.url = url
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        # Add a new balance
 | 
				
			||||||
 | 
					        account_by_code: dict[str, Account] \
 | 
				
			||||||
 | 
					            = {x.code: x for x in self.__all_accounts}
 | 
				
			||||||
 | 
					        self.accounts.append(ReportAccount(account=account_by_code[code],
 | 
				
			||||||
 | 
					                                           amount=amount,
 | 
				
			||||||
 | 
					                                           url=url))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVHalfRow:
 | 
				
			||||||
 | 
					    """A half row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: str | None, amount: Decimal | None):
 | 
				
			||||||
 | 
					        """The constructs a half row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: str | None = title
 | 
				
			||||||
 | 
					        """The title."""
 | 
				
			||||||
 | 
					        self.amount: Decimal | None = amount
 | 
				
			||||||
 | 
					        """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV."""
 | 
				
			||||||
 | 
					        self.asset_title: str | None = None
 | 
				
			||||||
 | 
					        """The title of the asset."""
 | 
				
			||||||
 | 
					        self.asset_amount: Decimal | None = None
 | 
				
			||||||
 | 
					        """The amount of the asset."""
 | 
				
			||||||
 | 
					        self.liability_title: str | None = None
 | 
				
			||||||
 | 
					        """The title of the liability."""
 | 
				
			||||||
 | 
					        self.liability_amount: Decimal | None = None
 | 
				
			||||||
 | 
					        """The amount of the liability."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.asset_title, self.asset_amount,
 | 
				
			||||||
 | 
					                self.liability_title, self.liability_amount]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
 | 
					                 period: Period,
 | 
				
			||||||
 | 
					                 has_data: bool,
 | 
				
			||||||
 | 
					                 assets: Section,
 | 
				
			||||||
 | 
					                 liabilities: Section,
 | 
				
			||||||
 | 
					                 owner_s_equity: Section):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param has_data: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        :param assets: The assets.
 | 
				
			||||||
 | 
					        :param liabilities: The liabilities.
 | 
				
			||||||
 | 
					        :param owner_s_equity: The owner's equity.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool = has_data
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.assets: Section = assets
 | 
				
			||||||
 | 
					        """The assets."""
 | 
				
			||||||
 | 
					        self.liabilities: Section = liabilities
 | 
				
			||||||
 | 
					        """The liabilities."""
 | 
				
			||||||
 | 
					        self.owner_s_equity: Section = owner_s_equity
 | 
				
			||||||
 | 
					        """The owner's equity."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: balance_sheet_url(currency, x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__has_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.BALANCE_SHEET,
 | 
				
			||||||
 | 
					                             currency=self.currency,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currency_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._get_currency_options(
 | 
				
			||||||
 | 
					            lambda x: balance_sheet_url(x, self.period), self.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BalanceSheet(BaseReport):
 | 
				
			||||||
 | 
					    """The balance sheet."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, period: Period):
 | 
				
			||||||
 | 
					        """Constructs a balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.__assets: Section
 | 
				
			||||||
 | 
					        """The assets."""
 | 
				
			||||||
 | 
					        self.__liabilities: Section
 | 
				
			||||||
 | 
					        """The liabilities."""
 | 
				
			||||||
 | 
					        self.__owner_s_equity: Section
 | 
				
			||||||
 | 
					        """The owner's equity."""
 | 
				
			||||||
 | 
					        self.__set_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __set_data(self) -> None:
 | 
				
			||||||
 | 
					        """Queries and sets assets, the liabilities, and the owner's equity
 | 
				
			||||||
 | 
					        sections in the balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        balances: list[ReportAccount] = AccountCollector(
 | 
				
			||||||
 | 
					            self.__currency, self.__period).accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        titles: list[BaseAccount] = BaseAccount.query\
 | 
				
			||||||
 | 
					            .filter(BaseAccount.code.in_({"1", "2", "3"})).all()
 | 
				
			||||||
 | 
					        subtitles: list[BaseAccount] = BaseAccount.query\
 | 
				
			||||||
 | 
					            .filter(BaseAccount.code.in_({x.account.base_code[:2]
 | 
				
			||||||
 | 
					                                          for x in balances})).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sections: dict[str, Section] = {x.code: Section(x) for x in titles}
 | 
				
			||||||
 | 
					        subsections: dict[str, Subsection] = {x.code: Subsection(x)
 | 
				
			||||||
 | 
					                                              for x in subtitles}
 | 
				
			||||||
 | 
					        for subsection in subsections.values():
 | 
				
			||||||
 | 
					            sections[subsection.title.code[0]].subsections.append(subsection)
 | 
				
			||||||
 | 
					        for balance in balances:
 | 
				
			||||||
 | 
					            subsections[balance.account.base_code[:2]].accounts.append(balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.__has_data = len(balances) > 0
 | 
				
			||||||
 | 
					        self.__assets = sections["1"]
 | 
				
			||||||
 | 
					        self.__liabilities = sections["2"]
 | 
				
			||||||
 | 
					        self.__owner_s_equity = sections["3"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "balance-sheet-{currency}-{period}.csv"\
 | 
				
			||||||
 | 
					            .format(currency=self.__currency.code,
 | 
				
			||||||
 | 
					                    period=period_spec(self.__period))
 | 
				
			||||||
 | 
					        return csv_download(filename, self.__get_csv_rows())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_csv_rows(self) -> list[CSVRow]:
 | 
				
			||||||
 | 
					        """Composes and returns the CSV rows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The CSV rows.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        asset_rows: list[CSVHalfRow] = self.__section_csv_rows(self.__assets)
 | 
				
			||||||
 | 
					        liability_rows: list[CSVHalfRow] = []
 | 
				
			||||||
 | 
					        liability_rows.extend(self.__section_csv_rows(self.__liabilities))
 | 
				
			||||||
 | 
					        liability_rows.append(CSVHalfRow(gettext("Total"),
 | 
				
			||||||
 | 
					                                         self.__liabilities.total))
 | 
				
			||||||
 | 
					        liability_rows.append(CSVHalfRow(None, None))
 | 
				
			||||||
 | 
					        liability_rows.extend(self.__section_csv_rows(self.__owner_s_equity))
 | 
				
			||||||
 | 
					        liability_rows.append(CSVHalfRow(gettext("Total"),
 | 
				
			||||||
 | 
					                                         self.__owner_s_equity.total))
 | 
				
			||||||
 | 
					        rows: list[CSVRow] = [CSVRow() for _ in
 | 
				
			||||||
 | 
					                              range(max(len(asset_rows), len(liability_rows)))]
 | 
				
			||||||
 | 
					        for i in range(len(rows)):
 | 
				
			||||||
 | 
					            if i < len(asset_rows):
 | 
				
			||||||
 | 
					                rows[i].asset_title = asset_rows[i].title
 | 
				
			||||||
 | 
					                rows[i].asset_amount = asset_rows[i].amount
 | 
				
			||||||
 | 
					            if i < len(liability_rows) and liability_rows[i].title is not None:
 | 
				
			||||||
 | 
					                rows[i].liability_title = liability_rows[i].title
 | 
				
			||||||
 | 
					                rows[i].liability_amount = liability_rows[i].amount
 | 
				
			||||||
 | 
					        total: CSVRow = CSVRow()
 | 
				
			||||||
 | 
					        total.asset_title = gettext("Total")
 | 
				
			||||||
 | 
					        total.asset_amount = self.__assets.total
 | 
				
			||||||
 | 
					        total.liability_title = gettext("Total")
 | 
				
			||||||
 | 
					        total.liability_amount \
 | 
				
			||||||
 | 
					            = self.__liabilities.total + self.__owner_s_equity.total
 | 
				
			||||||
 | 
					        rows.append(total)
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __section_csv_rows(section: Section) -> list[CSVHalfRow]:
 | 
				
			||||||
 | 
					        """Gathers the CSV rows for a section.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param section: The section.
 | 
				
			||||||
 | 
					        :return: The CSV rows for the section.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        rows: list[CSVHalfRow] \
 | 
				
			||||||
 | 
					            = [CSVHalfRow(section.title.title.title(), None)]
 | 
				
			||||||
 | 
					        for subsection in section.subsections:
 | 
				
			||||||
 | 
					            rows.append(CSVHalfRow(f" {subsection.title.title.title()}", None))
 | 
				
			||||||
 | 
					            for account in subsection.accounts:
 | 
				
			||||||
 | 
					                rows.append(CSVHalfRow(f"  {str(account.account).title()}",
 | 
				
			||||||
 | 
					                                       account.amount))
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(currency=self.__currency,
 | 
				
			||||||
 | 
					                                        period=self.__period,
 | 
				
			||||||
 | 
					                                        has_data=self.__has_data,
 | 
				
			||||||
 | 
					                                        assets=self.__assets,
 | 
				
			||||||
 | 
					                                        liabilities=self.__liabilities,
 | 
				
			||||||
 | 
					                                        owner_s_equity=self.__owner_s_equity)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/balance-sheet.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										460
									
								
								src/accounting/report/reports/income_expenses.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								src/accounting/report/reports/income_expenses.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,460 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import url_for, render_template, Response
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import income_expenses_url
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportLineItem:
 | 
				
			||||||
 | 
					    """A line item in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, line_item: JournalEntryLineItem | None = None):
 | 
				
			||||||
 | 
					        """Constructs the line item in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param line_item: The journal entry line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.is_brought_forward: bool = False
 | 
				
			||||||
 | 
					        """Whether this is the brought-forward line item."""
 | 
				
			||||||
 | 
					        self.is_total: bool = False
 | 
				
			||||||
 | 
					        """Whether this is the total line item."""
 | 
				
			||||||
 | 
					        self.date: date | None = None
 | 
				
			||||||
 | 
					        """The date."""
 | 
				
			||||||
 | 
					        self.account: Account | None = None
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.description: str | None = None
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.income: Decimal | None = None
 | 
				
			||||||
 | 
					        """The income amount."""
 | 
				
			||||||
 | 
					        self.expense: Decimal | None = None
 | 
				
			||||||
 | 
					        """The expense amount."""
 | 
				
			||||||
 | 
					        self.balance: Decimal | None = None
 | 
				
			||||||
 | 
					        """The balance."""
 | 
				
			||||||
 | 
					        self.note: str | None = None
 | 
				
			||||||
 | 
					        """The note."""
 | 
				
			||||||
 | 
					        self.url: str | None = None
 | 
				
			||||||
 | 
					        """The URL to the journal entry line item."""
 | 
				
			||||||
 | 
					        if line_item is not None:
 | 
				
			||||||
 | 
					            self.date = line_item.journal_entry.date
 | 
				
			||||||
 | 
					            self.account = line_item.account
 | 
				
			||||||
 | 
					            self.description = line_item.description
 | 
				
			||||||
 | 
					            self.income = None if line_item.is_debit else line_item.amount
 | 
				
			||||||
 | 
					            self.expense = line_item.amount if line_item.is_debit else None
 | 
				
			||||||
 | 
					            self.note = line_item.journal_entry.note
 | 
				
			||||||
 | 
					            self.url = url_for("accounting.journal-entry.detail",
 | 
				
			||||||
 | 
					                               journal_entry=line_item.journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LineItemCollector:
 | 
				
			||||||
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, account: CurrentAccount,
 | 
				
			||||||
 | 
					                 period: Period):
 | 
				
			||||||
 | 
					        """Constructs the line item collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__account: CurrentAccount = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period"""
 | 
				
			||||||
 | 
					        self.brought_forward: ReportLineItem | None
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.line_items: list[ReportLineItem]
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.total: ReportLineItem | None
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					        self.brought_forward = self.__get_brought_forward()
 | 
				
			||||||
 | 
					        self.line_items = self.__query_line_items()
 | 
				
			||||||
 | 
					        self.total = self.__get_total()
 | 
				
			||||||
 | 
					        self.__populate_balance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_brought_forward(self) -> ReportLineItem | None:
 | 
				
			||||||
 | 
					        """Queries, composes and returns the brought-forward line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The brought-forward line item, or None if the period starts
 | 
				
			||||||
 | 
					            from the beginning.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.__period.start is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
 | 
					        select: sa.Select = sa.Select(balance_func)\
 | 
				
			||||||
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
 | 
					                       == self.__currency.code),
 | 
				
			||||||
 | 
					                    self.__account_condition,
 | 
				
			||||||
 | 
					                    JournalEntry.date < self.__period.start)
 | 
				
			||||||
 | 
					        balance: int | None = db.session.scalar(select)
 | 
				
			||||||
 | 
					        if balance is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        line_item: ReportLineItem = ReportLineItem()
 | 
				
			||||||
 | 
					        line_item.is_brought_forward = True
 | 
				
			||||||
 | 
					        line_item.date = self.__period.start
 | 
				
			||||||
 | 
					        line_item.account = Account.accumulated_change()
 | 
				
			||||||
 | 
					        line_item.description = gettext("Brought forward")
 | 
				
			||||||
 | 
					        if balance > 0:
 | 
				
			||||||
 | 
					            line_item.income = balance
 | 
				
			||||||
 | 
					        elif balance < 0:
 | 
				
			||||||
 | 
					            line_item.expense = -balance
 | 
				
			||||||
 | 
					        line_item.balance = balance
 | 
				
			||||||
 | 
					        return line_item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_line_items(self) -> list[ReportLineItem]:
 | 
				
			||||||
 | 
					        """Queries and returns the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
 | 
					               self.__account_condition]
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        journal_entry_with_account: sa.Select = sa.Select(JournalEntry.id).\
 | 
				
			||||||
 | 
					            join(JournalEntryLineItem).join(Account).filter(*conditions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [ReportLineItem(x)
 | 
				
			||||||
 | 
					                for x in JournalEntryLineItem.query
 | 
				
			||||||
 | 
					                .join(JournalEntry).join(Account)
 | 
				
			||||||
 | 
					                .filter(JournalEntryLineItem.journal_entry_id
 | 
				
			||||||
 | 
					                        .in_(journal_entry_with_account),
 | 
				
			||||||
 | 
					                        JournalEntryLineItem.currency_code
 | 
				
			||||||
 | 
					                        == self.__currency.code,
 | 
				
			||||||
 | 
					                        sa.not_(self.__account_condition))
 | 
				
			||||||
 | 
					                .order_by(JournalEntry.date,
 | 
				
			||||||
 | 
					                          JournalEntry.no,
 | 
				
			||||||
 | 
					                          JournalEntryLineItem.is_debit,
 | 
				
			||||||
 | 
					                          JournalEntryLineItem.no)
 | 
				
			||||||
 | 
					                .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
 | 
					                         selectinload(JournalEntryLineItem.journal_entry))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __account_condition(self) -> sa.BinaryExpression:
 | 
				
			||||||
 | 
					        if self.__account.code == CurrentAccount.CURRENT_AL_CODE:
 | 
				
			||||||
 | 
					            return CurrentAccount.sql_condition()
 | 
				
			||||||
 | 
					        return Account.id == self.__account.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_total(self) -> ReportLineItem | None:
 | 
				
			||||||
 | 
					        """Composes the total line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total line item, or None if there is no data.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.brought_forward is None and len(self.line_items) == 0:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        line_item: ReportLineItem = ReportLineItem()
 | 
				
			||||||
 | 
					        line_item.is_total = True
 | 
				
			||||||
 | 
					        line_item.description = gettext("Total")
 | 
				
			||||||
 | 
					        line_item.income = sum([x.income for x in self.line_items
 | 
				
			||||||
 | 
					                                if x.income is not None])
 | 
				
			||||||
 | 
					        line_item.expense = sum([x.expense for x in self.line_items
 | 
				
			||||||
 | 
					                                 if x.expense is not None])
 | 
				
			||||||
 | 
					        line_item.balance = line_item.income - line_item.expense
 | 
				
			||||||
 | 
					        if self.brought_forward is not None:
 | 
				
			||||||
 | 
					            line_item.balance \
 | 
				
			||||||
 | 
					                = self.brought_forward.balance + line_item.balance
 | 
				
			||||||
 | 
					        return line_item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __populate_balance(self) -> None:
 | 
				
			||||||
 | 
					        """Populates the balance of the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        balance: Decimal = 0 if self.brought_forward is None \
 | 
				
			||||||
 | 
					            else self.brought_forward.balance
 | 
				
			||||||
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
 | 
					            if line_item.income is not None:
 | 
				
			||||||
 | 
					                balance = balance + line_item.income
 | 
				
			||||||
 | 
					            if line_item.expense is not None:
 | 
				
			||||||
 | 
					                balance = balance - line_item.expense
 | 
				
			||||||
 | 
					            line_item.balance = balance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, journal_entry_date: date | str | None,
 | 
				
			||||||
 | 
					                 account: str | None,
 | 
				
			||||||
 | 
					                 description: str | None,
 | 
				
			||||||
 | 
					                 income: str | Decimal | None,
 | 
				
			||||||
 | 
					                 expense: str | Decimal | None,
 | 
				
			||||||
 | 
					                 balance: str | Decimal | None,
 | 
				
			||||||
 | 
					                 note: str | None):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param description: The description.
 | 
				
			||||||
 | 
					        :param income: The income.
 | 
				
			||||||
 | 
					        :param expense: The expense.
 | 
				
			||||||
 | 
					        :param balance: The balance.
 | 
				
			||||||
 | 
					        :param note: The note.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.date: date | str | None = journal_entry_date
 | 
				
			||||||
 | 
					        """The date."""
 | 
				
			||||||
 | 
					        self.account: str | None = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.description: str | None = description
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.income: str | Decimal | None = income
 | 
				
			||||||
 | 
					        """The income."""
 | 
				
			||||||
 | 
					        self.expense: str | Decimal | None = expense
 | 
				
			||||||
 | 
					        """The expense."""
 | 
				
			||||||
 | 
					        self.balance: str | Decimal | None = balance
 | 
				
			||||||
 | 
					        """The balance."""
 | 
				
			||||||
 | 
					        self.note: str | None = note
 | 
				
			||||||
 | 
					        """The note."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.date, self.account, self.description,
 | 
				
			||||||
 | 
					                self.income, self.expense, self.balance, self.note]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
 | 
					                 account: CurrentAccount,
 | 
				
			||||||
 | 
					                 period: Period,
 | 
				
			||||||
 | 
					                 has_data: bool,
 | 
				
			||||||
 | 
					                 pagination: Pagination[ReportLineItem],
 | 
				
			||||||
 | 
					                 brought_forward: ReportLineItem | None,
 | 
				
			||||||
 | 
					                 line_items: list[ReportLineItem],
 | 
				
			||||||
 | 
					                 total: ReportLineItem | None):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param has_data: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        :param brought_forward: The brought-forward line item.
 | 
				
			||||||
 | 
					        :param line_items: The line items.
 | 
				
			||||||
 | 
					        :param total: The total line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.account: CurrentAccount = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool = has_data
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.pagination: Pagination[ReportLineItem] = pagination
 | 
				
			||||||
 | 
					        """The pagination."""
 | 
				
			||||||
 | 
					        self.brought_forward: ReportLineItem | None = brought_forward
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.line_items: list[ReportLineItem] = line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.total: ReportLineItem | None = total
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: income_expenses_url(currency, account, x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__has_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.account.account is None:
 | 
				
			||||||
 | 
					            return ReportChooser(ReportType.INCOME_EXPENSES,
 | 
				
			||||||
 | 
					                                 currency=self.currency,
 | 
				
			||||||
 | 
					                                 account=Account.cash(),
 | 
				
			||||||
 | 
					                                 period=self.period)
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.INCOME_EXPENSES,
 | 
				
			||||||
 | 
					                             currency=self.currency,
 | 
				
			||||||
 | 
					                             account=self.account.account,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currency_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._get_currency_options(
 | 
				
			||||||
 | 
					            lambda x: income_expenses_url(x, self.account, self.period),
 | 
				
			||||||
 | 
					            self.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the account options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        current_al: CurrentAccount \
 | 
				
			||||||
 | 
					            = CurrentAccount.current_assets_and_liabilities()
 | 
				
			||||||
 | 
					        options: list[OptionLink] \
 | 
				
			||||||
 | 
					            = [OptionLink(str(current_al),
 | 
				
			||||||
 | 
					                          income_expenses_url(self.currency, current_al,
 | 
				
			||||||
 | 
					                                              self.period),
 | 
				
			||||||
 | 
					                          self.account.id == 0)]
 | 
				
			||||||
 | 
					        in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
 | 
				
			||||||
 | 
					            .join(Account)\
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
 | 
					                       == self.currency.code),
 | 
				
			||||||
 | 
					                    CurrentAccount.sql_condition())\
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.account_id)
 | 
				
			||||||
 | 
					        options.extend([OptionLink(str(x),
 | 
				
			||||||
 | 
					                                   income_expenses_url(
 | 
				
			||||||
 | 
					                                       self.currency,
 | 
				
			||||||
 | 
					                                       CurrentAccount(x),
 | 
				
			||||||
 | 
					                                       self.period),
 | 
				
			||||||
 | 
					                                   x.id == self.account.id)
 | 
				
			||||||
 | 
					                        for x in Account.query.filter(Account.id.in_(in_use))
 | 
				
			||||||
 | 
					                       .order_by(Account.base_code, Account.no).all()])
 | 
				
			||||||
 | 
					        return options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IncomeExpenses(BaseReport):
 | 
				
			||||||
 | 
					    """The income and expenses log."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, account: CurrentAccount,
 | 
				
			||||||
 | 
					                 period: Period):
 | 
				
			||||||
 | 
					        """Constructs an income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__account: CurrentAccount = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        collector: LineItemCollector = LineItemCollector(
 | 
				
			||||||
 | 
					            self.__currency, self.__account, self.__period)
 | 
				
			||||||
 | 
					        self.__brought_forward: ReportLineItem | None \
 | 
				
			||||||
 | 
					            = collector.brought_forward
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.__line_items: list[ReportLineItem] = collector.line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.__total: ReportLineItem | None = collector.total
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "income-expenses-{currency}-{account}-{period}.csv"\
 | 
				
			||||||
 | 
					            .format(currency=self.__currency.code, account=self.__account.code,
 | 
				
			||||||
 | 
					                    period=period_spec(self.__period))
 | 
				
			||||||
 | 
					        return csv_download(filename, self.__get_csv_rows())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_csv_rows(self) -> list[CSVRow]:
 | 
				
			||||||
 | 
					        """Composes and returns the CSV rows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The CSV rows.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Account"),
 | 
				
			||||||
 | 
					                                     gettext("Description"), gettext("Income"),
 | 
				
			||||||
 | 
					                                     gettext("Expense"), gettext("Balance"),
 | 
				
			||||||
 | 
					                                     gettext("Note"))]
 | 
				
			||||||
 | 
					        if self.__brought_forward is not None:
 | 
				
			||||||
 | 
					            rows.append(CSVRow(self.__brought_forward.date,
 | 
				
			||||||
 | 
					                               str(self.__brought_forward.account).title(),
 | 
				
			||||||
 | 
					                               self.__brought_forward.description,
 | 
				
			||||||
 | 
					                               self.__brought_forward.income,
 | 
				
			||||||
 | 
					                               self.__brought_forward.expense,
 | 
				
			||||||
 | 
					                               self.__brought_forward.balance,
 | 
				
			||||||
 | 
					                               None))
 | 
				
			||||||
 | 
					        rows.extend([CSVRow(x.date, str(x.account).title(), x.description,
 | 
				
			||||||
 | 
					                            x.income, x.expense, x.balance, x.note)
 | 
				
			||||||
 | 
					                     for x in self.__line_items])
 | 
				
			||||||
 | 
					        if self.__total is not None:
 | 
				
			||||||
 | 
					            rows.append(CSVRow(gettext("Total"), None, None,
 | 
				
			||||||
 | 
					                               self.__total.income, self.__total.expense,
 | 
				
			||||||
 | 
					                               self.__total.balance, None))
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_line_items: list[ReportLineItem] = []
 | 
				
			||||||
 | 
					        if self.__brought_forward is not None:
 | 
				
			||||||
 | 
					            all_line_items.append(self.__brought_forward)
 | 
				
			||||||
 | 
					        all_line_items.extend(self.__line_items)
 | 
				
			||||||
 | 
					        if self.__total is not None:
 | 
				
			||||||
 | 
					            all_line_items.append(self.__total)
 | 
				
			||||||
 | 
					        pagination: Pagination[ReportLineItem] \
 | 
				
			||||||
 | 
					            = Pagination[ReportLineItem](all_line_items, is_reversed=True)
 | 
				
			||||||
 | 
					        page_line_items: list[ReportLineItem] = pagination.list
 | 
				
			||||||
 | 
					        has_data: bool = len(page_line_items) > 0
 | 
				
			||||||
 | 
					        brought_forward: ReportLineItem | None = None
 | 
				
			||||||
 | 
					        if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
 | 
				
			||||||
 | 
					            brought_forward = page_line_items[0]
 | 
				
			||||||
 | 
					            page_line_items = page_line_items[1:]
 | 
				
			||||||
 | 
					        total: ReportLineItem | None = None
 | 
				
			||||||
 | 
					        if len(page_line_items) > 0 and page_line_items[-1].is_total:
 | 
				
			||||||
 | 
					            total = page_line_items[-1]
 | 
				
			||||||
 | 
					            page_line_items = page_line_items[:-1]
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(currency=self.__currency,
 | 
				
			||||||
 | 
					                                        account=self.__account,
 | 
				
			||||||
 | 
					                                        period=self.__period,
 | 
				
			||||||
 | 
					                                        has_data=has_data,
 | 
				
			||||||
 | 
					                                        pagination=pagination,
 | 
				
			||||||
 | 
					                                        brought_forward=brought_forward,
 | 
				
			||||||
 | 
					                                        line_items=page_line_items,
 | 
				
			||||||
 | 
					                                        total=total)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/income-expenses.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										325
									
								
								src/accounting/report/reports/income_statement.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								src/accounting/report/reports/income_statement.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,325 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import render_template, Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, BaseAccount, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import ledger_url, income_statement_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportAccount:
 | 
				
			||||||
 | 
					    """An account in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, account: Account, amount: Decimal, url: str):
 | 
				
			||||||
 | 
					        """Constructs an account in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        :param url: The URL to the ledger of the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.amount: Decimal = amount
 | 
				
			||||||
 | 
					        """The amount of the account."""
 | 
				
			||||||
 | 
					        self.url: str = url
 | 
				
			||||||
 | 
					        """The URL to the ledger of the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccumulatedTotal:
 | 
				
			||||||
 | 
					    """An accumulated total."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: str):
 | 
				
			||||||
 | 
					        """Constructs an accumulated total.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: str = title
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.amount: Decimal = Decimal("0")
 | 
				
			||||||
 | 
					        """The amount of the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Subsection:
 | 
				
			||||||
 | 
					    """A subsection."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: BaseAccount):
 | 
				
			||||||
 | 
					        """Constructs a subsection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: BaseAccount = title
 | 
				
			||||||
 | 
					        """The title account."""
 | 
				
			||||||
 | 
					        self.accounts: list[ReportAccount] = []
 | 
				
			||||||
 | 
					        """The accounts in the subsection."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total of the subsection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total of the subsection.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.amount for x in self.accounts])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Section:
 | 
				
			||||||
 | 
					    """A section."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: BaseAccount, accumulated_title: str):
 | 
				
			||||||
 | 
					        """Constructs a section.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title account.
 | 
				
			||||||
 | 
					        :param accumulated_title: The title for the accumulated total.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: BaseAccount = title
 | 
				
			||||||
 | 
					        """The title account."""
 | 
				
			||||||
 | 
					        self.subsections: list[Subsection] = []
 | 
				
			||||||
 | 
					        """The subsections in the section."""
 | 
				
			||||||
 | 
					        self.accumulated: AccumulatedTotal \
 | 
				
			||||||
 | 
					            = AccumulatedTotal(accumulated_title)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def total(self) -> Decimal:
 | 
				
			||||||
 | 
					        """Returns the total of the section.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total of the section.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return sum([x.total for x in self.subsections])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, text: str | None, amount: str | Decimal | None):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param text: The text.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.text: str | None = text
 | 
				
			||||||
 | 
					        """The text."""
 | 
				
			||||||
 | 
					        self.amount: str | Decimal | None = amount
 | 
				
			||||||
 | 
					        """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.text, self.amount]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
 | 
					                 period: Period,
 | 
				
			||||||
 | 
					                 has_data: bool,
 | 
				
			||||||
 | 
					                 sections: list[Section], ):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param has_data: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool = has_data
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.sections: list[Section] = sections
 | 
				
			||||||
 | 
					        """The sections in the income statement."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: income_statement_url(currency, x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__has_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.INCOME_STATEMENT,
 | 
				
			||||||
 | 
					                             currency=self.currency,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currency_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._get_currency_options(
 | 
				
			||||||
 | 
					            lambda x: income_statement_url(x, self.period), self.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IncomeStatement(BaseReport):
 | 
				
			||||||
 | 
					    """The income statement."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, period: Period):
 | 
				
			||||||
 | 
					        """Constructs an income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.__sections: list[Section]
 | 
				
			||||||
 | 
					        """The sections."""
 | 
				
			||||||
 | 
					        self.__set_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __set_data(self) -> None:
 | 
				
			||||||
 | 
					        """Queries and sets data sections in the income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        balances: list[ReportAccount] = self.__query_balances()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        titles: list[BaseAccount] = BaseAccount.query\
 | 
				
			||||||
 | 
					            .filter(BaseAccount.code.in_({"4", "5", "6", "7", "8", "9"})).all()
 | 
				
			||||||
 | 
					        subtitles: list[BaseAccount] = BaseAccount.query\
 | 
				
			||||||
 | 
					            .filter(BaseAccount.code.in_({x.account.base_code[:2]
 | 
				
			||||||
 | 
					                                          for x in balances})).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        total_titles: dict[str, str] \
 | 
				
			||||||
 | 
					            = {"4": gettext("total operating revenue"),
 | 
				
			||||||
 | 
					               "5": gettext("gross income"),
 | 
				
			||||||
 | 
					               "6": gettext("operating income"),
 | 
				
			||||||
 | 
					               "7": gettext("before tax income"),
 | 
				
			||||||
 | 
					               "8": gettext("after tax income"),
 | 
				
			||||||
 | 
					               "9": gettext("net income or loss for current period")}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sections: dict[str, Section] \
 | 
				
			||||||
 | 
					            = {x.code: Section(x, total_titles[x.code]) for x in titles}
 | 
				
			||||||
 | 
					        subsections: dict[str, Subsection] \
 | 
				
			||||||
 | 
					            = {x.code: Subsection(x) for x in subtitles}
 | 
				
			||||||
 | 
					        for subsection in subsections.values():
 | 
				
			||||||
 | 
					            sections[subsection.title.code[0]].subsections.append(subsection)
 | 
				
			||||||
 | 
					        for balance in balances:
 | 
				
			||||||
 | 
					            subsections[balance.account.base_code[:2]].accounts.append(balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.__has_data = len(balances) > 0
 | 
				
			||||||
 | 
					        self.__sections = sorted(sections.values(), key=lambda x: x.title.code)
 | 
				
			||||||
 | 
					        total: Decimal = Decimal("0")
 | 
				
			||||||
 | 
					        for section in self.__sections:
 | 
				
			||||||
 | 
					            total = total + section.total
 | 
				
			||||||
 | 
					            section.accumulated.amount = total
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_balances(self) -> list[ReportAccount]:
 | 
				
			||||||
 | 
					        """Queries and returns the balances.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The balances.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        sub_conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [Account.base_code.startswith(str(x)) for x in range(4, 10)]
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
 | 
					               sa.or_(*sub_conditions)]
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, -JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
 | 
					        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
				
			||||||
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
 | 
					            .filter(*conditions)\
 | 
				
			||||||
 | 
					            .group_by(Account.id)\
 | 
				
			||||||
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
 | 
					        balances: list[sa.Row] = db.session.execute(select_balances).all()
 | 
				
			||||||
 | 
					        accounts: dict[int, Account] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in Account.query
 | 
				
			||||||
 | 
					               .filter(Account.id.in_([x.id for x in balances])).all()}
 | 
				
			||||||
 | 
					        return [ReportAccount(account=accounts[x.id],
 | 
				
			||||||
 | 
					                              amount=x.balance,
 | 
				
			||||||
 | 
					                              url=ledger_url(self.__currency,
 | 
				
			||||||
 | 
					                                             accounts[x.id],
 | 
				
			||||||
 | 
					                                             self.__period))
 | 
				
			||||||
 | 
					                for x in balances]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "income-statement-{currency}-{period}.csv"\
 | 
				
			||||||
 | 
					            .format(currency=self.__currency.code,
 | 
				
			||||||
 | 
					                    period=period_spec(self.__period))
 | 
				
			||||||
 | 
					        return csv_download(filename, self.__get_csv_rows())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_csv_rows(self) -> list[CSVRow]:
 | 
				
			||||||
 | 
					        """Composes and returns the CSV rows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The CSV rows.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        total_str: str = gettext("Total")
 | 
				
			||||||
 | 
					        rows: list[CSVRow] = [CSVRow(None, gettext("Amount"))]
 | 
				
			||||||
 | 
					        for section in self.__sections:
 | 
				
			||||||
 | 
					            rows.append(CSVRow(str(section.title).title(), None))
 | 
				
			||||||
 | 
					            for subsection in section.subsections:
 | 
				
			||||||
 | 
					                rows.append(CSVRow(f" {str(subsection.title).title()}", None))
 | 
				
			||||||
 | 
					                for account in subsection.accounts:
 | 
				
			||||||
 | 
					                    rows.append(CSVRow(f"  {str(account.account).title()}",
 | 
				
			||||||
 | 
					                                       account.amount))
 | 
				
			||||||
 | 
					                rows.append(CSVRow(f" {total_str}", subsection.total))
 | 
				
			||||||
 | 
					            rows.append(CSVRow(section.accumulated.title.title(),
 | 
				
			||||||
 | 
					                               section.accumulated.amount))
 | 
				
			||||||
 | 
					            rows.append(CSVRow(None, None))
 | 
				
			||||||
 | 
					        rows = rows[:-1]
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(currency=self.__currency,
 | 
				
			||||||
 | 
					                                        period=self.__period,
 | 
				
			||||||
 | 
					                                        has_data=self.__has_data,
 | 
				
			||||||
 | 
					                                        sections=self.__sections)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/income-statement.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										220
									
								
								src/accounting/report/reports/journal.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								src/accounting/report/reports/journal.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import render_template, Response
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import journal_url
 | 
				
			||||||
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportLineItem:
 | 
				
			||||||
 | 
					    """A line item in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, line_item: JournalEntryLineItem):
 | 
				
			||||||
 | 
					        """Constructs the line item in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param line_item: The journal entry line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.line_item: JournalEntryLineItem = line_item
 | 
				
			||||||
 | 
					        """The journal entry line item."""
 | 
				
			||||||
 | 
					        self.journal_entry: JournalEntry = line_item.journal_entry
 | 
				
			||||||
 | 
					        """The journal entry."""
 | 
				
			||||||
 | 
					        self.currency: Currency = line_item.currency
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.account: Account = line_item.account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.description: str | None = line_item.description
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.debit: Decimal | None = line_item.debit
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: Decimal | None = line_item.credit
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					        self.amount: Decimal = line_item.amount
 | 
				
			||||||
 | 
					        """The amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, journal_entry_date: str | date,
 | 
				
			||||||
 | 
					                 currency: str,
 | 
				
			||||||
 | 
					                 account: str,
 | 
				
			||||||
 | 
					                 description: str | None,
 | 
				
			||||||
 | 
					                 debit: str | Decimal | None,
 | 
				
			||||||
 | 
					                 credit: str | Decimal | None,
 | 
				
			||||||
 | 
					                 note: str | None):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
 | 
					        :param description: The description.
 | 
				
			||||||
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
 | 
					        :param note: The note.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.date: str | date = journal_entry_date
 | 
				
			||||||
 | 
					        """The date."""
 | 
				
			||||||
 | 
					        self.currency: str = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.account: str = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.description: str | None = description
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.debit: str | Decimal | None = debit
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: str | Decimal | None = credit
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					        self.note: str | None = note
 | 
				
			||||||
 | 
					        """The note."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.date, self.currency, self.account, self.description,
 | 
				
			||||||
 | 
					                self.debit, self.credit, self.note]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, period: Period,
 | 
				
			||||||
 | 
					                 pagination: Pagination[JournalEntryLineItem],
 | 
				
			||||||
 | 
					                 line_items: list[JournalEntryLineItem]):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param line_items: The line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.pagination: Pagination[JournalEntryLineItem] = pagination
 | 
				
			||||||
 | 
					        """The pagination."""
 | 
				
			||||||
 | 
					        self.line_items: list[JournalEntryLineItem] = line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: journal_url(x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return len(self.line_items) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.JOURNAL,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_csv_rows(line_items: list[JournalEntryLineItem]) -> list[CSVRow]:
 | 
				
			||||||
 | 
					    """Composes and returns the CSV rows from the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param line_items: The line items.
 | 
				
			||||||
 | 
					    :return: The CSV rows.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Currency"),
 | 
				
			||||||
 | 
					                                 gettext("Account"), gettext("Description"),
 | 
				
			||||||
 | 
					                                 gettext("Debit"), gettext("Credit"),
 | 
				
			||||||
 | 
					                                 gettext("Note"))]
 | 
				
			||||||
 | 
					    rows.extend([CSVRow(x.journal_entry.date, x.currency.code,
 | 
				
			||||||
 | 
					                        str(x.account).title(), x.description,
 | 
				
			||||||
 | 
					                        x.debit, x.credit, x.journal_entry.note)
 | 
				
			||||||
 | 
					                 for x in line_items])
 | 
				
			||||||
 | 
					    return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Journal(BaseReport):
 | 
				
			||||||
 | 
					    """The journal."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, period: Period):
 | 
				
			||||||
 | 
					        """Constructs a journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__line_items: list[JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = self.__query_line_items()
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_line_items(self) -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					        """Queries and returns the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        return JournalEntryLineItem.query.join(JournalEntry)\
 | 
				
			||||||
 | 
					            .filter(*conditions)\
 | 
				
			||||||
 | 
					            .order_by(JournalEntry.date,
 | 
				
			||||||
 | 
					                      JournalEntry.no,
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.is_debit.desc(),
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.no)\
 | 
				
			||||||
 | 
					            .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
 | 
					                     selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
 | 
					                     selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = f"journal-{period_spec(self.__period)}.csv"
 | 
				
			||||||
 | 
					        return csv_download(filename, get_csv_rows(self.__line_items))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pagination: Pagination[JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = Pagination[JournalEntryLineItem](self.__line_items,
 | 
				
			||||||
 | 
					                                               is_reversed=True)
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(period=self.__period,
 | 
				
			||||||
 | 
					                                        pagination=pagination,
 | 
				
			||||||
 | 
					                                        line_items=pagination.list)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/journal.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										417
									
								
								src/accounting/report/reports/ledger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								src/accounting/report/reports/ledger.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,417 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import url_for, render_template, Response
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import ledger_url
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportLineItem:
 | 
				
			||||||
 | 
					    """A line item in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, line_item: JournalEntryLineItem | None = None):
 | 
				
			||||||
 | 
					        """Constructs the line item in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param line_item: The journal entry line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.is_brought_forward: bool = False
 | 
				
			||||||
 | 
					        """Whether this is the brought-forward line item."""
 | 
				
			||||||
 | 
					        self.is_total: bool = False
 | 
				
			||||||
 | 
					        """Whether this is the total line item."""
 | 
				
			||||||
 | 
					        self.date: date | None = None
 | 
				
			||||||
 | 
					        """The date."""
 | 
				
			||||||
 | 
					        self.description: str | None = None
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.debit: Decimal | None = None
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: Decimal | None = None
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					        self.balance: Decimal | None = None
 | 
				
			||||||
 | 
					        """The balance."""
 | 
				
			||||||
 | 
					        self.note: str | None = None
 | 
				
			||||||
 | 
					        """The note."""
 | 
				
			||||||
 | 
					        self.url: str | None = None
 | 
				
			||||||
 | 
					        """The URL to the journal entry line item."""
 | 
				
			||||||
 | 
					        if line_item is not None:
 | 
				
			||||||
 | 
					            self.date = line_item.journal_entry.date
 | 
				
			||||||
 | 
					            self.description = line_item.description
 | 
				
			||||||
 | 
					            self.debit = line_item.amount if line_item.is_debit else None
 | 
				
			||||||
 | 
					            self.credit = None if line_item.is_debit else line_item.amount
 | 
				
			||||||
 | 
					            self.note = line_item.journal_entry.note
 | 
				
			||||||
 | 
					            self.url = url_for("accounting.journal-entry.detail",
 | 
				
			||||||
 | 
					                               journal_entry=line_item.journal_entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LineItemCollector:
 | 
				
			||||||
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, account: Account, period: Period):
 | 
				
			||||||
 | 
					        """Constructs the line item collector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period"""
 | 
				
			||||||
 | 
					        self.brought_forward: ReportLineItem | None
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.line_items: list[ReportLineItem]
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.total: ReportLineItem | None
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					        self.brought_forward = self.__get_brought_forward()
 | 
				
			||||||
 | 
					        self.line_items = self.__query_line_items()
 | 
				
			||||||
 | 
					        self.total = self.__get_total()
 | 
				
			||||||
 | 
					        self.__populate_balance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_brought_forward(self) -> ReportLineItem | None:
 | 
				
			||||||
 | 
					        """Queries, composes and returns the brought-forward line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The brought-forward line item, or None if the report starts
 | 
				
			||||||
 | 
					            from the beginning.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.__period.start is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        if self.__account.is_nominal:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount))
 | 
				
			||||||
 | 
					        select: sa.Select = sa.Select(balance_func).join(JournalEntry)\
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
 | 
					                       == self.__currency.code),
 | 
				
			||||||
 | 
					                    be(JournalEntryLineItem.account_id
 | 
				
			||||||
 | 
					                       == self.__account.id),
 | 
				
			||||||
 | 
					                    JournalEntry.date < self.__period.start)
 | 
				
			||||||
 | 
					        balance: int | None = db.session.scalar(select)
 | 
				
			||||||
 | 
					        if balance is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        line_item: ReportLineItem = ReportLineItem()
 | 
				
			||||||
 | 
					        line_item.is_brought_forward = True
 | 
				
			||||||
 | 
					        line_item.date = self.__period.start
 | 
				
			||||||
 | 
					        line_item.description = gettext("Brought forward")
 | 
				
			||||||
 | 
					        if balance > 0:
 | 
				
			||||||
 | 
					            line_item.debit = balance
 | 
				
			||||||
 | 
					        elif balance < 0:
 | 
				
			||||||
 | 
					            line_item.credit = -balance
 | 
				
			||||||
 | 
					        line_item.balance = balance
 | 
				
			||||||
 | 
					        return line_item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_line_items(self) -> list[ReportLineItem]:
 | 
				
			||||||
 | 
					        """Queries and returns the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code,
 | 
				
			||||||
 | 
					               JournalEntryLineItem.account_id == self.__account.id]
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        return [ReportLineItem(x) for x in JournalEntryLineItem.query
 | 
				
			||||||
 | 
					                .join(JournalEntry)
 | 
				
			||||||
 | 
					                .filter(*conditions)
 | 
				
			||||||
 | 
					                .order_by(JournalEntry.date,
 | 
				
			||||||
 | 
					                          JournalEntry.no,
 | 
				
			||||||
 | 
					                          JournalEntryLineItem.is_debit.desc(),
 | 
				
			||||||
 | 
					                          JournalEntryLineItem.no)
 | 
				
			||||||
 | 
					                .options(selectinload(JournalEntryLineItem.journal_entry))
 | 
				
			||||||
 | 
					                .all()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_total(self) -> ReportLineItem | None:
 | 
				
			||||||
 | 
					        """Composes the total line item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The total line item, or None if there is no data.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.brought_forward is None and len(self.line_items) == 0:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        line_item: ReportLineItem = ReportLineItem()
 | 
				
			||||||
 | 
					        line_item.is_total = True
 | 
				
			||||||
 | 
					        line_item.description = gettext("Total")
 | 
				
			||||||
 | 
					        line_item.debit = sum([x.debit for x in self.line_items
 | 
				
			||||||
 | 
					                               if x.debit is not None])
 | 
				
			||||||
 | 
					        line_item.credit = sum([x.credit for x in self.line_items
 | 
				
			||||||
 | 
					                                if x.credit is not None])
 | 
				
			||||||
 | 
					        line_item.balance = line_item.debit - line_item.credit
 | 
				
			||||||
 | 
					        if self.brought_forward is not None:
 | 
				
			||||||
 | 
					            line_item.balance \
 | 
				
			||||||
 | 
					                = self.brought_forward.balance + line_item.balance
 | 
				
			||||||
 | 
					        return line_item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __populate_balance(self) -> None:
 | 
				
			||||||
 | 
					        """Populates the balance of the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.__account.is_nominal:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					        balance: Decimal = 0 if self.brought_forward is None \
 | 
				
			||||||
 | 
					            else self.brought_forward.balance
 | 
				
			||||||
 | 
					        for line_item in self.line_items:
 | 
				
			||||||
 | 
					            if line_item.debit is not None:
 | 
				
			||||||
 | 
					                balance = balance + line_item.debit
 | 
				
			||||||
 | 
					            if line_item.credit is not None:
 | 
				
			||||||
 | 
					                balance = balance - line_item.credit
 | 
				
			||||||
 | 
					            line_item.balance = balance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, journal_entry_date: date | str | None,
 | 
				
			||||||
 | 
					                 description: str | None,
 | 
				
			||||||
 | 
					                 debit: str | Decimal | None,
 | 
				
			||||||
 | 
					                 credit: str | Decimal | None,
 | 
				
			||||||
 | 
					                 balance: str | Decimal | None,
 | 
				
			||||||
 | 
					                 note: str | None):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param journal_entry_date: The journal entry date.
 | 
				
			||||||
 | 
					        :param description: The description.
 | 
				
			||||||
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
 | 
					        :param balance: The balance.
 | 
				
			||||||
 | 
					        :param note: The note.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.date: date | str | None = journal_entry_date
 | 
				
			||||||
 | 
					        """The date."""
 | 
				
			||||||
 | 
					        self.description: str | None = description
 | 
				
			||||||
 | 
					        """The description."""
 | 
				
			||||||
 | 
					        self.debit: str | Decimal | None = debit
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: str | Decimal | None = credit
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					        self.balance: str | Decimal | None = balance
 | 
				
			||||||
 | 
					        """The balance."""
 | 
				
			||||||
 | 
					        self.note: str | None = note
 | 
				
			||||||
 | 
					        """The note."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.date, self.description,
 | 
				
			||||||
 | 
					                self.debit, self.credit, self.balance, self.note]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
 | 
					                 account: Account,
 | 
				
			||||||
 | 
					                 period: Period,
 | 
				
			||||||
 | 
					                 has_data: bool,
 | 
				
			||||||
 | 
					                 pagination: Pagination[ReportLineItem],
 | 
				
			||||||
 | 
					                 brought_forward: ReportLineItem | None,
 | 
				
			||||||
 | 
					                 line_items: list[ReportLineItem],
 | 
				
			||||||
 | 
					                 total: ReportLineItem | None):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param has_data: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        :param brought_forward: The brought-forward line item.
 | 
				
			||||||
 | 
					        :param line_items: The line items.
 | 
				
			||||||
 | 
					        :param total: The total line item.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__has_data: bool = has_data
 | 
				
			||||||
 | 
					        """True if there is any data, or False otherwise."""
 | 
				
			||||||
 | 
					        self.pagination: Pagination[ReportLineItem] = pagination
 | 
				
			||||||
 | 
					        """The pagination."""
 | 
				
			||||||
 | 
					        self.brought_forward: ReportLineItem | None = brought_forward
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.line_items: list[ReportLineItem] = line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.total: ReportLineItem | None = total
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: ledger_url(currency, account, x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.__has_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.LEDGER,
 | 
				
			||||||
 | 
					                             currency=self.currency,
 | 
				
			||||||
 | 
					                             account=self.account,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currency_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._get_currency_options(
 | 
				
			||||||
 | 
					            lambda x: ledger_url(x, self.account, self.period), self.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def account_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the account options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The account options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        in_use: sa.Select = sa.Select(JournalEntryLineItem.account_id)\
 | 
				
			||||||
 | 
					            .filter(be(JournalEntryLineItem.currency_code
 | 
				
			||||||
 | 
					                       == self.currency.code))\
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.account_id)
 | 
				
			||||||
 | 
					        return [OptionLink(str(x), ledger_url(self.currency, x, self.period),
 | 
				
			||||||
 | 
					                           x.id == self.account.id)
 | 
				
			||||||
 | 
					                for x in Account.query.filter(Account.id.in_(in_use))
 | 
				
			||||||
 | 
					                .order_by(Account.base_code, Account.no).all()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Ledger(BaseReport):
 | 
				
			||||||
 | 
					    """The ledger."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, account: Account, period: Period):
 | 
				
			||||||
 | 
					        """Constructs a ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        collector: LineItemCollector = LineItemCollector(
 | 
				
			||||||
 | 
					            self.__currency, self.__account, self.__period)
 | 
				
			||||||
 | 
					        self.__brought_forward: ReportLineItem | None \
 | 
				
			||||||
 | 
					            = collector.brought_forward
 | 
				
			||||||
 | 
					        """The brought-forward line item."""
 | 
				
			||||||
 | 
					        self.__line_items: list[ReportLineItem] = collector.line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					        self.__total: ReportLineItem | None = collector.total
 | 
				
			||||||
 | 
					        """The total line item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "ledger-{currency}-{account}-{period}.csv"\
 | 
				
			||||||
 | 
					            .format(currency=self.__currency.code, account=self.__account.code,
 | 
				
			||||||
 | 
					                    period=period_spec(self.__period))
 | 
				
			||||||
 | 
					        return csv_download(filename, self.__get_csv_rows())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_csv_rows(self) -> list[CSVRow]:
 | 
				
			||||||
 | 
					        """Composes and returns the CSV rows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The CSV rows.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        rows: list[CSVRow] = [CSVRow(gettext("Date"), gettext("Description"),
 | 
				
			||||||
 | 
					                                     gettext("Debit"), gettext("Credit"),
 | 
				
			||||||
 | 
					                                     gettext("Balance"), gettext("Note"))]
 | 
				
			||||||
 | 
					        if self.__brought_forward is not None:
 | 
				
			||||||
 | 
					            rows.append(CSVRow(self.__brought_forward.date,
 | 
				
			||||||
 | 
					                               self.__brought_forward.description,
 | 
				
			||||||
 | 
					                               self.__brought_forward.debit,
 | 
				
			||||||
 | 
					                               self.__brought_forward.credit,
 | 
				
			||||||
 | 
					                               self.__brought_forward.balance,
 | 
				
			||||||
 | 
					                               None))
 | 
				
			||||||
 | 
					        rows.extend([CSVRow(x.date, x.description,
 | 
				
			||||||
 | 
					                            x.debit, x.credit, x.balance, x.note)
 | 
				
			||||||
 | 
					                     for x in self.__line_items])
 | 
				
			||||||
 | 
					        if self.__total is not None:
 | 
				
			||||||
 | 
					            rows.append(CSVRow(gettext("Total"), None,
 | 
				
			||||||
 | 
					                               self.__total.debit, self.__total.credit,
 | 
				
			||||||
 | 
					                               self.__total.balance, None))
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_line_items: list[ReportLineItem] = []
 | 
				
			||||||
 | 
					        if self.__brought_forward is not None:
 | 
				
			||||||
 | 
					            all_line_items.append(self.__brought_forward)
 | 
				
			||||||
 | 
					        all_line_items.extend(self.__line_items)
 | 
				
			||||||
 | 
					        if self.__total is not None:
 | 
				
			||||||
 | 
					            all_line_items.append(self.__total)
 | 
				
			||||||
 | 
					        pagination: Pagination[ReportLineItem] \
 | 
				
			||||||
 | 
					            = Pagination[ReportLineItem](all_line_items, is_reversed=True)
 | 
				
			||||||
 | 
					        page_line_items: list[ReportLineItem] = pagination.list
 | 
				
			||||||
 | 
					        has_data: bool = len(page_line_items) > 0
 | 
				
			||||||
 | 
					        brought_forward: ReportLineItem | None = None
 | 
				
			||||||
 | 
					        if len(page_line_items) > 0 and page_line_items[0].is_brought_forward:
 | 
				
			||||||
 | 
					            brought_forward = page_line_items[0]
 | 
				
			||||||
 | 
					            page_line_items = page_line_items[1:]
 | 
				
			||||||
 | 
					        total: ReportLineItem | None = None
 | 
				
			||||||
 | 
					        if len(page_line_items) > 0 and page_line_items[-1].is_total:
 | 
				
			||||||
 | 
					            total = page_line_items[-1]
 | 
				
			||||||
 | 
					            page_line_items = page_line_items[:-1]
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(currency=self.__currency,
 | 
				
			||||||
 | 
					                                        account=self.__account,
 | 
				
			||||||
 | 
					                                        period=self.__period,
 | 
				
			||||||
 | 
					                                        has_data=has_data,
 | 
				
			||||||
 | 
					                                        pagination=pagination,
 | 
				
			||||||
 | 
					                                        brought_forward=brought_forward,
 | 
				
			||||||
 | 
					                                        line_items=page_line_items,
 | 
				
			||||||
 | 
					                                        total=total)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/ledger.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										216
									
								
								src/accounting/report/reports/search.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/accounting/report/reports/search.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,216 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The search.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import Response, render_template, request
 | 
				
			||||||
 | 
					from sqlalchemy.orm import selectinload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, CurrencyL10n, Account, AccountL10n, \
 | 
				
			||||||
 | 
					    JournalEntry, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import csv_download
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.utils.cast import be
 | 
				
			||||||
 | 
					from accounting.utils.pagination import Pagination
 | 
				
			||||||
 | 
					from accounting.utils.query import parse_query_keywords
 | 
				
			||||||
 | 
					from .journal import get_csv_rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LineItemCollector:
 | 
				
			||||||
 | 
					    """The line item collector."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        """Constructs the line item collector."""
 | 
				
			||||||
 | 
					        self.line_items: list[JournalEntryLineItem] = self.__query_line_items()
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __query_line_items(self) -> list[JournalEntryLineItem]:
 | 
				
			||||||
 | 
					        """Queries and returns the line items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        keywords: list[str] = parse_query_keywords(request.args.get("q"))
 | 
				
			||||||
 | 
					        if len(keywords) == 0:
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] = []
 | 
				
			||||||
 | 
					        for k in keywords:
 | 
				
			||||||
 | 
					            sub_conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					                = [JournalEntryLineItem.description.icontains(k),
 | 
				
			||||||
 | 
					                   JournalEntryLineItem.account_id.in_(
 | 
				
			||||||
 | 
					                       self.__get_account_condition(k)),
 | 
				
			||||||
 | 
					                   JournalEntryLineItem.currency_code.in_(
 | 
				
			||||||
 | 
					                       self.__get_currency_condition(k)),
 | 
				
			||||||
 | 
					                   JournalEntryLineItem.journal_entry_id.in_(
 | 
				
			||||||
 | 
					                       self.__get_journal_entry_condition(k))]
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                sub_conditions.append(
 | 
				
			||||||
 | 
					                    JournalEntryLineItem.amount == Decimal(k))
 | 
				
			||||||
 | 
					            except ArithmeticError:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            conditions.append(sa.or_(*sub_conditions))
 | 
				
			||||||
 | 
					        return JournalEntryLineItem.query.join(JournalEntry)\
 | 
				
			||||||
 | 
					            .filter(*conditions)\
 | 
				
			||||||
 | 
					            .order_by(JournalEntry.date,
 | 
				
			||||||
 | 
					                      JournalEntry.no,
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.is_debit,
 | 
				
			||||||
 | 
					                      JournalEntryLineItem.no)\
 | 
				
			||||||
 | 
					            .options(selectinload(JournalEntryLineItem.account),
 | 
				
			||||||
 | 
					                     selectinload(JournalEntryLineItem.currency),
 | 
				
			||||||
 | 
					                     selectinload(JournalEntryLineItem.journal_entry)).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_account_condition(k: str) -> sa.Select:
 | 
				
			||||||
 | 
					        """Composes and returns the condition to filter the account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param k: The keyword.
 | 
				
			||||||
 | 
					        :return: The condition to filter the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        code: sa.BinaryExpression = Account.base_code + "-" \
 | 
				
			||||||
 | 
					            + sa.func.substr("000" + sa.cast(Account.no, sa.String),
 | 
				
			||||||
 | 
					                             sa.func.char_length(sa.cast(Account.no,
 | 
				
			||||||
 | 
					                                                         sa.String)) + 1)
 | 
				
			||||||
 | 
					        select_l10n: sa.Select = sa.select(AccountL10n.account_id)\
 | 
				
			||||||
 | 
					            .filter(AccountL10n.title.icontains(k))
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [Account.base_code.contains(k),
 | 
				
			||||||
 | 
					               Account.title_l10n.icontains(k),
 | 
				
			||||||
 | 
					               code.contains(k),
 | 
				
			||||||
 | 
					               Account.id.in_(select_l10n)]
 | 
				
			||||||
 | 
					        if k in gettext("Needs Offset"):
 | 
				
			||||||
 | 
					            conditions.append(Account.is_need_offset)
 | 
				
			||||||
 | 
					        return sa.select(Account.id).filter(sa.or_(*conditions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_currency_condition(k: str) -> sa.Select:
 | 
				
			||||||
 | 
					        """Composes and returns the condition to filter the currency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param k: The keyword.
 | 
				
			||||||
 | 
					        :return: The condition to filter the currency.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        select_l10n: sa.Select = sa.select(CurrencyL10n.currency_code)\
 | 
				
			||||||
 | 
					            .filter(CurrencyL10n.name.icontains(k))
 | 
				
			||||||
 | 
					        return sa.select(Currency.code).filter(
 | 
				
			||||||
 | 
					            sa.or_(Currency.code.icontains(k),
 | 
				
			||||||
 | 
					                   Currency.name_l10n.icontains(k),
 | 
				
			||||||
 | 
					                   Currency.code.in_(select_l10n)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def __get_journal_entry_condition(k: str) -> sa.Select:
 | 
				
			||||||
 | 
					        """Composes and returns the condition to filter the journal entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param k: The keyword.
 | 
				
			||||||
 | 
					        :return: The condition to filter the journal entry.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntry.note.icontains(k)]
 | 
				
			||||||
 | 
					        journal_entry_date: datetime
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            journal_entry_date = datetime.strptime(k, "%Y")
 | 
				
			||||||
 | 
					            conditions.append(
 | 
				
			||||||
 | 
					                be(sa.extract("year", JournalEntry.date)
 | 
				
			||||||
 | 
					                   == journal_entry_date.year))
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            journal_entry_date = datetime.strptime(k, "%Y/%m")
 | 
				
			||||||
 | 
					            conditions.append(sa.and_(
 | 
				
			||||||
 | 
					                sa.extract("year", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.year,
 | 
				
			||||||
 | 
					                sa.extract("month", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.month))
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            journal_entry_date = datetime.strptime(f"2000/{k}", "%Y/%m/%d")
 | 
				
			||||||
 | 
					            conditions.append(sa.and_(
 | 
				
			||||||
 | 
					                sa.extract("month", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.month,
 | 
				
			||||||
 | 
					                sa.extract("day", JournalEntry.date)
 | 
				
			||||||
 | 
					                == journal_entry_date.day))
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        return sa.select(JournalEntry.id).filter(sa.or_(*conditions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, pagination: Pagination[JournalEntryLineItem],
 | 
				
			||||||
 | 
					                 line_items: list[JournalEntryLineItem]):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param line_items: The search result line items.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.pagination: Pagination[JournalEntryLineItem] = pagination
 | 
				
			||||||
 | 
					        """The pagination."""
 | 
				
			||||||
 | 
					        self.line_items: list[JournalEntryLineItem] = line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return len(self.line_items) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.SEARCH)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Search(BaseReport):
 | 
				
			||||||
 | 
					    """The search."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        """Constructs a search."""
 | 
				
			||||||
 | 
					        self.__line_items: list[JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = LineItemCollector().line_items
 | 
				
			||||||
 | 
					        """The line items."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "search-{q}.csv".format(q=request.args["q"])
 | 
				
			||||||
 | 
					        return csv_download(filename, get_csv_rows(self.__line_items))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        pagination: Pagination[JournalEntryLineItem] \
 | 
				
			||||||
 | 
					            = Pagination[JournalEntryLineItem](self.__line_items,
 | 
				
			||||||
 | 
					                                               is_reversed=True)
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(pagination=pagination,
 | 
				
			||||||
 | 
					                                        line_items=pagination.list)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/search.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										242
									
								
								src/accounting/report/reports/trial_balance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								src/accounting/report/reports/trial_balance.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,242 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import Response, render_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account, JournalEntry, \
 | 
				
			||||||
 | 
					    JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.report.period import Period, PeriodChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.base_page_params import BasePageParams
 | 
				
			||||||
 | 
					from accounting.report.utils.base_report import BaseReport
 | 
				
			||||||
 | 
					from accounting.report.utils.csv_export import BaseCSVRow, csv_download, \
 | 
				
			||||||
 | 
					    period_spec
 | 
				
			||||||
 | 
					from accounting.report.utils.option_link import OptionLink
 | 
				
			||||||
 | 
					from accounting.report.utils.report_chooser import ReportChooser
 | 
				
			||||||
 | 
					from accounting.report.utils.report_type import ReportType
 | 
				
			||||||
 | 
					from accounting.report.utils.urls import ledger_url, trial_balance_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportAccount:
 | 
				
			||||||
 | 
					    """An account in the report."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, account: Account, amount: Decimal, url: str):
 | 
				
			||||||
 | 
					        """Constructs an account in the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        :param amount: The amount.
 | 
				
			||||||
 | 
					        :param url: The URL to the ledger of the account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.account: Account = account
 | 
				
			||||||
 | 
					        """The account."""
 | 
				
			||||||
 | 
					        self.debit: Decimal | None = amount if amount > 0 else None
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: Decimal | None = -amount if amount < 0 else None
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					        self.url: str = url
 | 
				
			||||||
 | 
					        """The URL to the ledger of the account."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Total:
 | 
				
			||||||
 | 
					    """The totals."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, debit: Decimal, credit: Decimal):
 | 
				
			||||||
 | 
					        """Constructs the total in the trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.debit: Decimal | None = debit
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: Decimal | None = credit
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CSVRow(BaseCSVRow):
 | 
				
			||||||
 | 
					    """A row in the CSV."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, text: str | None,
 | 
				
			||||||
 | 
					                 debit: str | Decimal | None,
 | 
				
			||||||
 | 
					                 credit: str | Decimal | None):
 | 
				
			||||||
 | 
					        """Constructs a row in the CSV.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param text: The text.
 | 
				
			||||||
 | 
					        :param debit: The debit amount.
 | 
				
			||||||
 | 
					        :param credit: The credit amount.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.text: str | None = text
 | 
				
			||||||
 | 
					        """The text."""
 | 
				
			||||||
 | 
					        self.debit: str | Decimal | None = debit
 | 
				
			||||||
 | 
					        """The debit amount."""
 | 
				
			||||||
 | 
					        self.credit: str | Decimal | None = credit
 | 
				
			||||||
 | 
					        """The credit amount."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return [self.text, self.debit, self.credit]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PageParams(BasePageParams):
 | 
				
			||||||
 | 
					    """The HTML page parameters."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency,
 | 
				
			||||||
 | 
					                 period: Period,
 | 
				
			||||||
 | 
					                 accounts: list[ReportAccount],
 | 
				
			||||||
 | 
					                 total: Total):
 | 
				
			||||||
 | 
					        """Constructs the HTML page parameters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param accounts: The accounts in the trial balance.
 | 
				
			||||||
 | 
					        :param total: The total of the trial balance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.accounts: list[ReportAccount] = accounts
 | 
				
			||||||
 | 
					        """The accounts in the trial balance."""
 | 
				
			||||||
 | 
					        self.total: Total = total
 | 
				
			||||||
 | 
					        """The total of the trial balance."""
 | 
				
			||||||
 | 
					        self.period_chooser: PeriodChooser = PeriodChooser(
 | 
				
			||||||
 | 
					            lambda x: trial_balance_url(currency, x))
 | 
				
			||||||
 | 
					        """The period chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return len(self.accounts) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return ReportChooser(ReportType.TRIAL_BALANCE,
 | 
				
			||||||
 | 
					                             currency=self.currency,
 | 
				
			||||||
 | 
					                             period=self.period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def currency_options(self) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self._get_currency_options(
 | 
				
			||||||
 | 
					            lambda x: trial_balance_url(x, self.period), self.currency)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TrialBalance(BaseReport):
 | 
				
			||||||
 | 
					    """The trial balance."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, currency: Currency, period: Period):
 | 
				
			||||||
 | 
					        """Constructs a trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__currency: Currency = currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__period: Period = period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__accounts: list[ReportAccount]
 | 
				
			||||||
 | 
					        """The accounts in the trial balance."""
 | 
				
			||||||
 | 
					        self.__total: Total
 | 
				
			||||||
 | 
					        """The total of the trial balance."""
 | 
				
			||||||
 | 
					        self.__set_data()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __set_data(self) -> None:
 | 
				
			||||||
 | 
					        """Queries and sets data sections in the trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: None.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        conditions: list[sa.BinaryExpression] \
 | 
				
			||||||
 | 
					            = [JournalEntryLineItem.currency_code == self.__currency.code]
 | 
				
			||||||
 | 
					        if self.__period.start is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date >= self.__period.start)
 | 
				
			||||||
 | 
					        if self.__period.end is not None:
 | 
				
			||||||
 | 
					            conditions.append(JournalEntry.date <= self.__period.end)
 | 
				
			||||||
 | 
					        balance_func: sa.Function = sa.func.sum(sa.case(
 | 
				
			||||||
 | 
					            (JournalEntryLineItem.is_debit, JournalEntryLineItem.amount),
 | 
				
			||||||
 | 
					            else_=-JournalEntryLineItem.amount)).label("balance")
 | 
				
			||||||
 | 
					        select_balances: sa.Select = sa.select(Account.id, balance_func)\
 | 
				
			||||||
 | 
					            .join(JournalEntry).join(Account)\
 | 
				
			||||||
 | 
					            .filter(*conditions)\
 | 
				
			||||||
 | 
					            .group_by(Account.id)\
 | 
				
			||||||
 | 
					            .order_by(Account.base_code, Account.no)
 | 
				
			||||||
 | 
					        balances: list[sa.Row] = db.session.execute(select_balances).all()
 | 
				
			||||||
 | 
					        accounts: dict[int, Account] \
 | 
				
			||||||
 | 
					            = {x.id: x for x in Account.query
 | 
				
			||||||
 | 
					               .filter(Account.id.in_([x.id for x in balances])).all()}
 | 
				
			||||||
 | 
					        self.__accounts = [ReportAccount(account=accounts[x.id],
 | 
				
			||||||
 | 
					                                         amount=x.balance,
 | 
				
			||||||
 | 
					                                         url=ledger_url(self.__currency,
 | 
				
			||||||
 | 
					                                                        accounts[x.id],
 | 
				
			||||||
 | 
					                                                        self.__period))
 | 
				
			||||||
 | 
					                           for x in balances]
 | 
				
			||||||
 | 
					        self.__total = Total(
 | 
				
			||||||
 | 
					            sum([x.debit for x in self.__accounts if x.debit is not None]),
 | 
				
			||||||
 | 
					            sum([x.credit for x in self.__accounts if x.credit is not None]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        filename: str = "trial-balance-{currency}-{period}.csv"\
 | 
				
			||||||
 | 
					            .format(currency=self.__currency.code,
 | 
				
			||||||
 | 
					                    period=period_spec(self.__period))
 | 
				
			||||||
 | 
					        return csv_download(filename, self.__get_csv_rows())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __get_csv_rows(self) -> list[CSVRow]:
 | 
				
			||||||
 | 
					        """Composes and returns the CSV rows.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The CSV rows.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        rows: list[CSVRow] = [CSVRow(gettext("Account"), gettext("Debit"),
 | 
				
			||||||
 | 
					                                     gettext("Credit"))]
 | 
				
			||||||
 | 
					        rows.extend([CSVRow(str(x.account).title(), x.debit, x.credit)
 | 
				
			||||||
 | 
					                     for x in self.__accounts])
 | 
				
			||||||
 | 
					        rows.append(CSVRow(gettext("Total"), self.__total.debit,
 | 
				
			||||||
 | 
					                           self.__total.credit))
 | 
				
			||||||
 | 
					        return rows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        params: PageParams = PageParams(currency=self.__currency,
 | 
				
			||||||
 | 
					                                        period=self.__period,
 | 
				
			||||||
 | 
					                                        accounts=self.__accounts,
 | 
				
			||||||
 | 
					                                        total=self.__total)
 | 
				
			||||||
 | 
					        return render_template("accounting/report/trial-balance.html",
 | 
				
			||||||
 | 
					                               report=params)
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/accounting/report/template_filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/accounting/report/template_filters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The template filters for the reports.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.template_filters import format_amount as core_format_amount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_amount(value: Decimal | None) -> str | None:
 | 
				
			||||||
 | 
					    """Formats an amount for the report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The amount.
 | 
				
			||||||
 | 
					    :return: The formatted amount text.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if value is None:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					    is_negative: bool = value < 0
 | 
				
			||||||
 | 
					    formatted: str = core_format_amount(abs(value))
 | 
				
			||||||
 | 
					    if is_negative:
 | 
				
			||||||
 | 
					        formatted = f"({formatted})"
 | 
				
			||||||
 | 
					    return formatted
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/accounting/report/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/accounting/report/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The utilities for the reports.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
							
								
								
									
										88
									
								
								src/accounting/report/utils/base_page_params.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/accounting/report/utils/base_page_params.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The page parameters of a report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					from urllib.parse import urlparse, ParseResult, parse_qsl, urlencode, \
 | 
				
			||||||
 | 
					    urlunparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					from flask import request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import Currency, JournalEntryLineItem
 | 
				
			||||||
 | 
					from accounting.utils.journal_entry_types import JournalEntryType
 | 
				
			||||||
 | 
					from .option_link import OptionLink
 | 
				
			||||||
 | 
					from .report_chooser import ReportChooser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BasePageParams(ABC):
 | 
				
			||||||
 | 
					    """The base HTML page parameters class."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def has_data(self) -> bool:
 | 
				
			||||||
 | 
					        """Returns whether there is any data on the page.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: True if there is any data, or False otherwise.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def report_chooser(self) -> ReportChooser:
 | 
				
			||||||
 | 
					        """Returns the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report chooser.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def journal_entry_types(self) -> t.Type[JournalEntryType]:
 | 
				
			||||||
 | 
					        """Returns the journal entry types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The journal entry types.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return JournalEntryType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def csv_uri(self) -> str:
 | 
				
			||||||
 | 
					        uri: str = request.full_path if request.query_string \
 | 
				
			||||||
 | 
					            else request.path
 | 
				
			||||||
 | 
					        uri_p: ParseResult = urlparse(uri)
 | 
				
			||||||
 | 
					        params: list[tuple[str, str]] = parse_qsl(uri_p.query)
 | 
				
			||||||
 | 
					        params = [x for x in params if x[0] != "as"]
 | 
				
			||||||
 | 
					        params.append(("as", "csv"))
 | 
				
			||||||
 | 
					        parts: list[str] = list(uri_p)
 | 
				
			||||||
 | 
					        parts[4] = urlencode(params)
 | 
				
			||||||
 | 
					        return urlunparse(parts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _get_currency_options(get_url: t.Callable[[Currency], str],
 | 
				
			||||||
 | 
					                              active_currency: Currency) -> list[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param get_url: The callback to return the URL of a currency.
 | 
				
			||||||
 | 
					        :param active_currency: The active currency.
 | 
				
			||||||
 | 
					        :return: The currency options.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        in_use: set[str] = set(db.session.scalars(
 | 
				
			||||||
 | 
					            sa.select(JournalEntryLineItem.currency_code)
 | 
				
			||||||
 | 
					            .group_by(JournalEntryLineItem.currency_code)).all())
 | 
				
			||||||
 | 
					        return [OptionLink(str(x), get_url(x), x.code == active_currency.code)
 | 
				
			||||||
 | 
					                for x in Currency.query.filter(Currency.code.in_(in_use))
 | 
				
			||||||
 | 
					                .order_by(Currency.code).all()]
 | 
				
			||||||
							
								
								
									
										40
									
								
								src/accounting/report/utils/base_report.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/accounting/report/utils/base_report.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The base report.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseReport(ABC):
 | 
				
			||||||
 | 
					    """The base report class."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def csv(self) -> Response:
 | 
				
			||||||
 | 
					        """Returns the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The response of the report for download.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def html(self) -> str:
 | 
				
			||||||
 | 
					        """Composes and returns the report as HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The report as HTML.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
							
								
								
									
										108
									
								
								src/accounting/report/utils/csv_export.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/accounting/report/utils/csv_export.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The utilities to export the report as CSV for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					from datetime import timedelta, date
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					from io import StringIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.report.period import Period
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseCSVRow(ABC):
 | 
				
			||||||
 | 
					    """The base CSV row."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    @abstractmethod
 | 
				
			||||||
 | 
					    def values(self) -> list[str | Decimal | None]:
 | 
				
			||||||
 | 
					        """Returns the values of the row.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The values of the row.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def csv_download(filename: str, rows: list[BaseCSVRow]) -> Response:
 | 
				
			||||||
 | 
					    """Exports the data rows as a CSV file for download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param filename: The download file name.
 | 
				
			||||||
 | 
					    :param rows: The data rows.
 | 
				
			||||||
 | 
					    :return: The response for download the CSV file.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    with StringIO() as fp:
 | 
				
			||||||
 | 
					        writer = csv.writer(fp)
 | 
				
			||||||
 | 
					        writer.writerows([x.values for x in rows])
 | 
				
			||||||
 | 
					        fp.seek(0)
 | 
				
			||||||
 | 
					        response: Response = Response(fp.read(), mimetype="text/csv")
 | 
				
			||||||
 | 
					        response.headers["Content-Disposition"] \
 | 
				
			||||||
 | 
					            = f"attachment; filename={filename}"
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def period_spec(period: Period) -> str:
 | 
				
			||||||
 | 
					    """Constructs the period specification to be used in the filename.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The period specification to be used in the filename.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    start: str | None = __get_start_str(period.start)
 | 
				
			||||||
 | 
					    end: str | None = __get_end_str(period.end)
 | 
				
			||||||
 | 
					    if period.start is None and period.end is None:
 | 
				
			||||||
 | 
					        return "all-time"
 | 
				
			||||||
 | 
					    if start == end:
 | 
				
			||||||
 | 
					        return start
 | 
				
			||||||
 | 
					    if period.start is None:
 | 
				
			||||||
 | 
					        return f"until-{end}"
 | 
				
			||||||
 | 
					    if period.end is None:
 | 
				
			||||||
 | 
					        return f"since-{start}"
 | 
				
			||||||
 | 
					    return f"{start}-{end}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_start_str(start: date | None) -> str | None:
 | 
				
			||||||
 | 
					    """Returns the string representation of the start date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param start: The start date.
 | 
				
			||||||
 | 
					    :return: The string representation of the start date, or None if the start
 | 
				
			||||||
 | 
					        date is None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if start is None:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    if start.month == 1 and start.day == 1:
 | 
				
			||||||
 | 
					        return str(start.year)
 | 
				
			||||||
 | 
					    if start.day == 1:
 | 
				
			||||||
 | 
					        return start.strftime("%Y%m")
 | 
				
			||||||
 | 
					    return start.strftime("%Y%m%d")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_end_str(end: date | None) -> str | None:
 | 
				
			||||||
 | 
					    """Returns the string representation of the end date.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param end: The end date.
 | 
				
			||||||
 | 
					    :return: The string representation of the end date, or None if the end
 | 
				
			||||||
 | 
					        date is None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if end is None:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    if end.month == 12 and end.day == 31:
 | 
				
			||||||
 | 
					        return str(end.year)
 | 
				
			||||||
 | 
					    if (end + timedelta(days=1)).day == 1:
 | 
				
			||||||
 | 
					        return end.strftime("%Y%m")
 | 
				
			||||||
 | 
					    return end.strftime("%Y%m%d")
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/accounting/report/utils/option_link.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/accounting/report/utils/option_link.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The option link.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OptionLink:
 | 
				
			||||||
 | 
					    """An option link."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, title: str, url: str, is_active: bool,
 | 
				
			||||||
 | 
					                 fa_icon: str | None = None):
 | 
				
			||||||
 | 
					        """Constructs an option link.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param title: The title.
 | 
				
			||||||
 | 
					        :param url: The URL.
 | 
				
			||||||
 | 
					        :param is_active: True if active, or False otherwise
 | 
				
			||||||
 | 
					        :param fa_icon: The font-awesome icon, if any.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.title: str = title
 | 
				
			||||||
 | 
					        """The title."""
 | 
				
			||||||
 | 
					        self.url: str = url
 | 
				
			||||||
 | 
					        """The URL."""
 | 
				
			||||||
 | 
					        self.is_active: bool = is_active
 | 
				
			||||||
 | 
					        """True if active, or False otherwise."""
 | 
				
			||||||
 | 
					        self.fa_icon: str | None = fa_icon
 | 
				
			||||||
 | 
					        """The font-awesome icon, if any."""
 | 
				
			||||||
							
								
								
									
										159
									
								
								src/accounting/report/utils/report_chooser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/accounting/report/utils/report_chooser.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This file is largely taken from the NanoParma ERP project, first written in
 | 
				
			||||||
 | 
					2021/9/16 by imacat (imacat@nanoparma.com).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask_babel import LazyString
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
 | 
					from accounting.report.period import Period, get_period
 | 
				
			||||||
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from .option_link import OptionLink
 | 
				
			||||||
 | 
					from .report_type import ReportType
 | 
				
			||||||
 | 
					from .urls import journal_url, ledger_url, income_expenses_url, \
 | 
				
			||||||
 | 
					    trial_balance_url, income_statement_url, balance_sheet_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportChooser:
 | 
				
			||||||
 | 
					    """The report chooser."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, active_report: ReportType,
 | 
				
			||||||
 | 
					                 period: Period | None = None,
 | 
				
			||||||
 | 
					                 currency: Currency | None = None,
 | 
				
			||||||
 | 
					                 account: Account | None = None):
 | 
				
			||||||
 | 
					        """Constructs the report chooser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param active_report: The active report.
 | 
				
			||||||
 | 
					        :param period: The period.
 | 
				
			||||||
 | 
					        :param currency: The currency.
 | 
				
			||||||
 | 
					        :param account: The account.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.__active_report: ReportType = active_report
 | 
				
			||||||
 | 
					        """The currently active report."""
 | 
				
			||||||
 | 
					        self.__period: Period = get_period() if period is None else period
 | 
				
			||||||
 | 
					        """The period."""
 | 
				
			||||||
 | 
					        self.__currency: Currency = db.session.get(
 | 
				
			||||||
 | 
					            Currency, default_currency_code()) \
 | 
				
			||||||
 | 
					            if currency is None else currency
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__account: Account = Account.cash() if account is None \
 | 
				
			||||||
 | 
					            else account
 | 
				
			||||||
 | 
					        """The currency."""
 | 
				
			||||||
 | 
					        self.__reports: list[OptionLink] = []
 | 
				
			||||||
 | 
					        """The links to the reports."""
 | 
				
			||||||
 | 
					        self.current_report: str | LazyString = ""
 | 
				
			||||||
 | 
					        """The title of the current report."""
 | 
				
			||||||
 | 
					        self.is_search: bool = active_report == ReportType.SEARCH
 | 
				
			||||||
 | 
					        """Whether the current report is the search page."""
 | 
				
			||||||
 | 
					        self.__reports.append(self.__journal)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__ledger)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__income_expenses)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__trial_balance)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__income_statement)
 | 
				
			||||||
 | 
					        self.__reports.append(self.__balance_sheet)
 | 
				
			||||||
 | 
					        for report in self.__reports:
 | 
				
			||||||
 | 
					            if report.is_active:
 | 
				
			||||||
 | 
					                self.current_report = report.title
 | 
				
			||||||
 | 
					        if self.is_search:
 | 
				
			||||||
 | 
					            self.current_report = gettext("Search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __journal(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The journal.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Journal"), journal_url(self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.JOURNAL,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-book")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __ledger(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The ledger.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Ledger"),
 | 
				
			||||||
 | 
					                          ledger_url(self.__currency, self.__account,
 | 
				
			||||||
 | 
					                                     self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.LEDGER,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-clipboard")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __income_expenses(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The income and expenses log.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        account: Account = self.__account
 | 
				
			||||||
 | 
					        if not re.match(r"[12][12]", account.base_code):
 | 
				
			||||||
 | 
					            account: Account = Account.cash()
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Income and Expenses Log"),
 | 
				
			||||||
 | 
					                          income_expenses_url(self.__currency,
 | 
				
			||||||
 | 
					                                              CurrentAccount(account),
 | 
				
			||||||
 | 
					                                              self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.INCOME_EXPENSES,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-money-bill-wave")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __trial_balance(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The trial balance.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Trial Balance"),
 | 
				
			||||||
 | 
					                          trial_balance_url(self.__currency, self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.TRIAL_BALANCE,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-scale-unbalanced")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __income_statement(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The income statement.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Income Statement"),
 | 
				
			||||||
 | 
					                          income_statement_url(self.__currency, self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.INCOME_STATEMENT,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-file-invoice-dollar")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def __balance_sheet(self) -> OptionLink:
 | 
				
			||||||
 | 
					        """Returns the balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The balance sheet.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return OptionLink(gettext("Balance Sheet"),
 | 
				
			||||||
 | 
					                          balance_sheet_url(self.__currency, self.__period),
 | 
				
			||||||
 | 
					                          self.__active_report == ReportType.BALANCE_SHEET,
 | 
				
			||||||
 | 
					                          fa_icon="fa-solid fa-scale-balanced")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __iter__(self) -> t.Iterator[OptionLink]:
 | 
				
			||||||
 | 
					        """Returns the iteration of the reports.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: The iteration of the reports.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return iter(self.__reports)
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/accounting/report/utils/report_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/accounting/report/utils/report_type.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The report types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReportType(Enum):
 | 
				
			||||||
 | 
					    """The report types."""
 | 
				
			||||||
 | 
					    JOURNAL: str = "journal"
 | 
				
			||||||
 | 
					    """The journal."""
 | 
				
			||||||
 | 
					    LEDGER: str = "ledger"
 | 
				
			||||||
 | 
					    """The ledger."""
 | 
				
			||||||
 | 
					    INCOME_EXPENSES: str = "income-expenses"
 | 
				
			||||||
 | 
					    """The income and expenses log."""
 | 
				
			||||||
 | 
					    TRIAL_BALANCE: str = "trial-balance"
 | 
				
			||||||
 | 
					    """The trial balance."""
 | 
				
			||||||
 | 
					    INCOME_STATEMENT: str = "income-statement"
 | 
				
			||||||
 | 
					    """The income statement."""
 | 
				
			||||||
 | 
					    BALANCE_SHEET: str = "balance-sheet"
 | 
				
			||||||
 | 
					    """The balance sheet."""
 | 
				
			||||||
 | 
					    SEARCH: str = "search"
 | 
				
			||||||
 | 
					    """The search."""
 | 
				
			||||||
							
								
								
									
										118
									
								
								src/accounting/report/utils/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/accounting/report/utils/urls.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The utilities to get the ledger URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import url_for
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
 | 
					from accounting.report.period import Period
 | 
				
			||||||
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def journal_url(period: Period) \
 | 
				
			||||||
 | 
					        -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of a journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the journal.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.journal-default")
 | 
				
			||||||
 | 
					    return url_for("accounting.report.journal", period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def ledger_url(currency: Currency, account: Account, period: Period) \
 | 
				
			||||||
 | 
					        -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of a ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the ledger.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.ledger-default",
 | 
				
			||||||
 | 
					                       currency=currency, account=account)
 | 
				
			||||||
 | 
					    return url_for("accounting.report.ledger",
 | 
				
			||||||
 | 
					                   currency=currency, account=account,
 | 
				
			||||||
 | 
					                   period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def income_expenses_url(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
 | 
					                        period: Period) -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of an income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the income and expenses log.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if currency.code == default_currency_code() \
 | 
				
			||||||
 | 
					            and account.code == options.default_ie_account_code \
 | 
				
			||||||
 | 
					            and period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.default")
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.income-expenses-default",
 | 
				
			||||||
 | 
					                       currency=currency, account=account)
 | 
				
			||||||
 | 
					    return url_for("accounting.report.income-expenses",
 | 
				
			||||||
 | 
					                   currency=currency, account=account,
 | 
				
			||||||
 | 
					                   period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def trial_balance_url(currency: Currency, period: Period) -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of a trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the trial balance.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.trial-balance-default",
 | 
				
			||||||
 | 
					                       currency=currency)
 | 
				
			||||||
 | 
					    return url_for("accounting.report.trial-balance",
 | 
				
			||||||
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def income_statement_url(currency: Currency, period: Period) -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of an income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the income statement.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.income-statement-default",
 | 
				
			||||||
 | 
					                       currency=currency)
 | 
				
			||||||
 | 
					    return url_for("accounting.report.income-statement",
 | 
				
			||||||
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def balance_sheet_url(currency: Currency, period: Period) -> str:
 | 
				
			||||||
 | 
					    """Returns the URL of a balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The URL of the balance sheet.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if period.is_default:
 | 
				
			||||||
 | 
					        return url_for("accounting.report.balance-sheet-default",
 | 
				
			||||||
 | 
					                       currency=currency)
 | 
				
			||||||
 | 
					    return url_for("accounting.report.balance-sheet",
 | 
				
			||||||
 | 
					                   currency=currency, period=period)
 | 
				
			||||||
							
								
								
									
										299
									
								
								src/accounting/report/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								src/accounting/report/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,299 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The views for the report management.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from flask import Blueprint, request, Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting import db
 | 
				
			||||||
 | 
					from accounting.models import Currency, Account
 | 
				
			||||||
 | 
					from accounting.report.period import Period, get_period
 | 
				
			||||||
 | 
					from accounting.template_globals import default_currency_code
 | 
				
			||||||
 | 
					from accounting.utils.current_account import CurrentAccount
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					from accounting.utils.permission import has_permission, can_view
 | 
				
			||||||
 | 
					from .reports import Journal, Ledger, IncomeExpenses, TrialBalance, \
 | 
				
			||||||
 | 
					    IncomeStatement, BalanceSheet, Search
 | 
				
			||||||
 | 
					from .template_filters import format_amount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bp: Blueprint = Blueprint("report", __name__)
 | 
				
			||||||
 | 
					"""The view blueprint for the reports."""
 | 
				
			||||||
 | 
					bp.add_app_template_filter(format_amount, "accounting_report_format_amount")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("", endpoint="default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_report() -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income and expenses log in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The income and expenses log in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_income_expenses(
 | 
				
			||||||
 | 
					        db.session.get(Currency, default_currency_code()),
 | 
				
			||||||
 | 
					        options.default_ie_account,
 | 
				
			||||||
 | 
					        get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("journal", endpoint="journal-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_journal() -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the journal in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The journal in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_journal(get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("journal/<period:period>", endpoint="journal")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_journal(period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The journal in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_journal(period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_journal(period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the journal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The journal in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: Journal = Journal(period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("ledger/<currency:currency>/<account:account>",
 | 
				
			||||||
 | 
					        endpoint="ledger-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_ledger(currency: Currency, account: Account) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the ledger in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :return: The ledger in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_ledger(currency, account, get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("ledger/<currency:currency>/<account:account>/<period:period>",
 | 
				
			||||||
 | 
					        endpoint="ledger")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_ledger(currency: Currency, account: Account, period: Period) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The ledger in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_ledger(currency, account, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_ledger(currency: Currency, account: Account, period: Period) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the ledger.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The ledger in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: Ledger = Ledger(currency, account, period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("income-expenses/<currency:currency>/<ieAccount:account>",
 | 
				
			||||||
 | 
					        endpoint="income-expenses-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_income_expenses(currency: Currency, account: CurrentAccount) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income and expenses log in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :return: The income and expenses log in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_income_expenses(currency, account, get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get(
 | 
				
			||||||
 | 
					    "income-expenses/<currency:currency>/<ieAccount:account>/<period:period>",
 | 
				
			||||||
 | 
					    endpoint="income-expenses")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_income_expenses(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
 | 
					                        period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The income and expenses log in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_income_expenses(currency, account, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_income_expenses(currency: Currency, account: CurrentAccount,
 | 
				
			||||||
 | 
					                          period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income and expenses log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param account: The account.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The income and expenses log in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: IncomeExpenses = IncomeExpenses(currency, account, period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("trial-balance/<currency:currency>",
 | 
				
			||||||
 | 
					        endpoint="trial-balance-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_trial_balance(currency: Currency) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the trial balance in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :return: The trial balance in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_trial_balance(currency, get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("trial-balance/<currency:currency>/<period:period>",
 | 
				
			||||||
 | 
					        endpoint="trial-balance")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_trial_balance(currency: Currency, period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The trial balance in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_trial_balance(currency, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_trial_balance(currency: Currency, period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the trial balance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The trial balance in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: TrialBalance = TrialBalance(currency, period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("income-statement/<currency:currency>",
 | 
				
			||||||
 | 
					        endpoint="income-statement-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_income_statement(currency: Currency) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income statement in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :return: The income statement in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_income_statement(currency, get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("income-statement/<currency:currency>/<period:period>",
 | 
				
			||||||
 | 
					        endpoint="income-statement")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_income_statement(currency: Currency, period: Period) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The income statement in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_income_statement(currency, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_income_statement(currency: Currency, period: Period) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the income statement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The income statement in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: IncomeStatement = IncomeStatement(currency, period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("balance-sheet/<currency:currency>",
 | 
				
			||||||
 | 
					        endpoint="balance-sheet-default")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_default_balance_sheet(currency: Currency) -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the balance sheet in the default period.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :return: The balance sheet in the default period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_balance_sheet(currency, get_period())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("balance-sheet/<currency:currency>/<period:period>",
 | 
				
			||||||
 | 
					        endpoint="balance-sheet")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def get_balance_sheet(currency: Currency, period: Period) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The balance sheet in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return __get_balance_sheet(currency, period)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def __get_balance_sheet(currency: Currency, period: Period) \
 | 
				
			||||||
 | 
					        -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the balance sheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param currency: The currency.
 | 
				
			||||||
 | 
					    :param period: The period.
 | 
				
			||||||
 | 
					    :return: The balance sheet in the period.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: BalanceSheet = BalanceSheet(currency, period)
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.get("search", endpoint="search")
 | 
				
			||||||
 | 
					@has_permission(can_view)
 | 
				
			||||||
 | 
					def search() -> str | Response:
 | 
				
			||||||
 | 
					    """Returns the search result.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The search result.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    report: Search = Search()
 | 
				
			||||||
 | 
					    if "as" in request.args and request.args["as"] == "csv":
 | 
				
			||||||
 | 
					        return report.csv()
 | 
				
			||||||
 | 
					    return report.html()
 | 
				
			||||||
@@ -24,15 +24,70 @@
 | 
				
			|||||||
.accounting-clickable {
 | 
					.accounting-clickable {
 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.btn-group .btn .accounting-search-input {
 | 
					.form-floating > textarea.form-control {
 | 
				
			||||||
    min-height: calc(1em + .5rem + 2px);
 | 
					    height: 6rem;
 | 
				
			||||||
    padding: 0 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.btn-group .btn .accounting-search-label button {
 | 
					.accounting-dragged {
 | 
				
			||||||
    border: none;
 | 
					    color: #141619;
 | 
				
			||||||
 | 
					    background-color: #D3D3D4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.form-control.accounting-disabled {
 | 
				
			||||||
 | 
					    background-color: #e9ecef;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** The toolbar */
 | 
				
			||||||
 | 
					.accounting-toolbar {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-toolbar .input-group > .input-group-text {
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
    background-color: transparent;
 | 
					    background-color: transparent;
 | 
				
			||||||
    color: inherit;
 | 
					    color: inherit;
 | 
				
			||||||
    padding-right: 0;
 | 
					    border: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-toolbar .input-group > .input-group-text > button {
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-toolbar form.btn > .form-control {
 | 
				
			||||||
 | 
					    min-height: calc(1.5em + 2px);
 | 
				
			||||||
 | 
					    padding-top: 0.1rem;
 | 
				
			||||||
 | 
					    padding-bottom: 0.1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@media(min-width: 768px) {
 | 
				
			||||||
 | 
					    .accounting-toolbar > .btn, .accounting-toolbar > .btn-group > .btn {
 | 
				
			||||||
 | 
					        border-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .accounting-toolbar > .btn:first-child, .accounting-toolbar > .btn-group:first-child > .btn {
 | 
				
			||||||
 | 
					        border-top-left-radius: 0.375rem;
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 0.375rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .accounting-toolbar > .btn:last-child, .accounting-toolbar > .btn-group:last-child > .btn {
 | 
				
			||||||
 | 
					        border-top-right-radius: 0.375rem;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 0.375rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .accounting-toolbar .btn.input-group {
 | 
				
			||||||
 | 
					        width: 16rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@media(max-width:767px) {
 | 
				
			||||||
 | 
					    .accounting-toolbar > .btn:not(form), .accounting-toolbar > .btn-group > .btn {
 | 
				
			||||||
 | 
					        height: 3.2rem;
 | 
				
			||||||
 | 
					        width: 3.2rem;
 | 
				
			||||||
 | 
					        border-radius: 50%;
 | 
				
			||||||
 | 
					        margin-right: 0.5rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .accounting-toolbar > a.btn, .accounting-toolbar > .btn-group > a.btn {
 | 
				
			||||||
 | 
					        padding-top: 0.7rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .accounting-toolbar > form.btn {
 | 
				
			||||||
 | 
					        width: 12rem;
 | 
				
			||||||
 | 
					        height: 2.6rem;
 | 
				
			||||||
 | 
					        border-radius: 0.375rem;
 | 
				
			||||||
 | 
					        margin-top: 0.3rem;
 | 
				
			||||||
 | 
					        margin-left: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** The card layout */
 | 
					/** The card layout */
 | 
				
			||||||
@@ -51,6 +106,42 @@
 | 
				
			|||||||
    font-size: 1.4rem;
 | 
					    font-size: 1.4rem;
 | 
				
			||||||
    color: #373b3e;
 | 
					    color: #373b3e;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.accounting-sheet {
 | 
				
			||||||
 | 
					    padding: 2em 1.5em;
 | 
				
			||||||
 | 
					    margin: 1em;
 | 
				
			||||||
 | 
					    background-color: #F8F9FA;
 | 
				
			||||||
 | 
					    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-sheet h2 {
 | 
				
			||||||
 | 
					    border-bottom: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Links between objects */
 | 
				
			||||||
 | 
					.accounting-original-line-item {
 | 
				
			||||||
 | 
					    border-top: thin solid darkslategray;
 | 
				
			||||||
 | 
					    padding: 0.2rem 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-original-line-item a {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-original-line-item a:hover {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-offset-line-items {
 | 
				
			||||||
 | 
					    border-top: thin solid darkslategray;
 | 
				
			||||||
 | 
					    padding: 0.2rem 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-offset-line-items ul li {
 | 
				
			||||||
 | 
					    list-style: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-offset-line-items ul li a {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-offset-line-items ul li a:hover {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** The option selector */
 | 
					/** The option selector */
 | 
				
			||||||
.accounting-selector-list {
 | 
					.accounting-selector-list {
 | 
				
			||||||
@@ -58,6 +149,196 @@
 | 
				
			|||||||
    overflow-y: scroll;
 | 
					    overflow-y: scroll;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** The journal entry management */
 | 
				
			||||||
 | 
					.accounting-currency-control {
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-currency-content {
 | 
				
			||||||
 | 
					    width: calc(100% - 3rem);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-line-item-content {
 | 
				
			||||||
 | 
					    width: calc(100% - 3rem);
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-list-group-stripped .list-group-item:nth-child(2n+1) {
 | 
				
			||||||
 | 
					    background-color: #f2f2f2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-list-group-hover .list-group-item:hover {
 | 
				
			||||||
 | 
					    background-color: #ececec;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-entry-line-item {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-entry-line-item-header {
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					    border-bottom: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.list-group-item.accounting-journal-entry-line-item-total {
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					    border-top: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-line-item-editor-original-line-item-content {
 | 
				
			||||||
 | 
					    width: calc(100% - 3rem);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The report table */
 | 
				
			||||||
 | 
					.accounting-report-table-header, .accounting-report-table-footer {
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-header {
 | 
				
			||||||
 | 
					    border-bottom: thin solid slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-footer {
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					    border-top: thin solid slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-row {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					a.accounting-report-table-row {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-row > div {
 | 
				
			||||||
 | 
					    padding: .5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table .accounting-amount {
 | 
				
			||||||
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-body .accounting-amount {
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-body .accounting-report-table-row:nth-child(2n+1) {
 | 
				
			||||||
 | 
					    background-color: #f2f2f2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-report-table-body .accounting-report-table-row:hover {
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0.075);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr 1fr 2fr 4fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-ledger-real-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr 4fr 1fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-ledger-real-table .accounting-report-table-footer .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 5fr 1fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-ledger-nominal-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr 4fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-ledger-nominal-table .accounting-report-table-footer .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 5fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-expenses-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr 2fr 4fr 1fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-expenses-table .accounting-report-table-footer .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 7fr 1fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-trial-balance-table .accounting-report-table-header {
 | 
				
			||||||
 | 
					    border-bottom: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-trial-balance-table .accounting-report-table-footer {
 | 
				
			||||||
 | 
					    border-top: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-trial-balance-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    grid-template-columns: 3fr 1fr 1fr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-table .accounting-report-table-body {
 | 
				
			||||||
 | 
					    border-top: thick double slategray;
 | 
				
			||||||
 | 
					    border-bottom: thick double slategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-table .accounting-report-table-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-table .accounting-report-table-header .accounting-report-table-row {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-section, .accounting-income-statement-total {
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-subsection, .accounting-income-statement-subtotal {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-subtotal {
 | 
				
			||||||
 | 
					    border-top: thin solid darkslategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* Indents */
 | 
				
			||||||
 | 
					.accounting-income-statement-subsection {
 | 
				
			||||||
 | 
					    margin-left: 0.5rem;
 | 
				
			||||||
 | 
					    margin-right: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-account, .accounting-income-statement-subtotal {
 | 
				
			||||||
 | 
					    margin-left: 1rem;
 | 
				
			||||||
 | 
					    margin-right: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* A visual blank line between categories */
 | 
				
			||||||
 | 
					.accounting-income-statement-section {
 | 
				
			||||||
 | 
					    margin-top: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-section:first-child {
 | 
				
			||||||
 | 
					    margin-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-total {
 | 
				
			||||||
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-income-statement-total:last-child {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-section, .accounting-balance-sheet-total {
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-section {
 | 
				
			||||||
 | 
					    border-bottom: thick double darkslategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-total {
 | 
				
			||||||
 | 
					    border-top: thick double darkslategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-subtotal {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					    border-top: thick double darkslategray;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-account {
 | 
				
			||||||
 | 
					    margin-left: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-balance-sheet-total .accounting-amount, .accounting-balance-sheet-subtotal, .accounting-amount {
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The accounting report */
 | 
				
			||||||
 | 
					.accounting-mobile-journal-credit {
 | 
				
			||||||
 | 
					    padding-left: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The description editor */
 | 
				
			||||||
 | 
					.accounting-description-editor-buttons .btn {
 | 
				
			||||||
 | 
					    margin-bottom: 0.3rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The order of the journal entries in a same day */
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item, .accounting-journal-entry-order-item:hover {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-journal-entry-order-item-currency {
 | 
				
			||||||
 | 
					    margin-left: 0.5rem;
 | 
				
			||||||
 | 
					    border-top: thin solid lightgray;
 | 
				
			||||||
 | 
					    margin-top: 0.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The illustration of the description template for the recurring transactions */
 | 
				
			||||||
 | 
					.accounting-recurring-description-template-illustration p {
 | 
				
			||||||
 | 
					    margin: 0.2rem 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-recurring-description-template-illustration ul {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The Material Design text field (floating form control in Bootstrap) */
 | 
					/* The Material Design text field (floating form control in Bootstrap) */
 | 
				
			||||||
.accounting-material-text-field {
 | 
					.accounting-material-text-field {
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
@@ -82,7 +363,7 @@
 | 
				
			|||||||
.accounting-material-fab {
 | 
					.accounting-material-fab {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    right: 2rem;
 | 
					    right: 2rem;
 | 
				
			||||||
    bottom: 1rem;
 | 
					    bottom: 2rem;
 | 
				
			||||||
    z-index: 10;
 | 
					    z-index: 10;
 | 
				
			||||||
    flex-direction: column-reverse;
 | 
					    flex-direction: column-reverse;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -96,6 +377,36 @@
 | 
				
			|||||||
.accounting-material-fab .btn:hover, .accounting-material-fab .btn:focus {
 | 
					.accounting-material-fab .btn:hover, .accounting-material-fab .btn:focus {
 | 
				
			||||||
    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0,0,0,.12);
 | 
					    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0,0,0,.12);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.accounting-btn-material-fab {
 | 
				
			||||||
 | 
					    transition: transform .1s ease-in-out, right .1s ease-in-out, bottom .1s ease-in-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.show .accounting-btn-material-fab {
 | 
				
			||||||
 | 
					    transform: scale(1.5) rotate(-45deg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-material-fab-speed-dial-group {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    right: -2rem;
 | 
				
			||||||
 | 
					    bottom: -7rem;
 | 
				
			||||||
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transform: scale(0.1);
 | 
				
			||||||
 | 
					    line-height: 5.5rem;
 | 
				
			||||||
 | 
					    transition: opacity .1s ease-in-out, transform .1s ease-in-out, right .1s ease-in-out, bottom .1s ease-in-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.show .accounting-material-fab-speed-dial-group {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    transform: scale(0.6);
 | 
				
			||||||
 | 
					    right: -0.5rem;
 | 
				
			||||||
 | 
					    bottom: 0.7rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-material-fab-speed-dial-group .btn {
 | 
				
			||||||
 | 
					    background-color: white;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0,0,0,.12);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.accounting-material-fab-speed-dial-group .btn:hover, .accounting-material-fab-speed-dial-group .btn:focus {
 | 
				
			||||||
 | 
					    box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0,0,0,.12);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* The Material Design form switch */
 | 
					/* The Material Design form switch */
 | 
				
			||||||
@media(max-width:767px) {
 | 
					@media(max-width:767px) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,165 +20,415 @@
 | 
				
			|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 * First written: 2023/2/1
 | 
					 * First written: 2023/2/1
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initializes the page JavaScript.
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", function () {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
    initializeBaseAccountSelector();
 | 
					    AccountForm.initialize();
 | 
				
			||||||
    document.getElementById("accounting-base-code")
 | 
					 | 
				
			||||||
        .onchange = validateBase;
 | 
					 | 
				
			||||||
    document.getElementById("accounting-title")
 | 
					 | 
				
			||||||
        .onchange = validateTitle;
 | 
					 | 
				
			||||||
    document.getElementById("accounting-form")
 | 
					 | 
				
			||||||
        .onsubmit = validateForm;
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Initializes the base account selector.
 | 
					 * The account form.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function initializeBaseAccountSelector() {
 | 
					class AccountForm {
 | 
				
			||||||
    const selector = document.getElementById("accounting-base-selector-model");
 | 
					
 | 
				
			||||||
    const base = document.getElementById("accounting-base");
 | 
					    /**
 | 
				
			||||||
    const baseCode = document.getElementById("accounting-base-code");
 | 
					     * The base account selector
 | 
				
			||||||
    const baseContent = document.getElementById("accounting-base-content");
 | 
					     * @type {BaseAccountSelector}
 | 
				
			||||||
    const options = Array.from(document.getElementsByClassName("accounting-base-option"));
 | 
					     */
 | 
				
			||||||
    const btnClear = document.getElementById("accounting-btn-clear-base");
 | 
					    #baseAccountSelector;
 | 
				
			||||||
    selector.addEventListener("show.bs.modal", function () {
 | 
					
 | 
				
			||||||
        base.classList.add("accounting-not-empty");
 | 
					    /**
 | 
				
			||||||
        options.forEach(function (item) {
 | 
					     * The form element
 | 
				
			||||||
            item.classList.remove("active");
 | 
					     * @type {HTMLFormElement}
 | 
				
			||||||
        });
 | 
					     */
 | 
				
			||||||
        const selected = document.getElementById("accounting-base-option-" + baseCode.value);
 | 
					    #formElement;
 | 
				
			||||||
        if (selected !== null) {
 | 
					
 | 
				
			||||||
            selected.classList.add("active");
 | 
					    /**
 | 
				
			||||||
        }
 | 
					     * The control of the base account
 | 
				
			||||||
    });
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
    selector.addEventListener("hidden.bs.modal", function () {
 | 
					     */
 | 
				
			||||||
        if (baseCode.value === "") {
 | 
					    #baseControl;
 | 
				
			||||||
            base.classList.remove("accounting-not-empty");
 | 
					
 | 
				
			||||||
        }
 | 
					    /**
 | 
				
			||||||
    });
 | 
					     * The input of the base account
 | 
				
			||||||
    options.forEach(function (option) {
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
        option.onclick = function () {
 | 
					     */
 | 
				
			||||||
            baseCode.value = option.dataset.code;
 | 
					    #baseCode;
 | 
				
			||||||
            baseContent.innerText = option.dataset.content;
 | 
					
 | 
				
			||||||
            btnClear.classList.add("btn-danger");
 | 
					    /**
 | 
				
			||||||
            btnClear.classList.remove("btn-secondary")
 | 
					     * The base account
 | 
				
			||||||
            btnClear.disabled = false;
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
            validateBase();
 | 
					     */
 | 
				
			||||||
            bootstrap.Modal.getInstance(selector).hide();
 | 
					    #base;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message for the base account
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #baseError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The title
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message of the title
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #titleError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The control of the is-need-offset option
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isNeedOffsetControl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The is-need-offset option
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isNeedOffset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the account form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        this.#baseAccountSelector = new BaseAccountSelector(this);
 | 
				
			||||||
 | 
					        this.#formElement = document.getElementById("accounting-form");
 | 
				
			||||||
 | 
					        this.#baseControl = document.getElementById("accounting-base-control");
 | 
				
			||||||
 | 
					        this.#baseCode = document.getElementById("accounting-base-code");
 | 
				
			||||||
 | 
					        this.#base = document.getElementById("accounting-base");
 | 
				
			||||||
 | 
					        this.#baseError = document.getElementById("accounting-base-error");
 | 
				
			||||||
 | 
					        this.#title = document.getElementById("accounting-title");
 | 
				
			||||||
 | 
					        this.#titleError = document.getElementById("accounting-title-error");
 | 
				
			||||||
 | 
					        this.#isNeedOffsetControl = document.getElementById("accounting-is-need-offset-control");
 | 
				
			||||||
 | 
					        this.#isNeedOffset = document.getElementById("accounting-is-need-offset");
 | 
				
			||||||
 | 
					        this.#formElement.onsubmit = () => {
 | 
				
			||||||
 | 
					            return this.#validate();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.#baseControl.onclick = () => {
 | 
				
			||||||
 | 
					            this.#baseControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					            this.#baseAccountSelector.onOpen();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    btnClear.onclick = function () {
 | 
					 | 
				
			||||||
        baseCode.value = "";
 | 
					 | 
				
			||||||
        baseContent.innerText = "";
 | 
					 | 
				
			||||||
        btnClear.classList.add("btn-secondary")
 | 
					 | 
				
			||||||
        btnClear.classList.remove("btn-danger");
 | 
					 | 
				
			||||||
        btnClear.disabled = true;
 | 
					 | 
				
			||||||
        validateBase();
 | 
					 | 
				
			||||||
        bootstrap.Modal.getInstance(selector).hide();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    initializeBaseAccountQuery();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					    /**
 | 
				
			||||||
 * Initializes the query on the base account options.
 | 
					     * Returns the base code.
 | 
				
			||||||
 *
 | 
					     *
 | 
				
			||||||
 * @private
 | 
					     * @return {string|null}
 | 
				
			||||||
 */
 | 
					     */
 | 
				
			||||||
function initializeBaseAccountQuery() {
 | 
					    get baseCode() {
 | 
				
			||||||
    const query = document.getElementById("accounting-base-selector-query");
 | 
					        return this.#baseCode.value === ""? null: this.#baseCode.value;
 | 
				
			||||||
    const optionList = document.getElementById("accounting-base-option-list");
 | 
					    }
 | 
				
			||||||
    const options = Array.from(document.getElementsByClassName("accounting-base-option"));
 | 
					
 | 
				
			||||||
    const queryNoResult = document.getElementById("accounting-base-option-no-result");
 | 
					    /**
 | 
				
			||||||
    query.addEventListener("input", function () {
 | 
					     * The callback when the base account selector is closed.
 | 
				
			||||||
        console.log(query.value);
 | 
					     *
 | 
				
			||||||
        if (query.value === "") {
 | 
					     */
 | 
				
			||||||
            options.forEach(function (option) {
 | 
					    onBaseAccountSelectorClosed() {
 | 
				
			||||||
                option.classList.remove("d-none");
 | 
					        if (this.#baseCode.value === "") {
 | 
				
			||||||
            });
 | 
					            this.#baseControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
            optionList.classList.remove("d-none");
 | 
					 | 
				
			||||||
            queryNoResult.classList.add("d-none");
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let hasAnyMatched = false;
 | 
					    }
 | 
				
			||||||
        options.forEach(function (option) {
 | 
					
 | 
				
			||||||
            const queryValues = JSON.parse(option.dataset.queryValues);
 | 
					    /**
 | 
				
			||||||
            let isMatched = false;
 | 
					     * Saves the selected base account.
 | 
				
			||||||
            for (let i = 0; i < queryValues.length; i++) {
 | 
					     *
 | 
				
			||||||
                if (queryValues[i].includes(query.value)) {
 | 
					     * @param account {BaseAccountOption} the selected base account
 | 
				
			||||||
                    isMatched = true;
 | 
					     */
 | 
				
			||||||
                    break;
 | 
					    saveBaseAccount(account) {
 | 
				
			||||||
                }
 | 
					        this.#baseCode.value = account.code;
 | 
				
			||||||
            }
 | 
					        this.#base.innerText = account.text;
 | 
				
			||||||
            if (isMatched) {
 | 
					        if (["1", "2", "3"].includes(account.code.substring(0, 1))) {
 | 
				
			||||||
                option.classList.remove("d-none");
 | 
					            this.#isNeedOffsetControl.classList.remove("d-none");
 | 
				
			||||||
                hasAnyMatched = true;
 | 
					            this.#isNeedOffset.disabled = false;
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                option.classList.add("d-none");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        if (!hasAnyMatched) {
 | 
					 | 
				
			||||||
            optionList.classList.add("d-none");
 | 
					 | 
				
			||||||
            queryNoResult.classList.remove("d-none");
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            optionList.classList.remove("d-none");
 | 
					            this.#isNeedOffsetControl.classList.add("d-none");
 | 
				
			||||||
            queryNoResult.classList.add("d-none");
 | 
					            this.#isNeedOffset.disabled = true;
 | 
				
			||||||
 | 
					            this.#isNeedOffset.checked = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					        this.#validateBase();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clears the base account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    clearBaseAccount() {
 | 
				
			||||||
 | 
					        this.#baseCode.value = "";
 | 
				
			||||||
 | 
					        this.#base.innerText = "";
 | 
				
			||||||
 | 
					        this.#validateBase();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validate() {
 | 
				
			||||||
 | 
					        let isValid = true;
 | 
				
			||||||
 | 
					        isValid = this.#validateBase() && isValid;
 | 
				
			||||||
 | 
					        isValid = this.#validateTitle() && isValid;
 | 
				
			||||||
 | 
					        return isValid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the base account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateBase() {
 | 
				
			||||||
 | 
					        if (this.#baseCode.value === "") {
 | 
				
			||||||
 | 
					            this.#baseControl.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#baseError.innerText = A_("Please select the base account.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#baseControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#baseError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the title.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateTitle() {
 | 
				
			||||||
 | 
					        this.#title.value = this.#title.value.trim();
 | 
				
			||||||
 | 
					        if (this.#title.value === "") {
 | 
				
			||||||
 | 
					            this.#title.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#titleError.innerText = A_("Please fill in the title.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#title.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#titleError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account form
 | 
				
			||||||
 | 
					     * @type {AccountForm} the form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static #form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static initialize() {
 | 
				
			||||||
 | 
					        this.#form = new AccountForm();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Validates the form.
 | 
					 * The base account selector.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function validateForm() {
 | 
					class BaseAccountSelector {
 | 
				
			||||||
    let isValid = true;
 | 
					
 | 
				
			||||||
    isValid = validateBase() && isValid;
 | 
					    /**
 | 
				
			||||||
    isValid = validateTitle() && isValid;
 | 
					     * The account form
 | 
				
			||||||
    return isValid;
 | 
					     * @type {AccountForm}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The selector modal
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The query input
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message when the query has no result
 | 
				
			||||||
 | 
					     * @type {HTMLParagraphElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryNoResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The option list
 | 
				
			||||||
 | 
					     * @type {HTMLUListElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #optionList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The options
 | 
				
			||||||
 | 
					     * @type {BaseAccountOption[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The button to clear the base account value
 | 
				
			||||||
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #clearButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the base account selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param form {AccountForm} the form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(form) {
 | 
				
			||||||
 | 
					        this.form = form;
 | 
				
			||||||
 | 
					        const prefix = "accounting-base-selector";
 | 
				
			||||||
 | 
					        this.#modal = document.getElementById(`${prefix}-modal`);
 | 
				
			||||||
 | 
					        this.#query = document.getElementById(`${prefix}-query`);
 | 
				
			||||||
 | 
					        this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
 | 
				
			||||||
 | 
					        this.#optionList = document.getElementById(`${prefix}-option-list`);
 | 
				
			||||||
 | 
					        this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
 | 
				
			||||||
 | 
					        this.#clearButton = document.getElementById(`${prefix}-clear`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
 | 
				
			||||||
 | 
					        this.#query.oninput = () => this.#filterOptions();
 | 
				
			||||||
 | 
					        this.#clearButton.onclick = () => this.form.clearBaseAccount();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Filters the options.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #filterOptions() {
 | 
				
			||||||
 | 
					        let isAnyMatched = false;
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            if (option.isMatched(this.#query.value)) {
 | 
				
			||||||
 | 
					                option.setShown(true);
 | 
				
			||||||
 | 
					                isAnyMatched = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                option.setShown(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!isAnyMatched) {
 | 
				
			||||||
 | 
					            this.#optionList.classList.add("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.remove("d-none");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#optionList.classList.remove("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The callback when the base account selector is shown.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onOpen() {
 | 
				
			||||||
 | 
					        this.#query.value = "";
 | 
				
			||||||
 | 
					        this.#filterOptions();
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            option.setActive(option.code === this.form.baseCode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.form.baseCode === null) {
 | 
				
			||||||
 | 
					            this.#clearButton.classList.add("btn-secondary")
 | 
				
			||||||
 | 
					            this.#clearButton.classList.remove("btn-danger");
 | 
				
			||||||
 | 
					            this.#clearButton.disabled = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#clearButton.classList.add("btn-danger");
 | 
				
			||||||
 | 
					            this.#clearButton.classList.remove("btn-secondary")
 | 
				
			||||||
 | 
					            this.#clearButton.disabled = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Validates the base account.
 | 
					 * A base account option.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					 | 
				
			||||||
 * @private
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function validateBase() {
 | 
					class BaseAccountOption {
 | 
				
			||||||
    const field = document.getElementById("accounting-base-code");
 | 
					
 | 
				
			||||||
    const error = document.getElementById("accounting-base-code-error");
 | 
					    /**
 | 
				
			||||||
    const displayField = document.getElementById("accounting-base");
 | 
					     * The base account selector
 | 
				
			||||||
    field.value = field.value.trim();
 | 
					     * @type {BaseAccountSelector}
 | 
				
			||||||
    if (field.value === "") {
 | 
					     */
 | 
				
			||||||
        displayField.classList.add("is-invalid");
 | 
					    #selector;
 | 
				
			||||||
        error.innerText = A_("Please select the base account.");
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The element
 | 
				
			||||||
 | 
					     * @type {HTMLLIElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account code
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account text
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The values to query against
 | 
				
			||||||
 | 
					     * @type {string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryValues;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the account in the base account selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param selector {BaseAccountSelector} the base account selector
 | 
				
			||||||
 | 
					     * @param element {HTMLLIElement} the element
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(selector, element) {
 | 
				
			||||||
 | 
					        this.#selector = selector;
 | 
				
			||||||
 | 
					        this.#element = element;
 | 
				
			||||||
 | 
					        this.code = element.dataset.code;
 | 
				
			||||||
 | 
					        this.text = element.dataset.text;
 | 
				
			||||||
 | 
					        this.#queryValues = JSON.parse(element.dataset.queryValues);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#element.onclick = () => this.#selector.form.saveBaseAccount(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the account matches the query.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param query {string} the query term
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isMatched(query) {
 | 
				
			||||||
 | 
					        if (query === "") {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const queryValue of this.#queryValues) {
 | 
				
			||||||
 | 
					            if (queryValue.toLowerCase().includes(query.toLowerCase())) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    displayField.classList.remove("is-invalid");
 | 
					 | 
				
			||||||
    error.innerText = "";
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					    /**
 | 
				
			||||||
 * Validates the title.
 | 
					     * Sets whether the option is shown.
 | 
				
			||||||
 *
 | 
					     *
 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					     * @param isShown {boolean} true to show, or false otherwise
 | 
				
			||||||
 * @private
 | 
					     */
 | 
				
			||||||
 */
 | 
					    setShown(isShown) {
 | 
				
			||||||
function validateTitle() {
 | 
					        if (isShown) {
 | 
				
			||||||
    const field = document.getElementById("accounting-title");
 | 
					            this.#element.classList.remove("d-none");
 | 
				
			||||||
    const error = document.getElementById("accounting-title-error");
 | 
					        } else {
 | 
				
			||||||
    field.value = field.value.trim();
 | 
					            this.#element.classList.add("d-none");
 | 
				
			||||||
    if (field.value === "") {
 | 
					        }
 | 
				
			||||||
        field.classList.add("is-invalid");
 | 
					    }
 | 
				
			||||||
        error.innerText = A_("Please fill in the title.");
 | 
					
 | 
				
			||||||
        return false;
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets whether the option is active.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isActive {boolean} true if active, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setActive(isActive) {
 | 
				
			||||||
 | 
					        if (isActive) {
 | 
				
			||||||
 | 
					            this.#element.classList.add("active");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#element.classList.remove("active");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    field.classList.remove("is-invalid");
 | 
					 | 
				
			||||||
    error.innerText = "";
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,12 +20,13 @@
 | 
				
			|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 * First written: 2023/2/2
 | 
					 * First written: 2023/2/2
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initializes the page JavaScript.
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", function () {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
    const list = document.getElementById("accounting-order-list");
 | 
					    const list = document.getElementById("accounting-order-list");
 | 
				
			||||||
    if (list !== null) {
 | 
					    if (list !== null) {
 | 
				
			||||||
        const onReorder = function () {
 | 
					        const onReorder = () => {
 | 
				
			||||||
            const accounts = Array.from(list.children);
 | 
					            const accounts = Array.from(list.children);
 | 
				
			||||||
            for (let i = 0; i < accounts.length; i++) {
 | 
					            for (let i = 0; i < accounts.length; i++) {
 | 
				
			||||||
                const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
 | 
					                const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,155 +20,155 @@
 | 
				
			|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 * First written: 2023/2/6
 | 
					 * First written: 2023/2/6
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initializes the page JavaScript.
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", function () {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
    document.getElementById("accounting-code")
 | 
					    CurrencyForm.initialize();
 | 
				
			||||||
        .onchange = validateCode;
 | 
					 | 
				
			||||||
    document.getElementById("accounting-name")
 | 
					 | 
				
			||||||
        .onchange = validateName;
 | 
					 | 
				
			||||||
    document.getElementById("accounting-form")
 | 
					 | 
				
			||||||
        .onsubmit = validateForm;
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The asynchronous validation result
 | 
					 * The currency form.
 | 
				
			||||||
 * @type {object}
 | 
					 | 
				
			||||||
 * @private
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
let isAsyncValid = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Validates the form.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					 | 
				
			||||||
 * @private
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function validateForm() {
 | 
					 | 
				
			||||||
    isAsyncValid = {
 | 
					 | 
				
			||||||
        "code": false,
 | 
					 | 
				
			||||||
        "_sync": false,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    let isValid = true;
 | 
					 | 
				
			||||||
    isValid = validateCode() && isValid;
 | 
					 | 
				
			||||||
    isValid = validateName() && isValid;
 | 
					 | 
				
			||||||
    isAsyncValid["_sync"] = isValid;
 | 
					 | 
				
			||||||
    submitFormIfAllAsyncValid();
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Submits the form if the whole form passed the asynchronous
 | 
					 | 
				
			||||||
 * validations.
 | 
					 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @private
 | 
					 * @private
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function submitFormIfAllAsyncValid() {
 | 
					class CurrencyForm {
 | 
				
			||||||
    let isValid = true;
 | 
					 | 
				
			||||||
    Object.keys(isAsyncValid).forEach(function (key) {
 | 
					 | 
				
			||||||
        isValid = isAsyncValid[key] && isValid;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (isValid) {
 | 
					 | 
				
			||||||
        document.getElementById("accounting-form").submit()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					    /**
 | 
				
			||||||
 * Validates the code.
 | 
					     * The form.
 | 
				
			||||||
 *
 | 
					     * @type {HTMLFormElement}
 | 
				
			||||||
 * @param changeEvent {Event} the change event, if invoked from onchange
 | 
					     */
 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					    #formElement;
 | 
				
			||||||
 * @private
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function validateCode(changeEvent = null) {
 | 
					 | 
				
			||||||
    const key = "code";
 | 
					 | 
				
			||||||
    const isSubmission = changeEvent === null;
 | 
					 | 
				
			||||||
    let hasAsyncValidation = false;
 | 
					 | 
				
			||||||
    const field = document.getElementById("accounting-code");
 | 
					 | 
				
			||||||
    const error = document.getElementById("accounting-code-error");
 | 
					 | 
				
			||||||
    field.value = field.value.trim();
 | 
					 | 
				
			||||||
    if (field.value === "") {
 | 
					 | 
				
			||||||
        field.classList.add("is-invalid");
 | 
					 | 
				
			||||||
        error.innerText = A_("Please fill in the code.");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const blocklist = JSON.parse(field.dataset.blocklist);
 | 
					 | 
				
			||||||
    if (blocklist.includes(field.value)) {
 | 
					 | 
				
			||||||
        field.classList.add("is-invalid");
 | 
					 | 
				
			||||||
        error.innerText = A_("This code is not available.");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!field.value.match(/^[A-Z]{3}$/)) {
 | 
					 | 
				
			||||||
        field.classList.add("is-invalid");
 | 
					 | 
				
			||||||
        error.innerText = A_("Code can only be composed of 3 upper-cased letters.");
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const original = field.dataset.original;
 | 
					 | 
				
			||||||
    if (original === "" || field.value !== original) {
 | 
					 | 
				
			||||||
        hasAsyncValidation = true;
 | 
					 | 
				
			||||||
        validateAsyncCodeIsDuplicated(isSubmission, key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!hasAsyncValidation) {
 | 
					 | 
				
			||||||
        isAsyncValid[key] = true;
 | 
					 | 
				
			||||||
        field.classList.remove("is-invalid");
 | 
					 | 
				
			||||||
        error.innerText = "";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					    /**
 | 
				
			||||||
 * Validates asynchronously whether the code is duplicated.
 | 
					     * The code
 | 
				
			||||||
 * The boolean validation result is stored in isAsyncValid[key].
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 *
 | 
					     */
 | 
				
			||||||
 * @param isSubmission {boolean} whether this is invoked from a form submission
 | 
					    #code;
 | 
				
			||||||
 * @param key {string} the key to store the result in isAsyncValid
 | 
					
 | 
				
			||||||
 * @private
 | 
					    /**
 | 
				
			||||||
 */
 | 
					     * The error message of the code
 | 
				
			||||||
function validateAsyncCodeIsDuplicated(isSubmission, key) {
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
    const field = document.getElementById("accounting-code");
 | 
					     */
 | 
				
			||||||
    const error = document.getElementById("accounting-code-error");
 | 
					    #codeError;
 | 
				
			||||||
    const url = field.dataset.existsUrl;
 | 
					
 | 
				
			||||||
    const onLoad = function () {
 | 
					    /**
 | 
				
			||||||
        if (this.status === 200) {
 | 
					     * The name
 | 
				
			||||||
            const result = JSON.parse(this.responseText);
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
            if (result["exists"]) {
 | 
					     */
 | 
				
			||||||
                field.classList.add("is-invalid");
 | 
					    #name;
 | 
				
			||||||
                error.innerText = A_("Code conflicts with another currency.");
 | 
					
 | 
				
			||||||
                if (isSubmission) {
 | 
					    /**
 | 
				
			||||||
                    isAsyncValid[key] = false;
 | 
					     * The error message of the name
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #nameError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the currency form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        this.#formElement = document.getElementById("accounting-form");
 | 
				
			||||||
 | 
					        this.#code = document.getElementById("accounting-code");
 | 
				
			||||||
 | 
					        this.#codeError = document.getElementById("accounting-code-error");
 | 
				
			||||||
 | 
					        this.#name = document.getElementById("accounting-name");
 | 
				
			||||||
 | 
					        this.#nameError = document.getElementById("accounting-name-error");
 | 
				
			||||||
 | 
					        this.#code.onchange = () => {
 | 
				
			||||||
 | 
					            this.#validateCode().then();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.#name.onchange = () => {
 | 
				
			||||||
 | 
					            this.#validateName();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.#formElement.onsubmit = () => {
 | 
				
			||||||
 | 
					            this.#validate().then((isValid) => {
 | 
				
			||||||
 | 
					                if (isValid) {
 | 
				
			||||||
 | 
					                    this.#formElement.submit();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return;
 | 
					            });
 | 
				
			||||||
            }
 | 
					            return false;
 | 
				
			||||||
            field.classList.remove("is-invalid");
 | 
					        };
 | 
				
			||||||
            error.innerText = "";
 | 
					    }
 | 
				
			||||||
            if (isSubmission) {
 | 
					
 | 
				
			||||||
                isAsyncValid[key] = true;
 | 
					    /**
 | 
				
			||||||
                submitFormIfAllAsyncValid();
 | 
					     * Validates the form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {Promise<boolean>} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async #validate() {
 | 
				
			||||||
 | 
					        let isValid = true;
 | 
				
			||||||
 | 
					        isValid = await this.#validateCode() && isValid;
 | 
				
			||||||
 | 
					        isValid = this.#validateName() && isValid;
 | 
				
			||||||
 | 
					        return isValid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the code.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param changeEvent {Event} the change event, if invoked from onchange
 | 
				
			||||||
 | 
					     * @returns {Promise<boolean>} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async #validateCode(changeEvent = null) {
 | 
				
			||||||
 | 
					        this.#code.value = this.#code.value.trim();
 | 
				
			||||||
 | 
					        if (this.#code.value === "") {
 | 
				
			||||||
 | 
					            this.#code.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#codeError.innerText = A_("Please fill in the code.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const blocklist = JSON.parse(this.#code.dataset.blocklist);
 | 
				
			||||||
 | 
					        if (blocklist.includes(this.#code.value)) {
 | 
				
			||||||
 | 
					            this.#code.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#codeError.innerText = A_("This code is not available.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!this.#code.value.match(/^[A-Z]{3}$/)) {
 | 
				
			||||||
 | 
					            this.#code.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#codeError.innerText = A_("Code can only be composed of 3 upper-cased letters.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const original = this.#code.dataset.original;
 | 
				
			||||||
 | 
					        if (original === "" || this.#code.value !== original) {
 | 
				
			||||||
 | 
					            const response = await fetch(this.#code.dataset.existsUrl + "?q=" + encodeURIComponent(this.#code.value));
 | 
				
			||||||
 | 
					            const data = await response.json();
 | 
				
			||||||
 | 
					            if (data["exists"]) {
 | 
				
			||||||
 | 
					                this.#code.classList.add("is-invalid");
 | 
				
			||||||
 | 
					                this.#codeError.innerText = A_("Code conflicts with another currency.");
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					        this.#code.classList.remove("is-invalid");
 | 
				
			||||||
    const request = new XMLHttpRequest();
 | 
					        this.#codeError.innerText = "";
 | 
				
			||||||
    request.onload = onLoad;
 | 
					        return true;
 | 
				
			||||||
    request.open("GET", url + "?q=" + encodeURIComponent(field.value));
 | 
					    }
 | 
				
			||||||
    request.send();
 | 
					
 | 
				
			||||||
}
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the name.
 | 
				
			||||||
/**
 | 
					     *
 | 
				
			||||||
 * Validates the name.
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 *
 | 
					     */
 | 
				
			||||||
 * @returns {boolean} true if valid, or false otherwise
 | 
					    #validateName() {
 | 
				
			||||||
 * @private
 | 
					        this.#name.value = this.#name.value.trim();
 | 
				
			||||||
 */
 | 
					        if (this.#name.value === "") {
 | 
				
			||||||
function validateName() {
 | 
					            this.#name.classList.add("is-invalid");
 | 
				
			||||||
    const field = document.getElementById("accounting-name");
 | 
					            this.#nameError.innerText = A_("Please fill in the name.");
 | 
				
			||||||
    const error = document.getElementById("accounting-name-error");
 | 
					            return false;
 | 
				
			||||||
    field.value = field.value.trim();
 | 
					        }
 | 
				
			||||||
    if (field.value === "") {
 | 
					        this.#name.classList.remove("is-invalid");
 | 
				
			||||||
        field.classList.add("is-invalid");
 | 
					        this.#nameError.innerText = "";
 | 
				
			||||||
        error.innerText = A_("Please fill in the name.");
 | 
					        return true;
 | 
				
			||||||
        return false;
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The form
 | 
				
			||||||
 | 
					     * @type {CurrencyForm}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static #form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Initializes the currency form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static initialize() {
 | 
				
			||||||
 | 
					        this.#form = new CurrencyForm();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    field.classList.remove("is-invalid");
 | 
					 | 
				
			||||||
    error.innerText = "";
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1194
									
								
								src/accounting/static/js/description-editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1194
									
								
								src/accounting/static/js/description-editor.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -20,6 +20,7 @@
 | 
				
			|||||||
/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 * First written: 2023/2/3
 | 
					 * First written: 2023/2/3
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Initializes the drag-and-drop reordering on a list.
 | 
					 * Initializes the drag-and-drop reordering on a list.
 | 
				
			||||||
@@ -42,21 +43,21 @@ function initializeDragAndDropReordering(list, onReorder) {
 | 
				
			|||||||
function initializeMouseDragAndDropReordering(list, onReorder) {
 | 
					function initializeMouseDragAndDropReordering(list, onReorder) {
 | 
				
			||||||
    const items = Array.from(list.children);
 | 
					    const items = Array.from(list.children);
 | 
				
			||||||
    let dragged = null;
 | 
					    let dragged = null;
 | 
				
			||||||
    items.forEach(function (item) {
 | 
					    for (const item of items) {
 | 
				
			||||||
        item.draggable = true;
 | 
					        item.draggable = true;
 | 
				
			||||||
        item.addEventListener("dragstart", function () {
 | 
					        item.addEventListener("dragstart", () => {
 | 
				
			||||||
            dragged = item;
 | 
					            dragged = item;
 | 
				
			||||||
            dragged.classList.add("list-group-item-dark");
 | 
					            dragged.classList.add("accounting-dragged");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        item.addEventListener("dragover", function () {
 | 
					        item.addEventListener("dragover", () => {
 | 
				
			||||||
            onDragOver(dragged, item);
 | 
					            onDragOver(dragged, item);
 | 
				
			||||||
            onReorder();
 | 
					            onReorder();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        item.addEventListener("dragend", function () {
 | 
					        item.addEventListener("dragend", () => {
 | 
				
			||||||
            dragged.classList.remove("list-group-item-dark");
 | 
					            dragged.classList.remove("accounting-dragged");
 | 
				
			||||||
            dragged = null;
 | 
					            dragged = null;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -68,20 +69,20 @@ function initializeMouseDragAndDropReordering(list, onReorder) {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
function initializeTouchDragAndDropReordering(list, onReorder) {
 | 
					function initializeTouchDragAndDropReordering(list, onReorder) {
 | 
				
			||||||
    const items = Array.from(list.children);
 | 
					    const items = Array.from(list.children);
 | 
				
			||||||
    items.forEach(function (item) {
 | 
					    for (const item of items) {
 | 
				
			||||||
        item.addEventListener("touchstart", function () {
 | 
					        item.addEventListener("touchstart", () => {
 | 
				
			||||||
            item.classList.add("list-group-item-dark");
 | 
					            item.classList.add("accounting-dragged");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        item.addEventListener("touchmove", function (event) {
 | 
					        item.addEventListener("touchmove", (event) => {
 | 
				
			||||||
            const touch = event.targetTouches[0];
 | 
					            const touch = event.targetTouches[0];
 | 
				
			||||||
            const target = document.elementFromPoint(touch.pageX, touch.pageY);
 | 
					            const target = document.elementFromPoint(touch.pageX, touch.pageY);
 | 
				
			||||||
            onDragOver(item, target);
 | 
					            onDragOver(item, target);
 | 
				
			||||||
            onReorder();
 | 
					            onReorder();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        item.addEventListener("touchend", function () {
 | 
					        item.addEventListener("touchend", () => {
 | 
				
			||||||
            item.classList.remove("list-group-item-dark");
 | 
					            item.classList.remove("accounting-dragged");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -91,7 +92,7 @@ function initializeTouchDragAndDropReordering(list, onReorder) {
 | 
				
			|||||||
 * @param target {Element} the other item that was dragged over
 | 
					 * @param target {Element} the other item that was dragged over
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function onDragOver(dragged, target) {
 | 
					function onDragOver(dragged, target) {
 | 
				
			||||||
    if (target.parentElement !== dragged.parentElement || target === dragged) {
 | 
					    if (dragged === null || target.parentElement !== dragged.parentElement || target === dragged) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let isBefore = false;
 | 
					    let isBefore = false;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										319
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								src/accounting/static/js/journal-entry-account-selector.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,319 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/2/28
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The account selector.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class JournalEntryAccountSelector {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The line item editor
 | 
				
			||||||
 | 
					     * @type {JournalEntryLineItemEditor}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Either "debit" or "credit"
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The button to clear the account
 | 
				
			||||||
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #clearButton
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The query input
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message when the query has no result
 | 
				
			||||||
 | 
					     * @type {HTMLParagraphElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryNoResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The option list
 | 
				
			||||||
 | 
					     * @type {HTMLUListElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #optionList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The options
 | 
				
			||||||
 | 
					     * @type {JournalEntryAccountOption[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The more item to show all accounts
 | 
				
			||||||
 | 
					     * @type {HTMLLIElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #more;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Whether to show all accounts
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isShowMore = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs an account selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
 | 
				
			||||||
 | 
					     * @param debitCredit {string} either "debit" or "credit"
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(lineItemEditor, debitCredit) {
 | 
				
			||||||
 | 
					        this.lineItemEditor = lineItemEditor
 | 
				
			||||||
 | 
					        this.#debitCredit = debitCredit;
 | 
				
			||||||
 | 
					        const prefix = "accounting-account-selector-" + debitCredit;
 | 
				
			||||||
 | 
					        this.#query = document.getElementById(prefix + "-query");
 | 
				
			||||||
 | 
					        this.#queryNoResult = document.getElementById(prefix + "-option-no-result");
 | 
				
			||||||
 | 
					        this.#optionList = document.getElementById(prefix + "-option-list");
 | 
				
			||||||
 | 
					        this.#options = Array.from(document.getElementsByClassName(prefix + "-option")).map((element) => new JournalEntryAccountOption(this, element));
 | 
				
			||||||
 | 
					        this.#more = document.getElementById(prefix + "-more");
 | 
				
			||||||
 | 
					        this.#clearButton = document.getElementById(prefix + "-btn-clear");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#more.onclick = () => {
 | 
				
			||||||
 | 
					            this.#isShowMore = true;
 | 
				
			||||||
 | 
					            this.#more.classList.add("d-none");
 | 
				
			||||||
 | 
					            this.#filterOptions();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.#query.oninput = () => this.#filterOptions();
 | 
				
			||||||
 | 
					        this.#clearButton.onclick = () => this.lineItemEditor.clearAccount();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Filters the options.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #filterOptions() {
 | 
				
			||||||
 | 
					        const codesInUse = this.#getCodesUsedInForm();
 | 
				
			||||||
 | 
					        let isAnyMatched = false;
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) {
 | 
				
			||||||
 | 
					                option.setShown(true);
 | 
				
			||||||
 | 
					                isAnyMatched = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                option.setShown(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!isAnyMatched) {
 | 
				
			||||||
 | 
					            this.#optionList.classList.add("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.remove("d-none");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#optionList.classList.remove("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the account codes that are used in the form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string[]} the account codes that are used in the form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #getCodesUsedInForm() {
 | 
				
			||||||
 | 
					        const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
 | 
				
			||||||
 | 
					        if (this.lineItemEditor.accountCode !== null) {
 | 
				
			||||||
 | 
					            inUse.push(this.lineItemEditor.accountCode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return inUse
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The callback when the account selector is shown.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onOpen() {
 | 
				
			||||||
 | 
					        this.#query.value = "";
 | 
				
			||||||
 | 
					        this.#isShowMore = false;
 | 
				
			||||||
 | 
					        this.#more.classList.remove("d-none");
 | 
				
			||||||
 | 
					        this.#filterOptions();
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            option.setActive(option.code === this.lineItemEditor.accountCode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.lineItemEditor.accountCode === null) {
 | 
				
			||||||
 | 
					            this.#clearButton.classList.add("btn-secondary");
 | 
				
			||||||
 | 
					            this.#clearButton.classList.remove("btn-danger");
 | 
				
			||||||
 | 
					            this.#clearButton.disabled = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#clearButton.classList.add("btn-danger");
 | 
				
			||||||
 | 
					            this.#clearButton.classList.remove("btn-secondary");
 | 
				
			||||||
 | 
					            this.#clearButton.disabled = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the account selector instances.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
 | 
				
			||||||
 | 
					     * @return {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static getInstances(lineItemEditor) {
 | 
				
			||||||
 | 
					        const selectors = {}
 | 
				
			||||||
 | 
					        const modals = Array.from(document.getElementsByClassName("accounting-account-selector"));
 | 
				
			||||||
 | 
					        for (const modal of modals) {
 | 
				
			||||||
 | 
					            selectors[modal.dataset.debitCredit] = new JournalEntryAccountSelector(lineItemEditor, modal.dataset.debitCredit);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return selectors;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * An account option
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class JournalEntryAccountOption {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account selector
 | 
				
			||||||
 | 
					     * @type {JournalEntryAccountSelector}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #selector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The element
 | 
				
			||||||
 | 
					     * @type {HTMLLIElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account code
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account text
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Whether the account is in use
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isInUse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Whether line items in the account need offset
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isNeedOffset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The values to query against
 | 
				
			||||||
 | 
					     * @type {string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryValues;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the account in the account selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param selector {JournalEntryAccountSelector} the account selector
 | 
				
			||||||
 | 
					     * @param element {HTMLLIElement} the element
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(selector, element) {
 | 
				
			||||||
 | 
					        this.#selector = selector;
 | 
				
			||||||
 | 
					        this.#element = element;
 | 
				
			||||||
 | 
					        this.code = element.dataset.code;
 | 
				
			||||||
 | 
					        this.text = element.dataset.text;
 | 
				
			||||||
 | 
					        this.#isInUse = element.classList.contains("accounting-account-is-in-use");
 | 
				
			||||||
 | 
					        this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
 | 
				
			||||||
 | 
					        this.#queryValues = JSON.parse(element.dataset.queryValues);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#element.onclick = () => this.#selector.lineItemEditor.saveAccount(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the account matches the query.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isShowMore {boolean} true to show all accounts, or false to show only those in use
 | 
				
			||||||
 | 
					     * @param codesInUse {string[]} the account codes that are used in the form
 | 
				
			||||||
 | 
					     * @param query {string} the query term
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isMatched(isShowMore, codesInUse, query) {
 | 
				
			||||||
 | 
					        return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the account matches the "in-use" condition.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isShowMore {boolean} true to show all accounts, or false to show only those in use
 | 
				
			||||||
 | 
					     * @param codesInUse {string[]} the account codes that are used in the form
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isInUseMatched(isShowMore, codesInUse) {
 | 
				
			||||||
 | 
					        return isShowMore || this.#isInUse || codesInUse.includes(this.code);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the account matches the query term.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param query {string} the query term
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isQueryMatched(query) {
 | 
				
			||||||
 | 
					        if (query === "") {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const queryValue of this.#queryValues) {
 | 
				
			||||||
 | 
					            if (queryValue.toLowerCase().includes(query.toLowerCase())) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets whether the option is shown.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isShown {boolean} true to show, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setShown(isShown) {
 | 
				
			||||||
 | 
					        if (isShown) {
 | 
				
			||||||
 | 
					            this.#element.classList.remove("d-none");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#element.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets whether the option is active.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isActive {boolean} true if active, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setActive(isActive) {
 | 
				
			||||||
 | 
					        if (isActive) {
 | 
				
			||||||
 | 
					            this.#element.classList.add("active");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#element.classList.remove("active");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1121
									
								
								src/accounting/static/js/journal-entry-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1121
									
								
								src/accounting/static/js/journal-entry-form.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										598
									
								
								src/accounting/static/js/journal-entry-line-item-editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										598
									
								
								src/accounting/static/js/journal-entry-line-item-editor.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,598 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * journal-entry-line-item-editor.js: The JavaScript for the journal entry line item editor
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/2/25
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The journal entry line item editor.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class JournalEntryLineItemEditor {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The journal entry form
 | 
				
			||||||
 | 
					     * @type {JournalEntryForm}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The journal entry line item editor
 | 
				
			||||||
 | 
					     * @type {HTMLFormElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The bootstrap modal
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Either "debit" or "credit"
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #prefix = "accounting-line-item-editor"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The container of the original line item
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #originalLineItemContainer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The control of the original line item
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #originalLineItemControl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The original line item
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #originalLineItemText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message of the original line item
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #originalLineItemError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The delete button of the original line item
 | 
				
			||||||
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #originalLineItemDelete;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The control of the description
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #descriptionControl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The description
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #descriptionText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message of the description
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #descriptionError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The control of the account
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #accountControl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #accountText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message of the account
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #accountError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The amount
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #amountInput;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message of the amount
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #amountError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The journal entry line item to edit
 | 
				
			||||||
 | 
					     * @type {LineItemSubForm|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    lineItem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The debit or credit sub-form
 | 
				
			||||||
 | 
					     * @type {DebitCreditSubForm}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #debitCreditSubForm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Whether the journal entry line item needs offset
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isNeedOffset = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The ID of the original line item
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    originalLineItemId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The date of the original line item
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    originalLineItemDate = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The text of the original line item
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    originalLineItemText = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account code
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    accountCode = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account text
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    accountText = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The description
 | 
				
			||||||
 | 
					     * @type {string|null}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    description = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The description editors
 | 
				
			||||||
 | 
					     * @type {{debit: DescriptionEditor, credit: DescriptionEditor}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #descriptionEditors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account selectors
 | 
				
			||||||
 | 
					     * @type {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #accountSelectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The original line item selector
 | 
				
			||||||
 | 
					     * @type {OriginalLineItemSelector}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    originalLineItemSelector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs a new journal entry line item editor.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param form {JournalEntryForm} the journal entry form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(form) {
 | 
				
			||||||
 | 
					        this.form = form;
 | 
				
			||||||
 | 
					        this.#element = document.getElementById(this.#prefix);
 | 
				
			||||||
 | 
					        this.modal = document.getElementById(this.#prefix + "-modal");
 | 
				
			||||||
 | 
					        this.#originalLineItemContainer = document.getElementById(this.#prefix + "-original-line-item-container");
 | 
				
			||||||
 | 
					        this.#originalLineItemControl = document.getElementById(this.#prefix + "-original-line-item-control");
 | 
				
			||||||
 | 
					        this.#originalLineItemText = document.getElementById(this.#prefix + "-original-line-item");
 | 
				
			||||||
 | 
					        this.#originalLineItemError = document.getElementById(this.#prefix + "-original-line-item-error");
 | 
				
			||||||
 | 
					        this.#originalLineItemDelete = document.getElementById(this.#prefix + "-original-line-item-delete");
 | 
				
			||||||
 | 
					        this.#descriptionControl = document.getElementById(this.#prefix + "-description-control");
 | 
				
			||||||
 | 
					        this.#descriptionText = document.getElementById(this.#prefix + "-description");
 | 
				
			||||||
 | 
					        this.#descriptionError = document.getElementById(this.#prefix + "-description-error");
 | 
				
			||||||
 | 
					        this.#accountControl = document.getElementById(this.#prefix + "-account-control");
 | 
				
			||||||
 | 
					        this.#accountText = document.getElementById(this.#prefix + "-account");
 | 
				
			||||||
 | 
					        this.#accountError = document.getElementById(this.#prefix + "-account-error")
 | 
				
			||||||
 | 
					        this.#amountInput = document.getElementById(this.#prefix + "-amount");
 | 
				
			||||||
 | 
					        this.#amountError = document.getElementById(this.#prefix + "-amount-error");
 | 
				
			||||||
 | 
					        this.#descriptionEditors = DescriptionEditor.getInstances(this);
 | 
				
			||||||
 | 
					        this.#accountSelectors = JournalEntryAccountSelector.getInstances(this);
 | 
				
			||||||
 | 
					        this.originalLineItemSelector = new OriginalLineItemSelector(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.onclick = () => this.originalLineItemSelector.onOpen()
 | 
				
			||||||
 | 
					        this.#originalLineItemDelete.onclick = () => this.clearOriginalLineItem();
 | 
				
			||||||
 | 
					        this.#descriptionControl.onclick = () => this.#descriptionEditors[this.debitCredit].onOpen();
 | 
				
			||||||
 | 
					        this.#accountControl.onclick = () => this.#accountSelectors[this.debitCredit].onOpen();
 | 
				
			||||||
 | 
					        this.#amountInput.onchange = () => this.#validateAmount();
 | 
				
			||||||
 | 
					        this.#element.onsubmit = () => {
 | 
				
			||||||
 | 
					            if (this.#validate()) {
 | 
				
			||||||
 | 
					                if (this.lineItem === null) {
 | 
				
			||||||
 | 
					                    this.lineItem = this.#debitCreditSubForm.addLineItem();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.lineItem.save(this);
 | 
				
			||||||
 | 
					                bootstrap.Modal.getInstance(this.modal).hide();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        this.modal.addEventListener("hidden.bs.modal", () => this.#debitCreditSubForm.onLineItemEditorClosed());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the amount.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string} the amount
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get amount() {
 | 
				
			||||||
 | 
					        return this.#amountInput.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the currency code.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string} the currency code
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    get currencyCode() {
 | 
				
			||||||
 | 
					        return this.#debitCreditSubForm.currency.currencyCode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Saves the original line item from the original line item selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param originalLineItem {OriginalLineItem} the original line item
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    saveOriginalLineItem(originalLineItem) {
 | 
				
			||||||
 | 
					        this.isNeedOffset = false;
 | 
				
			||||||
 | 
					        this.#originalLineItemContainer.classList.remove("d-none");
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.originalLineItemId = originalLineItem.id;
 | 
				
			||||||
 | 
					        this.originalLineItemDate = originalLineItem.date;
 | 
				
			||||||
 | 
					        this.originalLineItemText = originalLineItem.text;
 | 
				
			||||||
 | 
					        this.#originalLineItemText.innerText = originalLineItem.text;
 | 
				
			||||||
 | 
					        this.#setEnableDescriptionAccount(false);
 | 
				
			||||||
 | 
					        if (originalLineItem.description === "") {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.description = originalLineItem.description === ""? null: originalLineItem.description;
 | 
				
			||||||
 | 
					        this.#descriptionText.innerText = originalLineItem.description;
 | 
				
			||||||
 | 
					        this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.accountCode = originalLineItem.accountCode;
 | 
				
			||||||
 | 
					        this.accountText = originalLineItem.accountText;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = originalLineItem.accountText;
 | 
				
			||||||
 | 
					        this.#amountInput.value = String(originalLineItem.netBalance);
 | 
				
			||||||
 | 
					        this.#amountInput.max = String(originalLineItem.netBalance);
 | 
				
			||||||
 | 
					        this.#amountInput.min = "0";
 | 
				
			||||||
 | 
					        this.#validate();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clears the original line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    clearOriginalLineItem() {
 | 
				
			||||||
 | 
					        this.isNeedOffset = false;
 | 
				
			||||||
 | 
					        this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.originalLineItemId = null;
 | 
				
			||||||
 | 
					        this.originalLineItemDate = null;
 | 
				
			||||||
 | 
					        this.originalLineItemText = null;
 | 
				
			||||||
 | 
					        this.#originalLineItemText.innerText = "";
 | 
				
			||||||
 | 
					        this.#setEnableDescriptionAccount(true);
 | 
				
			||||||
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.accountCode = null;
 | 
				
			||||||
 | 
					        this.accountText = null;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
 | 
					        this.#amountInput.max = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Saves the description from the description editor.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param description {string} the description
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    saveDescription(description) {
 | 
				
			||||||
 | 
					        if (description === "") {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.description = description === ""? null: description;
 | 
				
			||||||
 | 
					        this.#descriptionText.innerText = description;
 | 
				
			||||||
 | 
					        this.#validateDescription();
 | 
				
			||||||
 | 
					        bootstrap.Modal.getOrCreateInstance(this.modal).show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Saves the description with the suggested account from the description editor.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param description {string} the description
 | 
				
			||||||
 | 
					     * @param accountCode {string} the account code
 | 
				
			||||||
 | 
					     * @param accountText {string} the account text
 | 
				
			||||||
 | 
					     * @param isAccountNeedOffset {boolean} true if the line items in the account need offset, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    saveDescriptionWithAccount(description, accountCode, accountText, isAccountNeedOffset) {
 | 
				
			||||||
 | 
					        this.isNeedOffset = isAccountNeedOffset;
 | 
				
			||||||
 | 
					        this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.accountCode = accountCode;
 | 
				
			||||||
 | 
					        this.accountText = accountText;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = accountText;
 | 
				
			||||||
 | 
					        this.#validateAccount();
 | 
				
			||||||
 | 
					        this.saveDescription(description)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Clears the account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    clearAccount() {
 | 
				
			||||||
 | 
					        this.isNeedOffset = false;
 | 
				
			||||||
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.accountCode = null;
 | 
				
			||||||
 | 
					        this.accountText = null;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
 | 
					        this.#validateAccount();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Saves the selected account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param account {JournalEntryAccountOption} the selected account
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    saveAccount(account) {
 | 
				
			||||||
 | 
					        this.isNeedOffset = account.isNeedOffset;
 | 
				
			||||||
 | 
					        this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.accountCode = account.code;
 | 
				
			||||||
 | 
					        this.accountText = account.text;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = account.text;
 | 
				
			||||||
 | 
					        this.#validateAccount();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validate() {
 | 
				
			||||||
 | 
					        let isValid = true;
 | 
				
			||||||
 | 
					        isValid = this.#validateOriginalLineItem() && isValid;
 | 
				
			||||||
 | 
					        isValid = this.#validateDescription() && isValid;
 | 
				
			||||||
 | 
					        isValid = this.#validateAccount() && isValid;
 | 
				
			||||||
 | 
					        isValid = this.#validateAmount() && isValid
 | 
				
			||||||
 | 
					        return isValid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the original line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateOriginalLineItem() {
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#originalLineItemError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the description.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateDescription() {
 | 
				
			||||||
 | 
					        this.#descriptionText.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#descriptionError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateAccount() {
 | 
				
			||||||
 | 
					        if (this.accountCode === null) {
 | 
				
			||||||
 | 
					            this.#accountControl.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#accountError.innerText = A_("Please select the account.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#accountControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#accountError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the amount.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateAmount() {
 | 
				
			||||||
 | 
					        this.#amountInput.value = this.#amountInput.value.trim();
 | 
				
			||||||
 | 
					        this.#amountInput.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        if (this.#amountInput.value === "") {
 | 
				
			||||||
 | 
					            this.#amountInput.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#amountError.innerText = A_("Please fill in the amount.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const amount =new Decimal(this.#amountInput.value);
 | 
				
			||||||
 | 
					        if (amount.lessThanOrEqualTo(0)) {
 | 
				
			||||||
 | 
					            this.#amountInput.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#amountError.innerText = A_("Please fill in a positive amount.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#amountInput.max !== "") {
 | 
				
			||||||
 | 
					            if (amount.greaterThan(new Decimal(this.#amountInput.max))) {
 | 
				
			||||||
 | 
					                this.#amountInput.classList.add("is-invalid");
 | 
				
			||||||
 | 
					                this.#amountError.innerText = A_("The amount must not exceed the net balance %(balance)s of the original line item.", {balance: new Decimal(this.#amountInput.max)});
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#amountInput.min !== "") {
 | 
				
			||||||
 | 
					            const min = new Decimal(this.#amountInput.min);
 | 
				
			||||||
 | 
					            if (amount.lessThan(min)) {
 | 
				
			||||||
 | 
					                this.#amountInput.classList.add("is-invalid");
 | 
				
			||||||
 | 
					                this.#amountError.innerText = A_("The amount must not be less than the offset total %(total)s.", {total: formatDecimal(min)});
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#amountInput.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#amountError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The callback when adding a new journal entry line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param debitCredit {DebitCreditSubForm} the debit or credit sub-form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onAddNew(debitCredit) {
 | 
				
			||||||
 | 
					        this.lineItem = null;
 | 
				
			||||||
 | 
					        this.#debitCreditSubForm = debitCredit;
 | 
				
			||||||
 | 
					        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
				
			||||||
 | 
					        this.isNeedOffset = false;
 | 
				
			||||||
 | 
					        this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.#originalLineItemControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.originalLineItemId = null;
 | 
				
			||||||
 | 
					        this.originalLineItemDate = null;
 | 
				
			||||||
 | 
					        this.originalLineItemText = null;
 | 
				
			||||||
 | 
					        this.#originalLineItemText.innerText = "";
 | 
				
			||||||
 | 
					        this.#setEnableDescriptionAccount(true);
 | 
				
			||||||
 | 
					        this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.#descriptionControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.description = null;
 | 
				
			||||||
 | 
					        this.#descriptionText.innerText = ""
 | 
				
			||||||
 | 
					        this.#descriptionError.innerText = ""
 | 
				
			||||||
 | 
					        this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        this.#accountControl.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.accountCode = null;
 | 
				
			||||||
 | 
					        this.accountText = null;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = "";
 | 
				
			||||||
 | 
					        this.#accountError.innerText = "";
 | 
				
			||||||
 | 
					        this.#amountInput.value = "";
 | 
				
			||||||
 | 
					        this.#amountInput.max = "";
 | 
				
			||||||
 | 
					        this.#amountInput.min = "0";
 | 
				
			||||||
 | 
					        this.#amountInput.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#amountError.innerText = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The callback when editing a journal entry line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param lineItem {LineItemSubForm} the journal entry line item sub-form
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onEdit(lineItem) {
 | 
				
			||||||
 | 
					        this.lineItem = lineItem;
 | 
				
			||||||
 | 
					        this.#debitCreditSubForm = lineItem.debitCreditSubForm;
 | 
				
			||||||
 | 
					        this.debitCredit = this.#debitCreditSubForm.debitCredit;
 | 
				
			||||||
 | 
					        this.isNeedOffset = lineItem.isNeedOffset;
 | 
				
			||||||
 | 
					        this.originalLineItemId = lineItem.originalLineItemId;
 | 
				
			||||||
 | 
					        this.originalLineItemDate = lineItem.originalLineItemDate;
 | 
				
			||||||
 | 
					        this.originalLineItemText = lineItem.originalLineItemText;
 | 
				
			||||||
 | 
					        this.#originalLineItemText.innerText = this.originalLineItemText;
 | 
				
			||||||
 | 
					        if (this.originalLineItemId === null) {
 | 
				
			||||||
 | 
					            this.#originalLineItemContainer.classList.add("d-none");
 | 
				
			||||||
 | 
					            this.#originalLineItemControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#originalLineItemContainer.classList.remove("d-none");
 | 
				
			||||||
 | 
					            this.#originalLineItemControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#setEnableDescriptionAccount(!lineItem.isMatched && this.originalLineItemId === null);
 | 
				
			||||||
 | 
					        this.description = lineItem.description;
 | 
				
			||||||
 | 
					        if (this.description === null) {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#descriptionText.innerText = this.description === null? "": this.description;
 | 
				
			||||||
 | 
					        if (lineItem.accountCode === null) {
 | 
				
			||||||
 | 
					            this.#accountControl.classList.remove("accounting-not-empty");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#accountControl.classList.add("accounting-not-empty");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.accountCode = lineItem.accountCode;
 | 
				
			||||||
 | 
					        this.accountText = lineItem.accountText;
 | 
				
			||||||
 | 
					        this.#accountText.innerText = this.accountText;
 | 
				
			||||||
 | 
					        this.#amountInput.value = lineItem.amount === null? "": String(lineItem.amount);
 | 
				
			||||||
 | 
					        const maxAmount = this.#getMaxAmount();
 | 
				
			||||||
 | 
					        this.#amountInput.max = maxAmount === null? "": maxAmount;
 | 
				
			||||||
 | 
					        this.#amountInput.min = lineItem.amountMin === null? "": String(lineItem.amountMin);
 | 
				
			||||||
 | 
					        this.#validate();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Finds out the max amount.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {Decimal|null} the max amount
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #getMaxAmount() {
 | 
				
			||||||
 | 
					        if (this.originalLineItemId === null) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this.originalLineItemSelector.getNetBalance(this.lineItem, this.form, this.originalLineItemId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets the enable status of the description and account.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isEnabled {boolean} true to enable, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #setEnableDescriptionAccount(isEnabled) {
 | 
				
			||||||
 | 
					        if (isEnabled) {
 | 
				
			||||||
 | 
					            this.#descriptionControl.dataset.bsToggle = "modal";
 | 
				
			||||||
 | 
					            this.#descriptionControl.dataset.bsTarget = "#accounting-description-editor-" + this.#debitCreditSubForm.debitCredit + "-modal";
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.remove("accounting-disabled");
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.add("accounting-clickable");
 | 
				
			||||||
 | 
					            this.#accountControl.dataset.bsToggle = "modal";
 | 
				
			||||||
 | 
					            this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#debitCreditSubForm.debitCredit + "-modal";
 | 
				
			||||||
 | 
					            this.#accountControl.classList.remove("accounting-disabled");
 | 
				
			||||||
 | 
					            this.#accountControl.classList.add("accounting-clickable");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#descriptionControl.dataset.bsToggle = "";
 | 
				
			||||||
 | 
					            this.#descriptionControl.dataset.bsTarget = "";
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.add("accounting-disabled");
 | 
				
			||||||
 | 
					            this.#descriptionControl.classList.remove("accounting-clickable");
 | 
				
			||||||
 | 
					            this.#accountControl.dataset.bsToggle = "";
 | 
				
			||||||
 | 
					            this.#accountControl.dataset.bsTarget = "";
 | 
				
			||||||
 | 
					            this.#accountControl.classList.add("accounting-disabled");
 | 
				
			||||||
 | 
					            this.#accountControl.classList.remove("accounting-clickable");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/accounting/static/js/journal-entry-order.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/accounting/static/js/journal-entry-order.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * journal-entry-order.js: The JavaScript for the journal entry order
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/2/26
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
 | 
					    const list = document.getElementById("accounting-order-list");
 | 
				
			||||||
 | 
					    if (list !== null) {
 | 
				
			||||||
 | 
					        const onReorder = () => {
 | 
				
			||||||
 | 
					            const accounts = Array.from(list.children);
 | 
				
			||||||
 | 
					            for (let i = 0; i < accounts.length; i++) {
 | 
				
			||||||
 | 
					                const no = document.getElementById("accounting-order-" + accounts[i].dataset.id + "-no");
 | 
				
			||||||
 | 
					                no.value = String(i + 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        initializeDragAndDropReordering(list, onReorder);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/accounting/static/js/material-fab-speed-dial.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/accounting/static/js/material-fab-speed-dial.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * material-fab-speed-dial.js: The JavaScript for the speed dial for the material floating buttons
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/2/25
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
 | 
					    initializeMaterialFabSpeedDial();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Initializes the speed dial of the material floating buttons.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function initializeMaterialFabSpeedDial() {
 | 
				
			||||||
 | 
					    const btnFab = document.getElementById("accounting-btn-material-fab-speed-dial");
 | 
				
			||||||
 | 
					    const fab = document.getElementById(btnFab.dataset.target);
 | 
				
			||||||
 | 
					    btnFab.onclick = () => {
 | 
				
			||||||
 | 
					        if (fab.classList.contains("show")) {
 | 
				
			||||||
 | 
					            fab.classList.remove("show");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            fab.classList.add("show");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1036
									
								
								src/accounting/static/js/option-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1036
									
								
								src/accounting/static/js/option-form.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										416
									
								
								src/accounting/static/js/original-line-item-selector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								src/accounting/static/js/original-line-item-selector.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,416 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * original-line-item-selector.js: The JavaScript for the original line item selector
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/3/10
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The original line item selector.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class OriginalLineItemSelector {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The line item editor
 | 
				
			||||||
 | 
					     * @type {JournalEntryLineItemEditor}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    lineItemEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #prefix = "accounting-original-line-item-selector";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The query input
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error message when the query has no result
 | 
				
			||||||
 | 
					     * @type {HTMLParagraphElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryNoResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The option list
 | 
				
			||||||
 | 
					     * @type {HTMLUListElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #optionList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The options
 | 
				
			||||||
 | 
					     * @type {OriginalLineItem[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The options by their ID
 | 
				
			||||||
 | 
					     * @type {Object.<string, OriginalLineItem>}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #optionById;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The currency code
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #currencyCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Either "credit" or "debit"
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs an original line item selector.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(lineItemEditor) {
 | 
				
			||||||
 | 
					        this.lineItemEditor = lineItemEditor;
 | 
				
			||||||
 | 
					        this.#query = document.getElementById(this.#prefix + "-query");
 | 
				
			||||||
 | 
					        this.#queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
 | 
				
			||||||
 | 
					        this.#optionList = document.getElementById(this.#prefix + "-option-list");
 | 
				
			||||||
 | 
					        this.#options = Array.from(document.getElementsByClassName(this.#prefix + "-option")).map((element) => new OriginalLineItem(this, element));
 | 
				
			||||||
 | 
					        this.#optionById = {};
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            this.#optionById[option.id] = option;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#query.oninput = () => this.#filterOptions();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the net balance for an original line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param currentLineItem {LineItemSubForm} the line item sub-form that is currently editing
 | 
				
			||||||
 | 
					     * @param form {JournalEntryForm} the journal entry form
 | 
				
			||||||
 | 
					     * @param originalLineItemId {string} the ID of the original line item
 | 
				
			||||||
 | 
					     * @return {Decimal} the net balance of the original line item
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    getNetBalance(currentLineItem, form, originalLineItemId) {
 | 
				
			||||||
 | 
					        const otherLineItems = form.getLineItems().filter((lineItem) => lineItem !== currentLineItem);
 | 
				
			||||||
 | 
					        let otherOffset = new Decimal(0);
 | 
				
			||||||
 | 
					        for (const otherLineItem of otherLineItems) {
 | 
				
			||||||
 | 
					            if (otherLineItem.originalLineItemId === originalLineItemId) {
 | 
				
			||||||
 | 
					                const amount = otherLineItem.amount;
 | 
				
			||||||
 | 
					                if (amount !== null) {
 | 
				
			||||||
 | 
					                    otherOffset = otherOffset.plus(amount);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this.#optionById[originalLineItemId].bareNetBalance.minus(otherOffset);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Updates the net balances, subtracting the offset amounts on the form but the currently editing line item
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #updateNetBalances() {
 | 
				
			||||||
 | 
					        const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
 | 
				
			||||||
 | 
					        const otherOffsets = {}
 | 
				
			||||||
 | 
					        for (const otherLineItem of otherLineItems) {
 | 
				
			||||||
 | 
					            const otherOriginalLineItemId = otherLineItem.originalLineItemId;
 | 
				
			||||||
 | 
					            const amount = otherLineItem.amount;
 | 
				
			||||||
 | 
					            if (otherOriginalLineItemId === null || amount === null) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!(otherOriginalLineItemId in otherOffsets)) {
 | 
				
			||||||
 | 
					                otherOffsets[otherOriginalLineItemId] = new Decimal("0");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            if (option.id in otherOffsets) {
 | 
				
			||||||
 | 
					                option.updateNetBalance(otherOffsets[option.id]);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                option.resetNetBalance();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Filters the options.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #filterOptions() {
 | 
				
			||||||
 | 
					        let isAnyMatched = false;
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
 | 
				
			||||||
 | 
					                option.setShown(true);
 | 
				
			||||||
 | 
					                isAnyMatched = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                option.setShown(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!isAnyMatched) {
 | 
				
			||||||
 | 
					            this.#optionList.classList.add("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.remove("d-none");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#optionList.classList.remove("d-none");
 | 
				
			||||||
 | 
					            this.#queryNoResult.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The callback when the original line item selector is shown.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    onOpen() {
 | 
				
			||||||
 | 
					        this.#currencyCode = this.lineItemEditor.currencyCode;
 | 
				
			||||||
 | 
					        this.#debitCredit = this.lineItemEditor.debitCredit;
 | 
				
			||||||
 | 
					        for (const option of this.#options) {
 | 
				
			||||||
 | 
					            option.setActive(option.id === this.lineItemEditor.originalLineItemId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#query.value = "";
 | 
				
			||||||
 | 
					        this.#updateNetBalances();
 | 
				
			||||||
 | 
					        this.#filterOptions();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * An original line item.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class OriginalLineItem {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The original line item selector
 | 
				
			||||||
 | 
					     * @type {OriginalLineItemSelector}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #selector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The element
 | 
				
			||||||
 | 
					     * @type {HTMLLIElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #element;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The ID
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The date
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Either "debit" or "credit"
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #debitCredit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The currency code
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #currencyCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account code
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    accountCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The account text
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    accountText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The description
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    description;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The net balance, without the offset amounts on the form
 | 
				
			||||||
 | 
					     * @type {Decimal}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    bareNetBalance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The net balance
 | 
				
			||||||
 | 
					     * @type {Decimal}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    netBalance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The text of the net balance
 | 
				
			||||||
 | 
					     * @type {HTMLSpanElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    netBalanceText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The text representation of the original line item
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The values to query against
 | 
				
			||||||
 | 
					     * @type {string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #queryValues;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs an original line item.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param selector {OriginalLineItemSelector} the original line item selector
 | 
				
			||||||
 | 
					     * @param element {HTMLLIElement} the element
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(selector, element) {
 | 
				
			||||||
 | 
					        this.#selector = selector;
 | 
				
			||||||
 | 
					        this.#element = element;
 | 
				
			||||||
 | 
					        this.id = element.dataset.id;
 | 
				
			||||||
 | 
					        this.date = element.dataset.date;
 | 
				
			||||||
 | 
					        this.#debitCredit = element.dataset.debitCredit;
 | 
				
			||||||
 | 
					        this.#currencyCode = element.dataset.currencyCode;
 | 
				
			||||||
 | 
					        this.accountCode = element.dataset.accountCode;
 | 
				
			||||||
 | 
					        this.accountText = element.dataset.accountText;
 | 
				
			||||||
 | 
					        this.description = element.dataset.description;
 | 
				
			||||||
 | 
					        this.bareNetBalance = new Decimal(element.dataset.netBalance);
 | 
				
			||||||
 | 
					        this.netBalance = this.bareNetBalance;
 | 
				
			||||||
 | 
					        this.netBalanceText = document.getElementById("accounting-original-line-item-selector-option-" + this.id + "-net-balance");
 | 
				
			||||||
 | 
					        this.text = element.dataset.text;
 | 
				
			||||||
 | 
					        this.#queryValues = JSON.parse(element.dataset.queryValues);
 | 
				
			||||||
 | 
					        this.#element.onclick = () => this.#selector.lineItemEditor.saveOriginalLineItem(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Resets the net balance to its initial value, without the offset amounts on the form.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    resetNetBalance() {
 | 
				
			||||||
 | 
					        if (this.netBalance !== this.bareNetBalance) {
 | 
				
			||||||
 | 
					            this.netBalance = this.bareNetBalance;
 | 
				
			||||||
 | 
					            this.#updateNetBalanceText();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Updates the net balance with an offset.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param offset {Decimal} the offset to be added to the net balance
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    updateNetBalance(offset) {
 | 
				
			||||||
 | 
					        this.netBalance = this.bareNetBalance.minus(offset);
 | 
				
			||||||
 | 
					        this.#updateNetBalanceText();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Updates the text display of the net balance.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #updateNetBalanceText() {
 | 
				
			||||||
 | 
					        this.netBalanceText.innerText = formatDecimal(this.netBalance);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the original matches.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param debitCredit {string} either "debit" or "credit"
 | 
				
			||||||
 | 
					     * @param currencyCode {string} the currency code
 | 
				
			||||||
 | 
					     * @param query {string|null} the query term
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    isMatched(debitCredit, currencyCode, query = null) {
 | 
				
			||||||
 | 
					        return this.netBalance.greaterThan(0)
 | 
				
			||||||
 | 
					            && this.date <= this.#selector.lineItemEditor.form.date
 | 
				
			||||||
 | 
					            && this.#isDebitCreditMatched(debitCredit)
 | 
				
			||||||
 | 
					            && this.#currencyCode === currencyCode
 | 
				
			||||||
 | 
					            && this.#isQueryMatched(query);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the original line item matches the debit or credit.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param debitCredit {string} either "debit" or credit
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isDebitCreditMatched(debitCredit) {
 | 
				
			||||||
 | 
					        return (debitCredit === "debit" && this.#debitCredit === "credit")
 | 
				
			||||||
 | 
					            || (debitCredit === "credit" && this.#debitCredit === "debit");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns whether the original line item matches the query term.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param query {string|null} the query term
 | 
				
			||||||
 | 
					     * @return {boolean} true if the option matches, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #isQueryMatched(query) {
 | 
				
			||||||
 | 
					        if (query === "") {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#getNetBalanceForQuery().includes(query.toLowerCase())) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        for (const queryValue of this.#queryValues) {
 | 
				
			||||||
 | 
					            if (queryValue.toLowerCase().includes(query.toLowerCase())) {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Returns the net balance in the format for query match.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string} the net balance in the format for query match
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #getNetBalanceForQuery() {
 | 
				
			||||||
 | 
					        const frac = this.netBalance.modulo(1);
 | 
				
			||||||
 | 
					        const whole = Number(this.netBalance.minus(frac));
 | 
				
			||||||
 | 
					        return String(whole) + String(frac).substring(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets whether the option is shown.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isShown {boolean} true to show, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setShown(isShown) {
 | 
				
			||||||
 | 
					        if (isShown) {
 | 
				
			||||||
 | 
					            this.#element.classList.remove("d-none");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#element.classList.add("d-none");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Sets whether the option is active.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param isActive {boolean} true if active, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    setActive(isActive) {
 | 
				
			||||||
 | 
					        if (isActive) {
 | 
				
			||||||
 | 
					            this.#element.classList.add("active");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.#element.classList.remove("active");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										420
									
								
								src/accounting/static/js/period-chooser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								src/accounting/static/js/period-chooser.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,420 @@
 | 
				
			|||||||
 | 
					/* The Mia! Accounting Flask Project
 | 
				
			||||||
 | 
					 * period-chooser.js: The JavaScript for the period chooser
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					 *  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					 *  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					 *  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					 *  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					 *  limitations under the License.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Author: imacat@mail.imacat.idv.tw (imacat)
 | 
				
			||||||
 | 
					 * First written: 2023/3/4
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initializes the page JavaScript.
 | 
				
			||||||
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
 | 
					    PeriodChooser.initialize();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The period chooser.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class PeriodChooser {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The modal of the period chooser
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    modal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab planes
 | 
				
			||||||
 | 
					     * @type {{month: MonthTab, year: YearTab, day: DayTab, custom: CustomTab}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabPlanes = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs the period chooser.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        const prefix = "accounting-period-chooser";
 | 
				
			||||||
 | 
					        this.modal = document.getElementById(prefix + "-modal");
 | 
				
			||||||
 | 
					        for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) {
 | 
				
			||||||
 | 
					            const tab = new cls(this);
 | 
				
			||||||
 | 
					            this.tabPlanes[tab.tabId()] = tab;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The period chooser.
 | 
				
			||||||
 | 
					     * @type {PeriodChooser}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static #chooser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Initializes the period chooser.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    static initialize() {
 | 
				
			||||||
 | 
					        this.#chooser = new PeriodChooser();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A tab plane.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @abstract
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The period chooser
 | 
				
			||||||
 | 
					     * @type {PeriodChooser}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    chooser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The prefix of the HTML ID and class names
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    prefix;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab
 | 
				
			||||||
 | 
					     * @type {HTMLSpanElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #tab;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The page
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #page;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param chooser {PeriodChooser} the period chooser
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(chooser) {
 | 
				
			||||||
 | 
					        this.chooser = chooser;
 | 
				
			||||||
 | 
					        this.prefix = "accounting-period-chooser-" + this.tabId();
 | 
				
			||||||
 | 
					        this.#tab = document.getElementById(this.prefix + "-tab");
 | 
				
			||||||
 | 
					        this.#page = document.getElementById(this.prefix + "-page");
 | 
				
			||||||
 | 
					        this.#tab.onclick = () => this.#switchToMe();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab ID
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     * @abstract
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabId() { throw new Error("Method not implemented.") };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Switches to the tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #switchToMe() {
 | 
				
			||||||
 | 
					        for (const tabPlane of Object.values(this.chooser.tabPlanes)) {
 | 
				
			||||||
 | 
					            tabPlane.#tab.classList.remove("active")
 | 
				
			||||||
 | 
					            tabPlane.#tab.ariaCurrent = "false";
 | 
				
			||||||
 | 
					            tabPlane.#page.classList.add("d-none");
 | 
				
			||||||
 | 
					            tabPlane.#page.ariaCurrent = "false";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#tab.classList.add("active");
 | 
				
			||||||
 | 
					        this.#tab.ariaCurrent = "page";
 | 
				
			||||||
 | 
					        this.#page.classList.remove("d-none");
 | 
				
			||||||
 | 
					        this.#page.ariaCurrent = "page";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The month tab plane.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class MonthTab extends TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The month chooser.
 | 
				
			||||||
 | 
					     * @type {tempusDominus.TempusDominus}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #monthChooser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param chooser {PeriodChooser} the period chooser
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(chooser) {
 | 
				
			||||||
 | 
					        super(chooser);
 | 
				
			||||||
 | 
					        const monthChooser = document.getElementById(this.prefix + "-chooser");
 | 
				
			||||||
 | 
					        if (monthChooser !== null) {
 | 
				
			||||||
 | 
					            let start = monthChooser.dataset.start;
 | 
				
			||||||
 | 
					            this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
 | 
				
			||||||
 | 
					                restrictions: {
 | 
				
			||||||
 | 
					                    minDate: new Date(start),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                display: {
 | 
				
			||||||
 | 
					                    inline: true,
 | 
				
			||||||
 | 
					                    components: {
 | 
				
			||||||
 | 
					                        date: false,
 | 
				
			||||||
 | 
					                        clock: false,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                defaultDate: new Date(monthChooser.dataset.default),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            monthChooser.addEventListener(tempusDominus.Namespace.events.change, (e) => {
 | 
				
			||||||
 | 
					                const date = e.detail.date;
 | 
				
			||||||
 | 
					                const year = date.year;
 | 
				
			||||||
 | 
					                const month = date.month + 1;
 | 
				
			||||||
 | 
					                const period = month < 10? year + "-0" + month: year + "-" + month;
 | 
				
			||||||
 | 
					                window.location = chooser.modal.dataset.urlTemplate
 | 
				
			||||||
 | 
					                    .replaceAll("PERIOD", period);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab ID
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabId() {
 | 
				
			||||||
 | 
					        return "month";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The year tab plane.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class YearTab extends TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab ID
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabId() {
 | 
				
			||||||
 | 
					        return "year";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The day tab plane.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class DayTab extends TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The day input
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error of the date input
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #dateError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param chooser {PeriodChooser} the period chooser
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(chooser) {
 | 
				
			||||||
 | 
					        super(chooser);
 | 
				
			||||||
 | 
					        this.#date = document.getElementById(this.prefix + "-date");
 | 
				
			||||||
 | 
					        this.#dateError = document.getElementById(this.prefix + "-date-error");
 | 
				
			||||||
 | 
					        if (this.#date !== null) {
 | 
				
			||||||
 | 
					            this.#date.onchange = () => {
 | 
				
			||||||
 | 
					                if (this.#validateDate()) {
 | 
				
			||||||
 | 
					                    window.location = chooser.modal.dataset.urlTemplate
 | 
				
			||||||
 | 
					                        .replaceAll("PERIOD", this.#date.value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the date.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateDate() {
 | 
				
			||||||
 | 
					        if (this.#date.value === "") {
 | 
				
			||||||
 | 
					            this.#date.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#dateError.innerText = A_("Please fill in the date.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#date.value < this.#date.min) {
 | 
				
			||||||
 | 
					            this.#date.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#dateError.innerText = A_("The date is too early.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#date.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#dateError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab ID
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabId() {
 | 
				
			||||||
 | 
					        return "day";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The custom tab plane.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @private
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class CustomTab extends TabPlane {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The start of the period
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error of the start
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #startError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The end of the period
 | 
				
			||||||
 | 
					     * @type {HTMLInputElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #end;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The error of the end
 | 
				
			||||||
 | 
					     * @type {HTMLDivElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #endError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The confirm button
 | 
				
			||||||
 | 
					     * @type {HTMLButtonElement}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #conform;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Constructs a tab plane.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param chooser {PeriodChooser} the period chooser
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(chooser) {
 | 
				
			||||||
 | 
					        super(chooser);
 | 
				
			||||||
 | 
					        this.#start = document.getElementById(this.prefix + "-start");
 | 
				
			||||||
 | 
					        this.#startError = document.getElementById(this.prefix + "-start-error");
 | 
				
			||||||
 | 
					        this.#end = document.getElementById(this.prefix + "-end");
 | 
				
			||||||
 | 
					        this.#endError = document.getElementById(this.prefix + "-end-error");
 | 
				
			||||||
 | 
					        this.#conform = document.getElementById(this.prefix + "-confirm");
 | 
				
			||||||
 | 
					        if (this.#start !== null) {
 | 
				
			||||||
 | 
					            this.#start.onchange = () => {
 | 
				
			||||||
 | 
					                if (this.#validateStart()) {
 | 
				
			||||||
 | 
					                    this.#end.min = this.#start.value;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            this.#end.onchange = () => {
 | 
				
			||||||
 | 
					                if (this.#validateEnd()) {
 | 
				
			||||||
 | 
					                    this.#start.max = this.#end.value;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            this.#conform.onclick = () => {
 | 
				
			||||||
 | 
					                let isValid = true;
 | 
				
			||||||
 | 
					                isValid = this.#validateStart() && isValid;
 | 
				
			||||||
 | 
					                isValid = this.#validateEnd() && isValid;
 | 
				
			||||||
 | 
					                if (isValid) {
 | 
				
			||||||
 | 
					                    window.location = chooser.modal.dataset.urlTemplate
 | 
				
			||||||
 | 
					                        .replaceAll("PERIOD", this.#start.value + "-" + this.#end.value);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the start of the period.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateStart() {
 | 
				
			||||||
 | 
					        if (this.#start.value === "") {
 | 
				
			||||||
 | 
					            this.#start.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#startError.innerText = A_("Please fill in the start date.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#start.value < this.#start.min) {
 | 
				
			||||||
 | 
					            this.#start.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#startError.innerText = A_("The start date is too early.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#start.value > this.#start.max) {
 | 
				
			||||||
 | 
					            this.#start.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#startError.innerText = A_("The start date cannot be beyond the end date.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#start.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#startError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Validates the end of the period.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @returns {boolean} true if valid, or false otherwise
 | 
				
			||||||
 | 
					     * @private
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    #validateEnd() {
 | 
				
			||||||
 | 
					        this.#end.value = this.#end.value.trim();
 | 
				
			||||||
 | 
					        if (this.#end.value === "") {
 | 
				
			||||||
 | 
					            this.#end.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#endError.innerText = A_("Please fill in the end date.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.#end.value < this.#end.min) {
 | 
				
			||||||
 | 
					            this.#end.classList.add("is-invalid");
 | 
				
			||||||
 | 
					            this.#endError.innerText = A_("The end date cannot be beyond the start date.");
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.#end.classList.remove("is-invalid");
 | 
				
			||||||
 | 
					        this.#endError.innerText = "";
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The tab ID
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return {string}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    tabId() {
 | 
				
			||||||
 | 
					        return "custom";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								src/accounting/template_filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/accounting/template_filters.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The template filters.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import typing as t
 | 
				
			||||||
 | 
					from datetime import date, timedelta
 | 
				
			||||||
 | 
					from decimal import Decimal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask_babel import get_locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounting.locale import gettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_amount(value: Decimal | None) -> str | None:
 | 
				
			||||||
 | 
					    """Formats an amount for readability.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The amount.
 | 
				
			||||||
 | 
					    :return: The formatted amount text.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if value is None:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    if value == 0:
 | 
				
			||||||
 | 
					        return "-"
 | 
				
			||||||
 | 
					    whole: int = int(value)
 | 
				
			||||||
 | 
					    frac: Decimal = (value - whole).normalize()
 | 
				
			||||||
 | 
					    return "{:,}".format(whole) + str(abs(frac))[1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def format_date(value: date) -> str:
 | 
				
			||||||
 | 
					    """Formats a date to be human-friendly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The date.
 | 
				
			||||||
 | 
					    :return: The human-friendly date text.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    today: date = date.today()
 | 
				
			||||||
 | 
					    if value == today:
 | 
				
			||||||
 | 
					        return gettext("Today")
 | 
				
			||||||
 | 
					    if value == today - timedelta(days=1):
 | 
				
			||||||
 | 
					        return gettext("Yesterday")
 | 
				
			||||||
 | 
					    if value == today + timedelta(days=1):
 | 
				
			||||||
 | 
					        return gettext("Tomorrow")
 | 
				
			||||||
 | 
					    locale = str(get_locale())
 | 
				
			||||||
 | 
					    if locale == "zh" or locale.startswith("zh_"):
 | 
				
			||||||
 | 
					        if value == today - timedelta(days=2):
 | 
				
			||||||
 | 
					            return gettext("The day before yesterday")
 | 
				
			||||||
 | 
					        if value == today + timedelta(days=2):
 | 
				
			||||||
 | 
					            return gettext("The day after tomorrow")
 | 
				
			||||||
 | 
					    if locale == "zh" or locale.startswith("zh_"):
 | 
				
			||||||
 | 
					        weekdays = ["一", "二", "三", "四", "五", "六", "日"]
 | 
				
			||||||
 | 
					        weekday = weekdays[value.weekday()]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        weekday = value.strftime("%a")
 | 
				
			||||||
 | 
					    if value.year != today.year:
 | 
				
			||||||
 | 
					        return "{}/{}/{}({})".format(
 | 
				
			||||||
 | 
					            value.year, value.month, value.day, weekday)
 | 
				
			||||||
 | 
					    return "{}/{}({})".format(value.month, value.day, weekday)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def default(value: t.Any, default_value: t.Any = "") -> t.Any:
 | 
				
			||||||
 | 
					    """Returns the default value if the given value is None.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: The value.
 | 
				
			||||||
 | 
					    :param default_value: The default value when the given value is None.
 | 
				
			||||||
 | 
					    :return: The value, or the default value if the given value is None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return default_value if value is None else value
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/accounting/template_globals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/accounting/template_globals.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# The Mia! Accounting Flask Project.
 | 
				
			||||||
 | 
					# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#  Copyright (c) 2023 imacat.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					#  you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					#  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#      http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					#  See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					#  limitations under the License.
 | 
				
			||||||
 | 
					"""The template globals.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from accounting.models import Currency
 | 
				
			||||||
 | 
					from accounting.utils.options import options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def currency_options() -> str:
 | 
				
			||||||
 | 
					    """Returns the currency options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The currency options.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return Currency.query.order_by(Currency.code).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def default_currency_code() -> str:
 | 
				
			||||||
 | 
					    """Returns the default currency code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :return: The default currency code.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return options.default_currency_code
 | 
				
			||||||
@@ -25,49 +25,56 @@ First written: 2023/1/31
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
					  <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.list")|accounting_or_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary d-none d-md-inline" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary d-none d-md-inline" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-gear"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
      {{ A_("Settings") }}
 | 
					      {{ A_("Edit") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <a class="btn btn-primary" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
					  <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.order", base=obj.base)|accounting_append_next }}">
 | 
				
			||||||
    <i class="fa-solid fa-bars-staggered"></i>
 | 
					    <i class="fa-solid fa-bars-staggered"></i>
 | 
				
			||||||
    {{ A_("Order") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Order") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
					    {% if obj.can_delete %}
 | 
				
			||||||
      <i class="fa-solid fa-trash"></i>
 | 
					      <button class="btn btn-danger" type="button" data-bs-toggle="modal" data-bs-target="#accounting-delete-modal">
 | 
				
			||||||
      {{ A_("Delete") }}
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
    </button>
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% else %}
 | 
				
			||||||
 | 
					      <button class="btn btn-secondary" type="button" disabled="disabled">
 | 
				
			||||||
 | 
					        <i class="fa-solid fa-trash"></i>
 | 
				
			||||||
 | 
					        <span class="d-none d-md-inline">{{ A_("Delete") }}</span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() %}
 | 
				
			||||||
  <div class="d-md-none accounting-material-fab">
 | 
					  <div class="d-md-none accounting-material-fab">
 | 
				
			||||||
    <a class="btn btn-primary" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
					    <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.edit", account=obj)|accounting_inherit_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-pen-to-square"></i>
 | 
					      <i class="fa-solid fa-pen-to-square"></i>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() and obj.can_delete %}
 | 
				
			||||||
  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
					  <form action="{{ url_for("accounting.account.delete", account=obj) }}" method="post">
 | 
				
			||||||
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
    {% if "next" in request.args %}
 | 
					    {% if request.args.next %}
 | 
				
			||||||
      <input type="hidden" name="next" value="{{ request.args["next"] }}">
 | 
					      <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
    <div class="modal fade" id="accounting-delete-modal" tabindex="-1" aria-labelledby="accounting-delete-model-label" aria-hidden="true">
 | 
					    <div class="modal fade" id="accounting-delete-modal" tabindex="-1" aria-labelledby="accounting-delete-modal-label" aria-hidden="true">
 | 
				
			||||||
      <div class="modal-dialog">
 | 
					      <div class="modal-dialog">
 | 
				
			||||||
        <div class="modal-content">
 | 
					        <div class="modal-content">
 | 
				
			||||||
          <div class="modal-header">
 | 
					          <div class="modal-header">
 | 
				
			||||||
            <h1 class="modal-title fs-5" id="accounting-delete-model-label">{{ A_("Delete Account Confirmation") }}</h1>
 | 
					            <h1 class="modal-title fs-5" id="accounting-delete-modal-label">{{ A_("Confirm Delete Account") }}</h1>
 | 
				
			||||||
            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
					            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div class="modal-body">
 | 
					          <div class="modal-body">
 | 
				
			||||||
            {{ A_("Do you really want to delete this account?") }}
 | 
					            {{ A_("Do you really want to delete this account?") }}
 | 
				
			||||||
@@ -83,11 +90,11 @@ First written: 2023/1/31
 | 
				
			|||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="accounting-card col-sm-6">
 | 
					<div class="accounting-card col-sm-6">
 | 
				
			||||||
  <div class="accounting-card-title">{{ obj.title }}</div>
 | 
					  <div class="accounting-card-title">{{ obj.title|title }}</div>
 | 
				
			||||||
  <div class="accounting-card-code">{{ obj.code }}</div>
 | 
					  <div class="accounting-card-code">{{ obj.code }}</div>
 | 
				
			||||||
  {% if obj.is_offset_needed %}
 | 
					  {% if obj.is_need_offset %}
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <span class="badge rounded-pill bg-info">{{ A_("Offset needed") }}</span>
 | 
					      <span class="badge rounded-pill bg-info">{{ A_("Needs Offset") }}</span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <div class="small text-secondary fst-italic">
 | 
					  <div class="small text-secondary fst-italic">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,23 +27,23 @@ First written: 2023/2/1
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group btn-actions mb-3">
 | 
					<div class="mb-3 accounting-toolbar">
 | 
				
			||||||
  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
					  <a class="btn btn-primary" role="button" href="{% block back_url %}{% endblock %}">
 | 
				
			||||||
    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
					    <i class="fa-solid fa-circle-chevron-left"></i>
 | 
				
			||||||
    {{ A_("Back") }}
 | 
					    <span class="d-none d-md-inline">{{ A_("Back") }}</span>
 | 
				
			||||||
  </a>
 | 
					  </a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post">
 | 
					<form id="accounting-form" action="{% block action_url %}{% endblock %}" method="post">
 | 
				
			||||||
  {{ form.csrf_token }}
 | 
					  {{ form.csrf_token }}
 | 
				
			||||||
  {% if "next" in request.args %}
 | 
					  {% if request.args.next %}
 | 
				
			||||||
    <input type="hidden" name="next" value="{{ request.args["next"] }}">
 | 
					    <input type="hidden" name="next" value="{{ request.args.next }}">
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <div class="form-floating mb-3">
 | 
					  <div class="form-floating mb-3">
 | 
				
			||||||
    <input id="accounting-base-code" type="hidden" name="base_code" value="{{ "" if form.base_code.data is none else form.base_code.data }}">
 | 
					    <input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}">
 | 
				
			||||||
    <div id="accounting-base" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-model">
 | 
					    <div id="accounting-base-control" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal">
 | 
				
			||||||
      <label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
 | 
					      <label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
 | 
				
			||||||
      <div id="accounting-base-content">
 | 
					      <div id="accounting-base">
 | 
				
			||||||
        {% if form.base_code.data %}
 | 
					        {% if form.base_code.data %}
 | 
				
			||||||
          {% if form.base_code.errors %}
 | 
					          {% if form.base_code.errors %}
 | 
				
			||||||
            {{ A_("(Unknown)") }}
 | 
					            {{ A_("(Unknown)") }}
 | 
				
			||||||
@@ -53,19 +53,19 @@ First written: 2023/2/1
 | 
				
			|||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div id="accounting-base-code-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
 | 
					    <div id="accounting-base-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="form-floating mb-3">
 | 
					  <div class="form-floating mb-3">
 | 
				
			||||||
    <input id="accounting-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ "" if form.title.data is none else form.title.data }}" placeholder=" " required="required">
 | 
					    <input id="accounting-title" class="form-control {% if form.title.errors %} is-invalid {% endif %}" type="text" name="title" value="{{ form.title.data|accounting_default }}" placeholder=" " required="required">
 | 
				
			||||||
    <label class="form-label" for="accounting-title">{{ A_("Title") }}</label>
 | 
					    <label class="form-label" for="accounting-title">{{ A_("Title") }}</label>
 | 
				
			||||||
    <div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
 | 
					    <div id="accounting-title-error" class="invalid-feedback">{% if form.title.errors %}{{ form.title.errors[0] }}{% endif %}</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="form-check form-switch mb-3">
 | 
					  <div id="accounting-is-need-offset-control" class="form-check form-switch mb-3 {% if form.base_code.data[0] not in ["1", "2", "3"] %} d-none {% endif %}">
 | 
				
			||||||
    <input id="accounting-is-offset-needed" class="form-check-input" type="checkbox" name="is_offset_needed" value="1" {% if form.is_offset_needed.data %} checked="checked" {% endif %}>
 | 
					    <input id="accounting-is-need-offset" class="form-check-input" type="checkbox" name="is_need_offset" value="1" {% if form.is_need_offset.data %} checked="checked" {% endif %}>
 | 
				
			||||||
    <label class="form-check-label" for="accounting-is-offset-needed">
 | 
					    <label class="form-check-label" for="accounting-is-need-offset">
 | 
				
			||||||
      {{ A_("The entries in the account need offsets.") }}
 | 
					      {{ A_("The line items in the account need offset.") }}
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,37 +83,37 @@ First written: 2023/2/1
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="modal fade" id="accounting-base-selector-model" tabindex="-1" aria-labelledby="accounting-base-selector-model-label" aria-hidden="true">
 | 
					<div class="modal fade" id="accounting-base-selector-modal" tabindex="-1" aria-labelledby="accounting-base-selector-modal-label" aria-hidden="true">
 | 
				
			||||||
  <div class="modal-dialog">
 | 
					  <div class="modal-dialog">
 | 
				
			||||||
    <div class="modal-content">
 | 
					    <div class="modal-content">
 | 
				
			||||||
      <div class="modal-header">
 | 
					      <div class="modal-header">
 | 
				
			||||||
        <h1 class="modal-title fs-5" id="accounting-base-selector-model-label">{{ A_("Select Base Account") }}</h1>
 | 
					        <h1 class="modal-title fs-5" id="accounting-base-selector-modal-label">{{ A_("Select Base Account") }}</h1>
 | 
				
			||||||
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 | 
					        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ A_("Close") }}"></button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="modal-body">
 | 
					      <div class="modal-body">
 | 
				
			||||||
        <div class="input-group mb-2">
 | 
					        <div class="input-group mb-2">
 | 
				
			||||||
          <input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" aria-label="Search">
 | 
					          <input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
 | 
				
			||||||
          <label class="input-group-text" for="accounting-base-selector-query">
 | 
					          <label class="input-group-text" for="accounting-base-selector-query">
 | 
				
			||||||
            <i class="fa-solid fa-magnifying-glass"></i>
 | 
					            <i class="fa-solid fa-magnifying-glass"></i>
 | 
				
			||||||
            {{ A_("Search") }}
 | 
					            {{ A_("Search") }}
 | 
				
			||||||
          </label>
 | 
					          </label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ul id="accounting-base-option-list" class="list-group accounting-selector-list">
 | 
					        <ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
 | 
				
			||||||
          {% for base in form.base_options %}
 | 
					          {% for base in form.base_options %}
 | 
				
			||||||
          <li id="accounting-base-option-{{ base.code }}" class="list-group-item accounting-base-option accounting-clickable" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}">
 | 
					            <li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
 | 
				
			||||||
            {{ base }}
 | 
					              {{ base }}
 | 
				
			||||||
          </li>
 | 
					            </li>
 | 
				
			||||||
          {% endfor %}
 | 
					          {% endfor %}
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
        <p id="accounting-base-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
 | 
					        <p id="accounting-base-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="modal-footer">
 | 
					      <div class="modal-footer">
 | 
				
			||||||
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
 | 
					        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
 | 
				
			||||||
        {% if form.base_code.data %}
 | 
					        {% if form.base_code.data %}
 | 
				
			||||||
          <button id="accounting-btn-clear-base" type="button" class="btn btn-danger">{{ A_("Clear") }}</button>
 | 
					          <button id="accounting-base-selector-clear" type="button" class="btn btn-danger" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
 | 
				
			||||||
        {% else %}
 | 
					        {% else %}
 | 
				
			||||||
          <button id="accounting-btn-clear-base" type="button" class="btn btn-secondary" disabled="disabled">{{ A_("Clear") }}</button>
 | 
					          <button id="accounting-base-selector-clear" type="button" class="btn btn-secondary" disabled="disabled" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,23 +21,23 @@ First written: 2023/1/30
 | 
				
			|||||||
#}
 | 
					#}
 | 
				
			||||||
{% extends "accounting/base.html" %}
 | 
					{% extends "accounting/base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block header %}{% block title %}{% if "q" in request.args %}{{ A_("Search Result for \"%(query)s\"", query=request.args["q"]) }}{% else %}{{ A_("Account Management") }}{% endif %}{% endblock %}{% endblock %}
 | 
					{% block header %}{% block title %}{% if request.args.q %}{{ A_("Search Result for \"%(query)s\"", query=request.args.q) }}{% else %}{{ A_("Account Management") }}{% endif %}{% endblock %}{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="btn-group mb-2">
 | 
					<div class="mb-2 accounting-toolbar">
 | 
				
			||||||
  {% if accounting_can_edit() %}
 | 
					  {% if accounting_can_edit() %}
 | 
				
			||||||
    <a class="btn btn-primary text-nowrap d-none d-md-block" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
					    <a class="btn btn-primary text-nowrap d-none d-md-block" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-plus"></i>
 | 
					      <i class="fa-solid fa-plus"></i>
 | 
				
			||||||
      {{ A_("New") }}
 | 
					      {{ A_("New") }}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search">
 | 
					  <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
 | 
				
			||||||
    <input id="accounting-search" class="form-control form-control-sm accounting-search-input" type="search" name="q" value="{{ request.args["q"] if "q" in request.args else "" }}" placeholder=" " required="required" aria-label="Search">
 | 
					    <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
 | 
				
			||||||
    <label for="accounting-search" class="accounting-search-label">
 | 
					    <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
 | 
				
			||||||
      <button type="submit">
 | 
					      <button type="submit">
 | 
				
			||||||
        <i class="fa-solid fa-magnifying-glass"></i>
 | 
					        <i class="fa-solid fa-magnifying-glass"></i>
 | 
				
			||||||
        {{ A_("Search") }}
 | 
					        <span class="d-none d-md-inline">{{ A_("Search") }}</span>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
@@ -45,7 +45,7 @@ First written: 2023/1/30
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% if accounting_can_edit() %}
 | 
					{% if accounting_can_edit() %}
 | 
				
			||||||
  <div class="d-md-none accounting-material-fab">
 | 
					  <div class="d-md-none accounting-material-fab">
 | 
				
			||||||
    <a class="btn btn-primary" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
					    <a class="btn btn-primary" role="button" href="{{ url_for("accounting.account.create")|accounting_append_next }}">
 | 
				
			||||||
      <i class="fa-solid fa-plus"></i>
 | 
					      <i class="fa-solid fa-plus"></i>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -58,8 +58,8 @@ First written: 2023/1/30
 | 
				
			|||||||
  {% for item in list %}
 | 
					  {% for item in list %}
 | 
				
			||||||
    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}">
 | 
					    <a class="list-group-item list-group-item-action" href="{{ url_for("accounting.account.detail", account=item)|accounting_append_next }}">
 | 
				
			||||||
      {{ item }}
 | 
					      {{ item }}
 | 
				
			||||||
      {% if item.is_offset_needed %}
 | 
					      {% if item.is_need_offset %}
 | 
				
			||||||
        <span class="badge rounded-pill bg-info">{{ A_("Offset needed") }}</span>
 | 
					        <span class="badge rounded-pill bg-info">{{ A_("Needs Offset") }}</span>
 | 
				
			||||||
      {% endif %}
 | 
					      {% endif %}
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  {% endfor %}
 | 
					  {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user