mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-23 12:50:40 +00:00
Compare commits
606 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08cabd1717 | ||
|
|
6f98fe15e4 | ||
|
|
752d175d22 | ||
|
|
99ead94ffe | ||
|
|
e05ae14d49 | ||
|
|
cd9f949b12 | ||
|
|
aef544cd53 | ||
|
|
d34025dc11 | ||
|
|
4db12fe2da | ||
|
|
ea12897ce9 | ||
|
|
802d9b2d4a | ||
|
|
492ba539e8 | ||
|
|
21a7dd43a9 | ||
|
|
50d1fa4665 | ||
|
|
e10a58074f | ||
|
|
b2deb89826 | ||
|
|
c490a66540 | ||
|
|
afa0c13587 | ||
|
|
4dbee00b82 | ||
|
|
5d6fc71556 | ||
|
|
06dd5e0071 | ||
|
|
105f9ec2e1 | ||
|
|
dc6fdbb836 | ||
|
|
8668ae92d8 | ||
|
|
ec7e5c48de | ||
|
|
85088e4aff | ||
|
|
c2c6d27625 | ||
|
|
a70181e025 | ||
|
|
86017b223a | ||
|
|
930e79c351 | ||
|
|
2c7f5ec324 | ||
|
|
a9f5e86600 | ||
|
|
d6decf9172 | ||
|
|
1fac17b36f | ||
|
|
9d05a6ebc0 | ||
|
|
389ee909a5 | ||
|
|
e5aaa5b6e5 | ||
|
|
f3ceb4ac7d | ||
|
|
050ca4b726 | ||
|
|
928b6b1510 | ||
|
|
ef1e121bd4 | ||
|
|
68f1b41969 | ||
|
|
a329003f7f | ||
|
|
cf1eabe049 | ||
|
|
4c78a682ad | ||
|
|
4752ed2483 | ||
|
|
e56dd8268b | ||
|
|
e0477cf59f | ||
|
|
8c115e146b | ||
|
|
6267ab994c | ||
|
|
8bf8bcf739 | ||
|
|
eed02d3f44 | ||
|
|
6265582e53 | ||
|
|
361836e735 | ||
|
|
ae73d9c621 | ||
|
|
2c2ca22d12 | ||
|
|
e37a88fdb6 | ||
|
|
9c26093a51 | ||
|
|
5ce2d73692 | ||
|
|
ca0a962870 | ||
|
|
b72906a7a1 | ||
|
|
d604b12d51 | ||
|
|
d46cf46375 | ||
|
|
c3f6edcd01 | ||
|
|
120b481c4a | ||
|
|
029021f035 | ||
|
|
9e109acec7 | ||
|
|
355ba2f632 | ||
|
|
83ce3dd915 | ||
|
|
60508a9706 | ||
|
|
660d20f7fa | ||
|
|
0b2603bbf1 | ||
|
|
5660e8b26d | ||
|
|
cf0fa0db7b | ||
|
|
fc9a3c0c92 | ||
|
|
2b4cd0a9bb | ||
|
|
6decb7cc34 | ||
|
|
9039b86e8a | ||
|
|
ee8485a54a | ||
|
|
05db28c64f | ||
|
|
bcd0105915 | ||
|
|
225adf5cbc | ||
|
|
af5947edd0 | ||
|
|
d6f70f533a | ||
|
|
1dd4168c0e | ||
|
|
db4360d76c | ||
|
|
8ce81a058a | ||
|
|
55464c79c4 | ||
|
|
0c599c2b6d | ||
|
|
eb1f1255eb | ||
|
|
62cc86114b | ||
|
|
5268da2e55 | ||
|
|
d695fea251 | ||
|
|
6b2983d8c1 | ||
|
|
85d74050e1 | ||
|
|
d69a974a4d | ||
|
|
3d9d56ab50 | ||
|
|
dbd7b83204 | ||
|
|
6770610c6d | ||
|
|
75916629c8 | ||
|
|
e785928c0f | ||
|
|
edfa6e41e1 | ||
|
|
5a9522e70f | ||
|
|
4fa5131590 | ||
|
|
1b28a4e928 | ||
|
|
f2a72e5f82 | ||
|
|
661eb058b9 | ||
|
|
6516e68fa0 | ||
|
|
35a08f8830 | ||
|
|
562327f041 | ||
|
|
8e7d893669 | ||
|
|
d96cee8779 | ||
|
|
96c4d1af63 | ||
|
|
9c0a17e4d5 | ||
|
|
ee2c8c869a | ||
|
|
15b34a607f | ||
|
|
4fc6d3ef64 | ||
|
|
7bf6251c21 | ||
|
|
5fc5934942 | ||
|
|
01f9139ebd | ||
|
|
4be557bdce | ||
|
|
96d8b5242d | ||
|
|
67bd540135 | ||
|
|
30fd11f138 | ||
|
|
0986d3ebe4 | ||
|
|
c551c2714c | ||
|
|
efc97cc59f | ||
|
|
21a01575b6 | ||
|
|
6f3b5604b9 | ||
|
|
50e47e796d | ||
|
|
4d3e43bdbe | ||
|
|
928c887de5 | ||
|
|
2984bad2c0 | ||
|
|
2d09ef2509 | ||
|
|
8b5997e38f | ||
|
|
86b10ce9bb | ||
|
|
2c4610c021 | ||
|
|
4b3f143f83 | ||
|
|
d495d93840 | ||
|
|
a1b6628c41 | ||
|
|
770bc1c293 | ||
|
|
907e3af1b0 | ||
|
|
e706aa692a | ||
|
|
a5fa287dad | ||
|
|
c1f14f2991 | ||
|
|
6d66002374 | ||
|
|
27cd51e267 | ||
|
|
1d6f97ad94 | ||
|
|
ea69ba7cd8 | ||
|
|
74c880c232 | ||
|
|
8d188dccd7 | ||
|
|
47f06dc180 | ||
|
|
9de0d4329c | ||
|
|
40fbb1d6ff | ||
|
|
0722aa5a3f | ||
|
|
cfea2de131 | ||
|
|
fe206b0d77 | ||
|
|
4e621b09ba | ||
|
|
1f42302997 | ||
|
|
0e1884539e | ||
|
|
b17a811abf | ||
|
|
979d801de5 | ||
|
|
49d5b7c4d3 | ||
|
|
176feb20ad | ||
|
|
ec27077d9c | ||
|
|
0a70b3ffcc | ||
|
|
7db135dab5 | ||
|
|
4278b08147 | ||
|
|
f36a68b42b | ||
|
|
8bc76bae9c | ||
|
|
9d2cbccff2 | ||
|
|
10ecdb99fe | ||
|
|
2aa1380c81 | ||
|
|
99e004b619 | ||
|
|
b415e858e7 | ||
|
|
4cec68c7ad | ||
|
|
085a4c61ac | ||
|
|
5f08ef5cd1 | ||
|
|
e06a01fae5 | ||
|
|
998fef779b | ||
|
|
b99ca7d9e9 | ||
|
|
0fd2964032 | ||
|
|
15baa3f305 | ||
|
|
5920525369 | ||
|
|
05c92cce71 | ||
|
|
a0f01dac1a | ||
|
|
e8c174c12b | ||
|
|
927f80035d | ||
|
|
e5ae828580 | ||
|
|
f89a3dbb65 | ||
|
|
2d9142832d | ||
|
|
a7ccc9420b | ||
|
|
40459288f6 | ||
|
|
250a1c9341 | ||
|
|
63d4fddb49 | ||
|
|
7e6d6f08a2 | ||
|
|
d1c72dc27b | ||
|
|
86ae644574 | ||
|
|
5a2a404a50 | ||
|
|
ef10c4ea4f | ||
|
|
f6725e2eed | ||
|
|
e90532e406 | ||
|
|
84e26e21ab | ||
|
|
34ca0c3bb6 | ||
|
|
42e4b8a68c | ||
|
|
34d159b3a2 | ||
|
|
17ad402695 | ||
|
|
953b5790ed | ||
|
|
557ef5d214 | ||
|
|
680354ac0d | ||
|
|
73d98addbc | ||
|
|
479e8573c2 | ||
|
|
6a0b15211a | ||
|
|
7aeadcbf98 | ||
|
|
82982e25c6 | ||
|
|
782c9dda1a | ||
|
|
dfe47261ae | ||
|
|
3d29007aeb | ||
|
|
52a161f076 | ||
|
|
0fe901a137 | ||
|
|
8c28cb6b25 | ||
|
|
4ba37e49d8 | ||
|
|
1e89c007ed | ||
|
|
78c68397d9 | ||
|
|
f61cec27ae | ||
|
|
78768f883c | ||
|
|
edcdfdd194 | ||
|
|
861edb438b | ||
|
|
d91013a467 | ||
|
|
310b131469 | ||
|
|
86e1818420 | ||
|
|
5fe347c909 | ||
|
|
44dde1c58d | ||
|
|
291f0a580b | ||
|
|
9c4eaa230c | ||
|
|
28f1f9355d | ||
|
|
41db9d3886 | ||
|
|
53c4c153ca | ||
|
|
bee27f314f | ||
|
|
c9b6b0d868 | ||
|
|
dea735de4d | ||
|
|
f7cedac526 | ||
|
|
c5051561e4 | ||
|
|
48158fbde0 | ||
|
|
d6a3d0d468 | ||
|
|
30e9f08f37 | ||
|
|
42494db3c7 | ||
|
|
a6a2b2daae | ||
|
|
741c18b144 | ||
|
|
7027be8fbc | ||
|
|
01f30682ee | ||
|
|
5edebb28a5 | ||
|
|
11359bd235 | ||
|
|
2ad6d637ee | ||
|
|
564ff034b7 | ||
|
|
d8d4cd23a5 | ||
|
|
984acb661d | ||
|
|
00f144ed68 | ||
|
|
f060534625 | ||
|
|
f101a1ce3b | ||
|
|
ad2d6a1625 | ||
|
|
2f56ba7f42 | ||
|
|
ef6b172616 | ||
|
|
61a42ea5d7 | ||
|
|
c1a6c56217 | ||
|
|
b7e95bfd22 | ||
|
|
7989bc23e1 | ||
|
|
055e7820c8 | ||
|
|
d618c9a481 | ||
|
|
5789de25b9 | ||
|
|
4df38d357f | ||
|
|
45ff8fa296 | ||
|
|
7f95e42bec | ||
|
|
578ddb9be4 | ||
|
|
28607f0026 | ||
|
|
31e0bb477e | ||
|
|
1657a83151 | ||
|
|
aab91a2307 | ||
|
|
0d9741fdd7 | ||
|
|
d9d86dae35 | ||
|
|
d61f38b8ed | ||
|
|
8c8dc241e5 | ||
|
|
c9f49caecc | ||
|
|
cd57e009dd | ||
|
|
f11e298984 | ||
|
|
2eec0c057a | ||
|
|
50b4257a6f | ||
|
|
a23e8b13be | ||
|
|
9f09bf14cb | ||
|
|
5413372aeb | ||
|
|
710d30074d | ||
|
|
14e30d12b4 | ||
|
|
5929d50c72 | ||
|
|
d2923bae85 | ||
|
|
9e72a844f7 | ||
|
|
208bd2b8ff | ||
|
|
5110975c6d | ||
|
|
82764af09e | ||
|
|
3fd9df0d2e | ||
|
|
cf81202d94 | ||
|
|
0c0f103b83 | ||
|
|
ea4f7365ea | ||
|
|
346c06977c | ||
|
|
2dddd7906b | ||
|
|
026c2d7590 | ||
|
|
8e5252d6f8 | ||
|
|
ffc119a8a4 | ||
|
|
8f4dc8048d | ||
|
|
03e3374a8b | ||
|
|
ea86bc2235 | ||
|
|
60b81a2a59 | ||
|
|
354c34e4d8 | ||
|
|
a9bd11f59a | ||
|
|
33174b1ba2 | ||
|
|
47b216373d | ||
|
|
d17baddb0d | ||
|
|
80b5c16a2e | ||
|
|
a9b142eccd | ||
|
|
a66d1c30ae | ||
|
|
c379b783b1 | ||
|
|
f45b1db1a4 | ||
|
|
a69623c131 | ||
|
|
2c1f72e44c | ||
|
|
3915018400 | ||
|
|
b832b60b28 | ||
|
|
b70eb46222 | ||
|
|
829660e7f3 | ||
|
|
4649cf0a25 | ||
|
|
0bc947f30d | ||
|
|
8447bf34f0 | ||
|
|
6fde07da0e | ||
|
|
6bd95a6d17 | ||
|
|
6c74180e1c | ||
|
|
9bd3d7a020 | ||
|
|
84b0fa38d5 | ||
|
|
75cb29890d | ||
|
|
c31b4e6b4b | ||
|
|
cb64f90d7d | ||
|
|
7f261f3448 | ||
|
|
efdc2173b2 | ||
|
|
99828d945f | ||
|
|
11a6ebaeef | ||
|
|
d2b2002664 | ||
|
|
56dad7d365 | ||
|
|
6869e5dde9 | ||
|
|
5d5ec2ab7c | ||
|
|
f5a4ec129b | ||
|
|
e185a06a15 | ||
|
|
193d7981ea | ||
|
|
957eabf53e | ||
|
|
3bb186736d | ||
|
|
1121c6663f | ||
|
|
944479313c | ||
|
|
daa75eea00 | ||
|
|
a2b7fc18ab | ||
|
|
7d6984c873 | ||
|
|
64cbf446bd | ||
|
|
b99cdb5be7 | ||
|
|
d6de50634f | ||
|
|
4d7c0c004a | ||
|
|
f0e3fb466a | ||
|
|
8337439589 | ||
|
|
88e5ed7998 | ||
|
|
fee2255661 | ||
|
|
23d91145d0 | ||
|
|
6c8e0fd1fb | ||
|
|
512a171ad5 | ||
|
|
30f034555b | ||
|
|
a35ce12d60 | ||
|
|
8cf057849e | ||
|
|
f3c60ea0a7 | ||
|
|
c724573a18 | ||
|
|
53e1b57354 | ||
|
|
42e7725442 | ||
|
|
18b7af977c | ||
|
|
db746a4def | ||
|
|
3d5fb5fc90 | ||
|
|
a833010d2b | ||
|
|
0d6148f218 | ||
|
|
6d51d14dfd | ||
|
|
cb2cb4447a | ||
|
|
b3a8fe9391 | ||
|
|
2a820a85ed | ||
|
|
8d8dd0cd2b | ||
|
|
3a5f19853a | ||
|
|
c0dd794e15 | ||
|
|
f56ee58e81 | ||
|
|
2c99075899 | ||
|
|
01cd509113 | ||
|
|
5563bea789 | ||
|
|
a0a932a235 | ||
|
|
6f75a3c617 | ||
|
|
a4d3934f75 | ||
|
|
b4171e4bd9 | ||
|
|
76d32ab07a | ||
|
|
3d469db47b | ||
|
|
80244bafa4 | ||
|
|
c54e97b89a | ||
|
|
14202fae06 | ||
|
|
9fc0ac1a92 | ||
|
|
5bdd2989c6 | ||
|
|
42dccadff1 | ||
|
|
0650c22b53 | ||
|
|
ec26c92263 | ||
|
|
26248924b6 | ||
|
|
8d29dc6a81 | ||
|
|
6eb50fa300 | ||
|
|
8c350d43b2 | ||
|
|
caa6ca1d0b | ||
|
|
47f1714be4 | ||
|
|
ecb0506dba | ||
|
|
42382b3945 | ||
|
|
993114942e | ||
|
|
5a28a1728e | ||
|
|
80a5df0e96 | ||
|
|
25193c5e92 | ||
|
|
1ee14ac135 | ||
|
|
a3e5ffe915 | ||
|
|
ff868a9290 | ||
|
|
247ae7a965 | ||
|
|
3a149b3c9b | ||
|
|
08bed618f6 | ||
|
|
d9ca680a29 | ||
|
|
7401dc4015 | ||
|
|
eb7e063d5c | ||
|
|
0dbe79645c | ||
|
|
995773088a | ||
|
|
be736cf641 | ||
|
|
85089d3d64 | ||
|
|
eed6d2b81c | ||
|
|
edf53f1ab7 | ||
|
|
758282739e | ||
|
|
49d995c3ac | ||
|
|
72ca2ec9a5 | ||
|
|
4297895bd9 | ||
|
|
7428df8778 | ||
|
|
767c79663c | ||
|
|
fa85482662 | ||
|
|
831e2aaf18 | ||
|
|
9ac665b4bd | ||
|
|
8d8d84bae4 | ||
|
|
9f4cb98de6 | ||
|
|
72c16097d6 | ||
|
|
2203ea9301 | ||
|
|
28c9f2adab | ||
|
|
d04f7ffe87 | ||
|
|
21e5c01f11 | ||
|
|
ae6c1a30ac | ||
|
|
1040198ce1 | ||
|
|
69878b7847 | ||
|
|
ec0201cb85 | ||
|
|
5d7fb1d945 | ||
|
|
33542cb909 | ||
|
|
549dc286d0 | ||
|
|
f547befeb9 | ||
|
|
a144059c7c | ||
|
|
b96361e837 | ||
|
|
cd5994017c | ||
|
|
431fb62803 | ||
|
|
9e9de4c99e | ||
|
|
d3369368db | ||
|
|
b24de3e35b | ||
|
|
0344442d42 | ||
|
|
0decd0955b | ||
|
|
6f7fdbefac | ||
|
|
dbd466b6b2 | ||
|
|
4914481105 | ||
|
|
8514c01a91 | ||
|
|
751c20984f | ||
|
|
81b1cbd367 | ||
|
|
a72383da0e | ||
|
|
eb8213c4e7 | ||
|
|
4d9f522f22 | ||
|
|
528f42c713 | ||
|
|
7cc7179b05 | ||
|
|
cfaad685a4 | ||
|
|
e420fa9779 | ||
|
|
fbf1160357 | ||
|
|
a3e3585e50 | ||
|
|
0f9849e672 | ||
|
|
3be037221a | ||
|
|
b2e108afcc | ||
|
|
114a5f8cca | ||
|
|
aba54ba18f | ||
|
|
6a7768367e | ||
|
|
3a1ad6e844 | ||
|
|
40abd82e2d | ||
|
|
2933a86458 | ||
|
|
ecf0d0b388 | ||
|
|
b84ca04975 | ||
|
|
abb88662c1 | ||
|
|
73af5be1c3 | ||
|
|
81e3d269dc | ||
|
|
3dafa98eef | ||
|
|
3699b96adf | ||
|
|
f782af8ab3 | ||
|
|
2af48e40a1 | ||
|
|
389227bce8 | ||
|
|
8cd1952da3 | ||
|
|
f128fcd86c | ||
|
|
d890d02b5c | ||
|
|
defd5541b0 | ||
|
|
8624a0abce | ||
|
|
13b79c6781 | ||
|
|
2926915a06 | ||
|
|
536dc47eb0 | ||
|
|
225843265e | ||
|
|
a1f98603a6 | ||
|
|
ab59b33606 | ||
|
|
1f1e934675 | ||
|
|
582fffca93 | ||
|
|
e9b3a24221 | ||
|
|
0d5ac078f6 | ||
|
|
f109164f88 | ||
|
|
765c1104c4 | ||
|
|
a8de8aecf5 | ||
|
|
20c1bcd654 | ||
|
|
6eaad90535 | ||
|
|
91a5c562d8 | ||
|
|
70d5593ace | ||
|
|
f3401243be | ||
|
|
19dfeca96d | ||
|
|
774db56762 | ||
|
|
d15c535f86 | ||
|
|
a429f2f626 | ||
|
|
dfbc7e8b33 | ||
|
|
b41f10c1b9 | ||
|
|
44745a757d | ||
|
|
47bf2d408b | ||
|
|
c71f06be9e | ||
|
|
50b1fa5deb | ||
|
|
a99057754d | ||
|
|
6f16ae3e00 | ||
|
|
38d80aab99 | ||
|
|
d8939e0bb0 | ||
|
|
f376abf0fe | ||
|
|
64e75a8e08 | ||
|
|
67a210f722 | ||
|
|
a2df276880 | ||
|
|
151d293aaf | ||
|
|
13f6f3259d | ||
|
|
da6eea7743 | ||
|
|
b28fa8b994 | ||
|
|
7ed7c22469 | ||
|
|
6a4f06cf4a | ||
|
|
dfe0079573 | ||
|
|
a983d65404 | ||
|
|
2aa90fdc14 | ||
|
|
a81bc28afb | ||
|
|
70e40183bb | ||
|
|
bdfc1ddfc3 | ||
|
|
fb770f5633 | ||
|
|
9789648175 | ||
|
|
94f4c92a03 | ||
|
|
3cac4a598f | ||
|
|
654fea8a3b | ||
|
|
838d341d91 | ||
|
|
fd4143e686 | ||
|
|
2e3b2db03b | ||
|
|
51fa336350 | ||
|
|
0a3c0ecea2 | ||
|
|
2d644ac066 | ||
|
|
d8768c5377 | ||
|
|
65c1387a04 | ||
|
|
3ad1f2d0d0 | ||
|
|
af31f3b291 | ||
|
|
2f81c99143 | ||
|
|
e679b336ef | ||
|
|
82c17632f7 | ||
|
|
59b9b7dc91 | ||
|
|
d242d5330c | ||
|
|
05c606b65d | ||
|
|
52578b03b4 | ||
|
|
8978a54479 | ||
|
|
b44598cb7a | ||
|
|
48e05b6110 | ||
|
|
3640bc7f88 | ||
|
|
531410820f | ||
|
|
d08eba7308 | ||
|
|
c1bfa0a31a | ||
|
|
155ca14b19 | ||
|
|
3ee5991896 | ||
|
|
7ec7e11634 | ||
|
|
17a6224a3f | ||
|
|
e9bcb1bbb2 | ||
|
|
830b95bdcb | ||
|
|
2de86eb0f4 | ||
|
|
9616c59ed8 | ||
|
|
f27e9f3089 | ||
|
|
9321408154 | ||
|
|
97cc3082e1 | ||
|
|
b7b4de7c8d | ||
|
|
dc9cf74be8 | ||
|
|
4e826fb3e9 | ||
|
|
553513c4c6 | ||
|
|
5cfa2d0692 | ||
|
|
3da7071327 | ||
|
|
c3293d110c | ||
|
|
78eb443614 | ||
|
|
97cadfe5d3 | ||
|
|
87d8603d1d | ||
|
|
7dc68cabcc | ||
|
|
7c8d13c51a | ||
|
|
14f9aef55c | ||
|
|
6e833cce6a | ||
|
|
976abf7b3c |
@@ -2,8 +2,9 @@ import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.31.5"
|
||||
__version__ = "15.38.4"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
def check_app_permission():
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
if is_website_user():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -103,14 +103,12 @@ class Account(NestedSet):
|
||||
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
|
||||
|
||||
def validate(self):
|
||||
from erpnext.accounts.utils import validate_field_number
|
||||
|
||||
if frappe.local.flags.allow_unverified_charts:
|
||||
return
|
||||
self.validate_parent()
|
||||
self.validate_parent_child_account_type()
|
||||
self.validate_root_details()
|
||||
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
|
||||
self.validate_account_number()
|
||||
self.validate_group_or_ledger()
|
||||
self.set_root_and_report_type()
|
||||
self.validate_mandatory()
|
||||
@@ -202,7 +200,7 @@ class Account(NestedSet):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
@@ -311,6 +309,22 @@ class Account(NestedSet):
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
|
||||
|
||||
def validate_account_number(self, account_number=None):
|
||||
if not account_number:
|
||||
account_number = self.account_number
|
||||
|
||||
if account_number:
|
||||
account_with_same_number = frappe.db.get_value(
|
||||
"Account",
|
||||
{"account_number": account_number, "company": self.company, "name": ["!=", self.name]},
|
||||
)
|
||||
if account_with_same_number:
|
||||
frappe.throw(
|
||||
_("Account Number {0} already used in account {1}").format(
|
||||
account_number, account_with_same_number
|
||||
)
|
||||
)
|
||||
|
||||
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
|
||||
for company in descendants:
|
||||
company_bold = frappe.bold(company)
|
||||
@@ -464,19 +478,6 @@ def get_account_autoname(account_number, account_name, company):
|
||||
return " - ".join(parts)
|
||||
|
||||
|
||||
def validate_account_number(name, account_number, company):
|
||||
if account_number:
|
||||
account_with_same_number = frappe.db.get_value(
|
||||
"Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
|
||||
)
|
||||
if account_with_same_number:
|
||||
frappe.throw(
|
||||
_("Account Number {0} already used in account {1}").format(
|
||||
account_number, account_with_same_number
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
@@ -517,7 +518,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
frappe.throw(message, title=_("Rename Not Allowed"))
|
||||
|
||||
validate_account_number(name, account_number, account.company)
|
||||
account.validate_account_number(account_number)
|
||||
if account_number:
|
||||
frappe.db.set_value("Account", name, "account_number", account_number.strip())
|
||||
else:
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"Utility Expenses": {},
|
||||
"Write Off": {},
|
||||
"Exchange Gain/Loss": {},
|
||||
"Gain/Loss on Asset Disposal": {}
|
||||
"Gain/Loss on Asset Disposal": {},
|
||||
"Impairment": {}
|
||||
},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
@@ -132,7 +133,8 @@
|
||||
"Source of Funds (Liabilities)": {
|
||||
"Capital Account": {
|
||||
"Reserves and Surplus": {},
|
||||
"Shareholders Funds": {}
|
||||
"Shareholders Funds": {},
|
||||
"Revaluation Surplus": {}
|
||||
},
|
||||
"Current Liabilities": {
|
||||
"Accounts Payable": {
|
||||
|
||||
@@ -72,6 +72,7 @@ def get():
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
_("Impairment"): {},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
@@ -104,6 +105,7 @@ def get():
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -208,8 +208,54 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-09-22 21:31:34.763977",
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payment Request",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payment Order",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Guarantee",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payroll Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Transaction",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Accounting",
|
||||
"link_doctype": "Payment Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Accounting",
|
||||
"link_doctype": "Journal Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Party",
|
||||
"link_doctype": "Customer",
|
||||
"link_fieldname": "default_bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Party",
|
||||
"link_doctype": "Supplier",
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-09-24 06:57:41.292970",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -246,4 +292,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "bank_account",
|
||||
"non_standard_fieldnames": {
|
||||
"Customer": "default_bank_account",
|
||||
"Supplier": "default_bank_account",
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Payments"),
|
||||
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
|
||||
},
|
||||
{"label": _("Party"), "items": ["Customer", "Supplier"]},
|
||||
{"items": ["Bank Guarantee"]},
|
||||
{"items": ["Journal Entry"]},
|
||||
],
|
||||
}
|
||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -96,8 +96,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -105,8 +108,18 @@ class BankClearance(Document):
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -6,16 +6,29 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
|
||||
|
||||
|
||||
class TestBankClearance(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_warehouse(
|
||||
warehouse_name="_Test Warehouse",
|
||||
properties={"parent_warehouse": "All Warehouses - _TC"},
|
||||
company="_Test Company",
|
||||
)
|
||||
create_item("_Test Item")
|
||||
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
|
||||
|
||||
clear_payment_entries()
|
||||
clear_loan_transactions()
|
||||
clear_pos_sales_invoices()
|
||||
make_bank_account()
|
||||
add_transactions()
|
||||
|
||||
@@ -83,11 +96,41 @@ class TestBankClearance(unittest.TestCase):
|
||||
bank_clearance.get_payment_entries()
|
||||
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||
|
||||
def test_update_clearance_date_on_si(self):
|
||||
sales_invoice = make_pos_sales_invoice()
|
||||
|
||||
date = getdate()
|
||||
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||
bank_clearance.from_date = add_months(date, -1)
|
||||
bank_clearance.to_date = date
|
||||
bank_clearance.include_pos_transactions = 1
|
||||
bank_clearance.get_payment_entries()
|
||||
|
||||
self.assertNotEqual(len(bank_clearance.payment_entries), 0)
|
||||
for payment in bank_clearance.payment_entries:
|
||||
if payment.payment_entry == sales_invoice.name:
|
||||
payment.clearance_date = date
|
||||
|
||||
bank_clearance.update_clearance_date()
|
||||
|
||||
si_clearance_date = frappe.db.get_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": sales_invoice.name, "account": bank_clearance.account},
|
||||
"clearance_date",
|
||||
)
|
||||
|
||||
self.assertEqual(si_clearance_date, date)
|
||||
|
||||
|
||||
def clear_payment_entries():
|
||||
frappe.db.delete("Payment Entry")
|
||||
|
||||
|
||||
def clear_pos_sales_invoices():
|
||||
frappe.db.delete("Sales Invoice", {"is_pos": 1})
|
||||
|
||||
|
||||
@if_lending_app_installed
|
||||
def clear_loan_transactions():
|
||||
for dt in [
|
||||
@@ -115,9 +158,45 @@ def add_transactions():
|
||||
|
||||
|
||||
def make_payment_entry():
|
||||
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
supplier = create_supplier(supplier_name="_Test Supplier")
|
||||
pi = make_purchase_invoice(
|
||||
supplier=supplier,
|
||||
supplier_warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
uom="Nos",
|
||||
qty=1,
|
||||
rate=690,
|
||||
)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
|
||||
def make_pos_sales_invoice():
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append(
|
||||
"accounts", {"company": "_Test Company", "default_account": "_Test Bank Clearance - _TC"}
|
||||
)
|
||||
mode_of_payment.save()
|
||||
|
||||
customer = make_customer(customer="_Test Customer")
|
||||
|
||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si.set("payments", [])
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
||||
)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
return si
|
||||
|
||||
@@ -47,9 +47,11 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 8:
|
||||
if no_of_columns != 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
_(
|
||||
"Columns are not according to template. Please compare the uploaded file with standard template"
|
||||
),
|
||||
title=(_("Wrong Template")),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Main Cost Center 2",
|
||||
"Main Cost Center 3",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
"Sub Cost Center 3",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
"Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
)
|
||||
|
||||
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
||||
@@ -120,7 +122,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
def test_valid_from_based_on_existing_gle(self):
|
||||
# GLE posted against Sub Cost Center 1 on today
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC",
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 1 - _TC",
|
||||
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
|
||||
jv.cancel()
|
||||
|
||||
def test_multiple_cost_center_allocation_on_same_main_cost_center(self):
|
||||
coa1 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40},
|
||||
valid_from=add_days(today(), -5),
|
||||
)
|
||||
|
||||
coa2 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50},
|
||||
valid_from=add_days(today(), -1),
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 3 - _TC",
|
||||
posting_date=today(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.cost_center, gle.debit, gle.credit)
|
||||
.where(gle.voucher_type == "Journal Entry")
|
||||
.where(gle.voucher_no == jv.name)
|
||||
.where(gle.account == "Sales - _TC")
|
||||
.orderby(gle.cost_center)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertTrue(gle.cost_center in expected_values)
|
||||
self.assertEqual(gle.debit, 0)
|
||||
self.assertEqual(gle.credit, expected_values[gle.cost_center])
|
||||
|
||||
coa1.cancel()
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "frankfurter.app/{transaction_date}"
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
|
||||
DOCTYPE = "Dunning Letter Text"
|
||||
FIELDS = ["body_text", "closing_text", "language"]
|
||||
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if not language:
|
||||
language = doc.get("language")
|
||||
|
||||
if language:
|
||||
filters = {"parent": dunning_type, "language": language}
|
||||
else:
|
||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
||||
letter_text = frappe.db.get_value(
|
||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
||||
)
|
||||
if letter_text:
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
|
||||
@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
},
|
||||
|
||||
make_jv: function (frm) {
|
||||
let revaluation_journal = null;
|
||||
let zero_balance_journal = null;
|
||||
frappe.call({
|
||||
method: "make_jv_entries",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: "Making Journal Entries...",
|
||||
freeze_message: __("Creating Journal Entries..."),
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let response = r.message;
|
||||
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
|
||||
frappe.msgprint(__("Journals have been created"));
|
||||
frappe.msgprint(__("Journal entries have been created"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
|
||||
accounts_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
row.exchange_rate = 1;
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
|
||||
// set difference
|
||||
if (doc.difference) {
|
||||
if (doc.difference > 0) {
|
||||
row.credit_in_account_currency = doc.difference;
|
||||
row.credit_in_account_currency = doc.difference / row.exchange_rate;
|
||||
row.credit = doc.difference;
|
||||
} else {
|
||||
row.debit_in_account_currency = -doc.difference;
|
||||
row.debit_in_account_currency = -doc.difference / row.exchange_rate;
|
||||
row.debit = -doc.difference;
|
||||
}
|
||||
}
|
||||
@@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, {
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
$.extend(d, r.message);
|
||||
erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn);
|
||||
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
|
||||
refresh_field("accounts");
|
||||
}
|
||||
@@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, {
|
||||
});
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
let length = frm.doc.accounts.length;
|
||||
if (row.idx != length) return;
|
||||
|
||||
let difference = frm.doc.accounts.reduce((total, row) => {
|
||||
if (row.idx == length) return total;
|
||||
|
||||
return total + row.debit - row.credit;
|
||||
}, 0);
|
||||
|
||||
if (difference) {
|
||||
if (difference > 0) {
|
||||
row.credit_in_account_currency = difference / row.exchange_rate;
|
||||
row.credit = difference;
|
||||
} else {
|
||||
row.debit_in_account_currency = -difference / row.exchange_rate;
|
||||
row.debit = -difference;
|
||||
}
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,6 +195,11 @@ class JournalEntry(AccountsController):
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||
return
|
||||
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
|
||||
@@ -515,6 +515,23 @@ class TestJournalEntry(unittest.TestCase):
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
def test_transaction_exchange_rate_on_journals(self):
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
|
||||
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD", "exchange_rate": 85})
|
||||
jv.submit()
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": jv.name, "is_cancelled": 0},
|
||||
fields=["account", "transaction_exchange_rate"],
|
||||
order_by="account",
|
||||
)
|
||||
expected = [
|
||||
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
|
||||
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
frm.refresh_fields();
|
||||
frm.page.clear_indicator();
|
||||
frm.dashboard.hide_progress();
|
||||
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
|
||||
|
||||
if (frm.doc.invoice_type == "Sales") {
|
||||
frappe.msgprint(__("Opening Sales Invoices have been created."));
|
||||
} else {
|
||||
frappe.msgprint(__("Opening Purchase Invoices have been created."));
|
||||
}
|
||||
},
|
||||
1500,
|
||||
data.title
|
||||
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||
frm.page.set_primary_action(__("Create Invoices"), () => {
|
||||
let btn_primary = frm.page.btn_primary.get(0);
|
||||
let freeze_message;
|
||||
if (frm.doc.invoice_type == "Sales") {
|
||||
freeze_message = __("Creating Sales Invoices ...");
|
||||
} else {
|
||||
freeze_message = __("Creating Purchase Invoices ...");
|
||||
}
|
||||
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
freeze_message: freeze_message,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -165,6 +173,42 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
return {
|
||||
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.add_fetch(
|
||||
"payment_request",
|
||||
"outstanding_amount",
|
||||
"payment_request_outstanding",
|
||||
"Payment Entry Reference"
|
||||
);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
@@ -191,6 +235,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
}
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -213,7 +258,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
hide_unhide_fields: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.toggle_display(
|
||||
@@ -280,7 +325,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.set_currency_labels(
|
||||
@@ -360,7 +405,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
payment_type: function (frm) {
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
@@ -395,6 +448,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +686,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
@@ -758,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
@@ -779,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type == "Receive")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
@@ -950,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
c.outstanding_amount = d.outstanding_amount;
|
||||
c.bill_no = d.bill_no;
|
||||
c.payment_term = d.payment_term;
|
||||
c.payment_term_outstanding = d.payment_term_outstanding;
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
@@ -999,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
frm.events.allocate_party_amount_against_ref_docs(
|
||||
frm,
|
||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
|
||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
|
||||
false
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1013,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return ["Sales Invoice", "Purchase Invoice"];
|
||||
},
|
||||
|
||||
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
|
||||
var total_positive_outstanding_including_order = 0;
|
||||
var total_negative_outstanding = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
|
||||
paid_amount -= total_deductions;
|
||||
|
||||
$.each(frm.doc.references || [], function (i, row) {
|
||||
if (flt(row.outstanding_amount) > 0)
|
||||
total_positive_outstanding_including_order += flt(row.outstanding_amount);
|
||||
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
|
||||
var allocated_negative_outstanding = 0;
|
||||
if (
|
||||
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
|
||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
|
||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
|
||||
) {
|
||||
if (total_positive_outstanding_including_order > paid_amount) {
|
||||
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
|
||||
allocated_negative_outstanding =
|
||||
total_negative_outstanding < remaining_outstanding
|
||||
? total_negative_outstanding
|
||||
: remaining_outstanding;
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
|
||||
if (paid_amount > total_negative_outstanding) {
|
||||
if (total_negative_outstanding == 0) {
|
||||
frappe.msgprint(
|
||||
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
|
||||
frm.doc.payment_type,
|
||||
frm.doc.party_type == "Customer" ? "to" : "from",
|
||||
frm.doc.party_type,
|
||||
])
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
|
||||
total_negative_outstanding,
|
||||
])
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
|
||||
allocated_negative_outstanding =
|
||||
paid_amount +
|
||||
(total_positive_outstanding_including_order < allocated_positive_outstanding
|
||||
? total_positive_outstanding_including_order
|
||||
: allocated_positive_outstanding);
|
||||
}
|
||||
}
|
||||
|
||||
$.each(frm.doc.references || [], function (i, row) {
|
||||
if (frappe.flags.allocate_payment_amount == 0) {
|
||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
||||
row.allocated_amount = 0;
|
||||
} else if (
|
||||
frappe.flags.allocate_payment_amount != 0 &&
|
||||
(!row.allocated_amount || paid_amount_change)
|
||||
) {
|
||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
||||
row.allocated_amount =
|
||||
row.outstanding_amount >= allocated_positive_outstanding
|
||||
? allocated_positive_outstanding
|
||||
: row.outstanding_amount;
|
||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
||||
row.allocated_amount =
|
||||
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
|
||||
? -1 * allocated_negative_outstanding
|
||||
: row.outstanding_amount;
|
||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.refresh_fields();
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
},
|
||||
|
||||
@@ -1647,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
return current_tax_amount;
|
||||
},
|
||||
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
const { matched_payment_requests } = frappe.last_response;
|
||||
if (!matched_payment_requests) return;
|
||||
|
||||
const COLUMN_LABEL = [
|
||||
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
|
||||
];
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Unset Matched Payment Request"),
|
||||
message: COLUMN_LABEL.concat(matched_payment_requests),
|
||||
as_table: true,
|
||||
wide: true,
|
||||
primary_action: {
|
||||
label: __("Allocate Payment Request"),
|
||||
action() {
|
||||
frappe.hide_msgprint();
|
||||
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
|
||||
frm.dirty();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Reference", {
|
||||
@@ -1739,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
frappe.ui.form.on("Payment Entry", {
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||
from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
@@ -37,7 +39,6 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -99,13 +100,17 @@ class PaymentEntry(AccountsController):
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
def before_save(self):
|
||||
self.set_matched_unset_payment_requests_to_response()
|
||||
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def set_liability_account(self):
|
||||
@@ -146,9 +151,21 @@ class PaymentEntry(AccountsController):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
liability_account = get_party_account(
|
||||
self.party_type, self.party, self.company, include_advance=True
|
||||
)[1]
|
||||
accounts = get_party_account(self.party_type, self.party, self.company, include_advance=True)
|
||||
|
||||
liability_account = accounts[1] if len(accounts) > 1 else None
|
||||
fieldname = (
|
||||
"default_advance_received_account"
|
||||
if self.party_type == "Customer"
|
||||
else "default_advance_paid_account"
|
||||
)
|
||||
|
||||
if not liability_account:
|
||||
throw(
|
||||
_("Please set default {0} in Company {1}").format(
|
||||
frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
self.set(self.party_account_field, liability_account)
|
||||
|
||||
@@ -177,30 +194,34 @@ class PaymentEntry(AccountsController):
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.set_payment_req_status()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def set_payment_req_status(self):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
||||
def update_payment_requests(self, cancel=False):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
update_payment_req_status(self, None)
|
||||
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
|
||||
|
||||
def update_outstanding_amounts(self):
|
||||
self.set_missing_ref_details(force=True)
|
||||
|
||||
def validate_duplicate_entry(self):
|
||||
reference_names = []
|
||||
reference_names = set()
|
||||
for d in self.get("references"):
|
||||
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
|
||||
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
|
||||
if key in reference_names:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
||||
d.idx, d.reference_doctype, d.reference_name
|
||||
)
|
||||
)
|
||||
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
|
||||
|
||||
reference_names.add(key)
|
||||
|
||||
def set_bank_account_data(self):
|
||||
if self.bank_account:
|
||||
@@ -226,6 +247,8 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.validate_allocated_amount_as_per_payment_request()
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
@@ -238,6 +261,27 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_as_per_payment_request(self):
|
||||
"""
|
||||
Allocated amount should not be greater than the outstanding amount of the Payment Request.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
|
||||
|
||||
if not pr_outstanding_amounts:
|
||||
return
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
|
||||
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
def term_based_allocation_enabled_for_reference(
|
||||
self, reference_doctype: str, reference_name: str
|
||||
) -> bool:
|
||||
@@ -1119,90 +1163,103 @@ class PaymentEntry(AccountsController):
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
else:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1603,6 +1660,380 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return current_tax_fraction
|
||||
|
||||
def set_matched_unset_payment_requests_to_response(self):
|
||||
"""
|
||||
Find matched Payment Requests for those references which have no Payment Request set.\n
|
||||
And set to `frappe.response` to show in the frontend for allocation.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
matched_payment_requests = get_matched_payment_request_of_references(
|
||||
[row for row in self.references if not row.payment_request]
|
||||
)
|
||||
|
||||
if not matched_payment_requests:
|
||||
return
|
||||
|
||||
frappe.response["matched_payment_requests"] = matched_payment_requests
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
|
||||
"""
|
||||
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
|
||||
:param paid_amount: Paid Amount / Received Amount.
|
||||
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
|
||||
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
if not allocate_payment_amount:
|
||||
for ref in self.references:
|
||||
ref.allocated_amount = 0
|
||||
return
|
||||
|
||||
# calculating outstanding amounts
|
||||
precision = self.precision("paid_amount")
|
||||
total_positive_outstanding_including_order = 0
|
||||
total_negative_outstanding = 0
|
||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||
|
||||
for ref in self.references:
|
||||
reference_outstanding_amount = ref.outstanding_amount
|
||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||
|
||||
if reference_outstanding_amount > 0:
|
||||
total_positive_outstanding_including_order += abs_outstanding_amount
|
||||
else:
|
||||
total_negative_outstanding += abs_outstanding_amount
|
||||
|
||||
# calculating allocated outstanding amounts
|
||||
allocated_negative_outstanding = 0
|
||||
allocated_positive_outstanding = 0
|
||||
|
||||
# checking party type and payment type
|
||||
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
|
||||
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
|
||||
):
|
||||
if total_positive_outstanding_including_order > paid_amount:
|
||||
remaining_outstanding = flt(
|
||||
total_positive_outstanding_including_order - paid_amount, precision
|
||||
)
|
||||
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
|
||||
allocated_negative_outstanding = paid_amount + min(
|
||||
total_positive_outstanding_including_order, allocated_positive_outstanding
|
||||
)
|
||||
|
||||
# inner function to set `allocated_amount` to those row which have no PR
|
||||
def _allocation_to_unset_pr_row(
|
||||
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
|
||||
):
|
||||
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - row.allocated_amount, precision
|
||||
)
|
||||
elif outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - abs(row.allocated_amount), precision
|
||||
)
|
||||
return allocated_positive_outstanding, allocated_negative_outstanding
|
||||
|
||||
# allocate amount based on `paid_amount` is changed or not
|
||||
if not paid_amount_change:
|
||||
for ref in self.references:
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
ref.outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
|
||||
|
||||
else:
|
||||
payment_request_outstanding_amounts = (
|
||||
get_payment_request_outstanding_set_in_references(self.references) or {}
|
||||
)
|
||||
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
|
||||
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = references_outstanding_amounts[key]
|
||||
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
|
||||
|
||||
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_positive_outstanding,
|
||||
reference_outstanding_amount,
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts)
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = ref.allocated_amount
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] = flt(
|
||||
remaining_references_allocated_amounts[key] - allocated_amount, precision
|
||||
)
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
|
||||
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_negative_outstanding,
|
||||
abs(reference_outstanding_amount),
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts) * -1
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = abs(ref.allocated_amount)
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = remaining_references_allocated_amounts[key]
|
||||
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
reference_outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_matched_payment_requests(self, matched_payment_requests):
|
||||
"""
|
||||
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
|
||||
:param matched_payment_requests: List of tuple of matched Payment Requests.
|
||||
|
||||
---
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not self.references or not matched_payment_requests:
|
||||
return
|
||||
|
||||
if isinstance(matched_payment_requests, str):
|
||||
matched_payment_requests = json.loads(matched_payment_requests)
|
||||
|
||||
# modify matched_payment_requests
|
||||
# like (reference_doctype, reference_name, allocated_amount): payment_request
|
||||
payment_requests = {}
|
||||
|
||||
for row in matched_payment_requests:
|
||||
key = tuple(row[:3])
|
||||
payment_requests[key] = row[3]
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
|
||||
|
||||
if key in payment_requests:
|
||||
ref.payment_request = payment_requests[key]
|
||||
del payment_requests[key] # to avoid duplicate allocation
|
||||
|
||||
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# to fetch matched rows
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.allocated_amount)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
# query to group by reference_doctype, reference_name, outstanding_amount
|
||||
subquery = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(
|
||||
PR.reference_doctype,
|
||||
PR.reference_name,
|
||||
PR.outstanding_amount.as_("allocated_amount"),
|
||||
PR.name.as_("payment_request"),
|
||||
Count("*").as_("count"),
|
||||
)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
)
|
||||
|
||||
# query to fetch matched rows which are single
|
||||
matched_prs = (
|
||||
frappe.qb.from_(subquery)
|
||||
.select(
|
||||
subquery.reference_doctype,
|
||||
subquery.reference_name,
|
||||
subquery.allocated_amount,
|
||||
subquery.payment_request,
|
||||
)
|
||||
.where(subquery.count == 1)
|
||||
.run()
|
||||
)
|
||||
|
||||
return matched_prs if matched_prs else None
|
||||
|
||||
|
||||
def get_references_outstanding_amount(references=None):
|
||||
"""
|
||||
Fetch accurate outstanding amount of `References`.\n
|
||||
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
|
||||
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
|
||||
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
|
||||
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
|
||||
|
||||
return {**refs_with_payment_term, **refs_without_payment_term}
|
||||
|
||||
|
||||
def get_outstanding_of_references_with_payment_term(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have `Payment Term` set.\n
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.payment_term)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.payment_term
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PS = frappe.qb.DocType("Payment Schedule")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PS)
|
||||
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
|
||||
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
|
||||
|
||||
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
outstanding_amounts = {}
|
||||
|
||||
for ref in references:
|
||||
if ref.payment_term:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, None)
|
||||
|
||||
if key not in outstanding_amounts:
|
||||
outstanding_amounts[key] = ref.outstanding_amount
|
||||
|
||||
return outstanding_amounts
|
||||
|
||||
|
||||
def get_payment_request_outstanding_set_in_references(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
|
||||
Example: {payment_request: outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
|
||||
|
||||
if not referenced_payment_requests:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.outstanding_amount)
|
||||
.where(PR.name.isin(referenced_payment_requests))
|
||||
).run()
|
||||
|
||||
return dict(response) if response else None
|
||||
|
||||
|
||||
def validate_inclusive_tax(tax, doc):
|
||||
def _on_previous_row_error(row_range):
|
||||
@@ -1749,7 +2180,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
@@ -2233,6 +2664,8 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
@@ -2382,9 +2815,179 @@ def get_payment_entry(
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
|
||||
|
||||
return pe
|
||||
|
||||
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
reference_payment_requests = {}
|
||||
|
||||
for row in response:
|
||||
key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
if key not in reference_payment_requests:
|
||||
reference_payment_requests[key] = {row.name: row.outstanding_amount}
|
||||
else:
|
||||
reference_payment_requests[key][row.name] = row.outstanding_amount
|
||||
|
||||
return reference_payment_requests
|
||||
|
||||
|
||||
def allocate_open_payment_requests_to_references(references=None, precision=None):
|
||||
"""
|
||||
Allocate unpaid Payment Requests to the references. \n
|
||||
---
|
||||
- Allocation based on below factors
|
||||
- Reference Allocated Amount
|
||||
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
|
||||
- Reference Payment Request's outstanding amount
|
||||
---
|
||||
- Allocation based on below scenarios
|
||||
- Reference's Allocated Amount == Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- This PR will not be allocated further
|
||||
- Reference's Allocated Amount < Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce the PR's outstanding amount by the allocated amount
|
||||
- This PR can be allocated further
|
||||
- Reference's Allocated Amount > Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce Allocated Amount of the reference by the PR's outstanding amount
|
||||
- Create a new row for the remaining amount until the Allocated Amount is 0
|
||||
- Allocate PR if available
|
||||
---
|
||||
- Note:
|
||||
- Priority is given to the first Payment Request of respective references.
|
||||
- Single Reference can have multiple rows.
|
||||
- With Payment Terms or without Payment Terms
|
||||
- With Payment Request or without Payment Request
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# get all unpaid payment requests for the references
|
||||
references_open_payment_requests = get_open_payment_requests_for_references(references)
|
||||
|
||||
if not references_open_payment_requests:
|
||||
return
|
||||
|
||||
if not precision:
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
# to manage new rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
reference_key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
# unpaid payment requests for the reference
|
||||
reference_payment_requests = references_open_payment_requests.get(reference_key)
|
||||
|
||||
if not reference_payment_requests:
|
||||
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
|
||||
continue
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
|
||||
allocated_amount = row.allocated_amount
|
||||
|
||||
# allocate the payment request to the reference and PR's outstanding amount
|
||||
row.payment_request = payment_request
|
||||
|
||||
if pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
# reduce the outstanding amount of the payment request
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
else:
|
||||
# split the reference row to allocate the remaining amount
|
||||
del reference_payment_requests[payment_request]
|
||||
row.allocated_amount = pr_outstanding_amount
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
|
||||
# set the remaining amount to the next row
|
||||
while allocated_amount:
|
||||
# create a new row for the remaining amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(
|
||||
iter(reference_payment_requests.items()), (None, None)
|
||||
)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = payment_request
|
||||
new_row.allocated_amount = min(
|
||||
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
|
||||
)
|
||||
|
||||
if not payment_request or not pr_outstanding_amount:
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
else:
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
|
||||
def update_accounting_dimensions(pe, doc):
|
||||
"""
|
||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||
|
||||
@@ -1791,6 +1791,79 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
# 'Is Opening' should always be 'No' for normal advance payments
|
||||
self.assertEqual(gl_with_opening_set, [])
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_delete_linked_exchange_gain_loss_journal(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=83.970000000,
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# create a payment entry for the invoice
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 100
|
||||
pe.source_exchange_rate = 90
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 2710,
|
||||
},
|
||||
)
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1},
|
||||
pluck="parent",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
|
||||
# check cancellation of payment entry and journal entry
|
||||
pe.cancel()
|
||||
self.assertTrue(pe.docstatus == 2)
|
||||
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
|
||||
|
||||
# check deletion of payment entry and journal entry
|
||||
pe.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"payment_term_outstanding",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"column_break_4",
|
||||
@@ -18,7 +19,9 @@
|
||||
"allocated_amount",
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss",
|
||||
"account"
|
||||
"account",
|
||||
"payment_request",
|
||||
"payment_request_outstanding"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -120,12 +123,33 @@
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Request",
|
||||
"options": "Payment Request"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_term",
|
||||
"fieldname": "payment_term_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"label": "Payment Term Outstanding",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
|
||||
"fieldname": "payment_request_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"is_virtual": 1,
|
||||
"label": "Payment Request Outstanding",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-05 09:44:08.310593",
|
||||
"modified": "2024-09-16 18:11:50.019343",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_request: DF.Link | None
|
||||
payment_request_outstanding: DF.Float
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@property
|
||||
def payment_request_outstanding(self):
|
||||
if not self.payment_request:
|
||||
return
|
||||
|
||||
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
@@ -1845,6 +1846,78 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
def test_reconciliation_on_closed_period_payment(self):
|
||||
# create backdated fiscal year
|
||||
first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)")
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
create_fiscal_year(
|
||||
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
|
||||
)
|
||||
|
||||
# make journal entry for previous year
|
||||
je_1 = frappe.new_doc("Journal Entry")
|
||||
je_1.posting_date = add_days(prev_fy_start_date, 20)
|
||||
je_1.company = self.company
|
||||
je_1.user_remark = "test"
|
||||
je_1.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 1000,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"cost_center": self.sub_cc.name,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit_in_account_currency": 500,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"cost_center": self.sub_cc.name,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit_in_account_currency": 500,
|
||||
},
|
||||
],
|
||||
)
|
||||
je_1.submit()
|
||||
|
||||
# make period closing voucher
|
||||
pcv = make_period_closing_voucher(
|
||||
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||
)
|
||||
pcv.reload()
|
||||
# check if period closing voucher is completed
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
|
||||
# make journal entry for active year
|
||||
je_2 = self.create_journal_entry(
|
||||
acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today()
|
||||
)
|
||||
je_2.accounts[0].party_type = "Customer"
|
||||
je_2.accounts[0].party = self.customer
|
||||
je_2.submit()
|
||||
|
||||
# process reconciliation on closed period payment
|
||||
pr = self.create_payment_reconciliation(party_is_customer=True)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
je_1.reload()
|
||||
je_2.reload()
|
||||
|
||||
# check whether the payment reconciliation is done on the closed period
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
@@ -1872,3 +1945,61 @@ def make_supplier(supplier_name, currency=None):
|
||||
return supplier.name
|
||||
else:
|
||||
return supplier_name
|
||||
|
||||
|
||||
def create_fiscal_year(company, year_start_date, year_end_date):
|
||||
fy_docname = frappe.db.exists(
|
||||
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}
|
||||
)
|
||||
if not fy_docname:
|
||||
fy_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}",
|
||||
"year_start_date": year_start_date,
|
||||
"year_end_date": year_end_date,
|
||||
"companies": [{"company": company}],
|
||||
}
|
||||
).save()
|
||||
return fy_doc
|
||||
else:
|
||||
fy_doc = frappe.get_doc("Fiscal Year", fy_docname)
|
||||
if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}):
|
||||
fy_doc.append("companies", {"company": company})
|
||||
fy_doc.save()
|
||||
return fy_doc
|
||||
|
||||
|
||||
def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
parent_account = frappe.db.get_value(
|
||||
"Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name"
|
||||
)
|
||||
surplus_account = create_account(
|
||||
account_name="Reserve and Surplus",
|
||||
is_group=0,
|
||||
company=company,
|
||||
root_type="Liability",
|
||||
report_type="Balance Sheet",
|
||||
account_currency="INR",
|
||||
parent_account=parent_account,
|
||||
doctype="Account",
|
||||
)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"company": company,
|
||||
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
if submit:
|
||||
pcv.submit()
|
||||
|
||||
return pcv
|
||||
|
||||
@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
}
|
||||
|
||||
if (
|
||||
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
|
||||
frm.doc.status == "Initiated"
|
||||
frm.doc.payment_request_type == "Outward" &&
|
||||
["Initiated", "Partially Paid"].includes(frm.doc.status)
|
||||
) {
|
||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||
frappe.call({
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"transaction_date",
|
||||
"column_break_2",
|
||||
"naming_series",
|
||||
"company",
|
||||
"mode_of_payment",
|
||||
"party_details",
|
||||
"party_type",
|
||||
@@ -18,9 +19,11 @@
|
||||
"reference_name",
|
||||
"transaction_details",
|
||||
"grand_total",
|
||||
"currency",
|
||||
"is_a_subscription",
|
||||
"column_break_18",
|
||||
"currency",
|
||||
"outstanding_amount",
|
||||
"party_account_currency",
|
||||
"subscription_section",
|
||||
"subscription_plans",
|
||||
"bank_account_details",
|
||||
@@ -68,6 +71,7 @@
|
||||
{
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_preview": 1,
|
||||
"label": "Transaction Date"
|
||||
},
|
||||
{
|
||||
@@ -132,7 +136,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "reference_doctype",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_details",
|
||||
@@ -140,12 +145,14 @@
|
||||
"label": "Transaction Details"
|
||||
},
|
||||
{
|
||||
"description": "Amount in customer's currency",
|
||||
"description": "Amount in transaction currency",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -390,13 +397,38 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.docstatus === 1",
|
||||
"description": "Amount in party's bank account currency",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Outstanding Amount",
|
||||
"non_negative": 1,
|
||||
"options": "party_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Party Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-20 13:54:55.245774",
|
||||
"modified": "2024-09-16 17:50:54.440090",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -431,6 +463,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -3,9 +3,11 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
@@ -15,7 +17,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
@@ -45,6 +47,7 @@ class PaymentRequest(Document):
|
||||
bank_account: DF.Link | None
|
||||
bank_account_no: DF.ReadOnly | None
|
||||
branch_code: DF.ReadOnly | None
|
||||
company: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
email_to: DF.Data | None
|
||||
@@ -56,16 +59,18 @@ class PaymentRequest(Document):
|
||||
mode_of_payment: DF.Link | None
|
||||
mute_email: DF.Check
|
||||
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
||||
outstanding_amount: DF.Currency
|
||||
party: DF.DynamicLink | None
|
||||
party_account_currency: DF.Link | None
|
||||
party_type: DF.Link | None
|
||||
payment_account: DF.ReadOnly | None
|
||||
payment_channel: DF.Literal["", "Email", "Phone"]
|
||||
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
|
||||
payment_gateway: DF.ReadOnly | None
|
||||
payment_gateway_account: DF.Link | None
|
||||
payment_order: DF.Link | None
|
||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||
payment_url: DF.Data | None
|
||||
print_format: DF.Literal
|
||||
print_format: DF.Literal[None]
|
||||
project: DF.Link | None
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
@@ -99,6 +104,12 @@ class PaymentRequest(Document):
|
||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||
|
||||
def validate_payment_request_amount(self):
|
||||
if self.grand_total == 0:
|
||||
frappe.throw(
|
||||
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
|
||||
title=_("Invalid Amount"),
|
||||
)
|
||||
|
||||
existing_payment_request_amount = flt(
|
||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||
)
|
||||
@@ -146,6 +157,28 @@ class PaymentRequest(Document):
|
||||
).format(self.grand_total, amount)
|
||||
)
|
||||
|
||||
def before_submit(self):
|
||||
if (
|
||||
self.currency != self.party_account_currency
|
||||
and self.party_account_currency == get_company_currency(self.company)
|
||||
):
|
||||
# set outstanding amount in party account currency
|
||||
invoice = frappe.get_value(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
|
||||
as_dict=1,
|
||||
)
|
||||
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
|
||||
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
|
||||
self.outstanding_amount = flt(
|
||||
self.grand_total / grand_total * base_grand_total,
|
||||
self.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
else:
|
||||
self.outstanding_amount = self.grand_total
|
||||
|
||||
def on_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
@@ -274,7 +307,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def set_as_paid(self):
|
||||
if self.payment_channel == "Phone":
|
||||
self.db_set("status", "Paid")
|
||||
self.db_set({"status": "Paid", "outstanding_amount": 0})
|
||||
|
||||
else:
|
||||
payment_entry = self.create_payment_entry()
|
||||
@@ -295,26 +328,32 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
party_account_currency = (
|
||||
self.get("party_account_currency")
|
||||
or ref_doc.get("party_account_currency")
|
||||
or get_account_currency(party_account)
|
||||
)
|
||||
|
||||
party_amount = bank_amount = self.outstanding_amount
|
||||
|
||||
bank_amount = self.grand_total
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
exchange_rate = ref_doc.get("conversion_rate")
|
||||
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
|
||||
|
||||
# outstanding amount is already in Part's account currency
|
||||
payment_entry = get_payment_entry(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
party_amount=party_amount,
|
||||
bank_account=self.payment_account,
|
||||
bank_amount=bank_amount,
|
||||
created_from_payment_request=True,
|
||||
)
|
||||
|
||||
payment_entry.update(
|
||||
{
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_no": self.name, # to prevent validation error
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||
self.reference_doctype, self.reference_name, self.name
|
||||
@@ -322,6 +361,9 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
|
||||
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
|
||||
|
||||
# Update dimensions
|
||||
payment_entry.update(
|
||||
{
|
||||
@@ -330,13 +372,16 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
self.payment_request_type == "Outward"
|
||||
and payment_entry.paid_from_account_currency == ref_doc.company_currency
|
||||
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
|
||||
):
|
||||
payment_entry.paid_amount = payment_entry.base_paid_amount = (
|
||||
payment_entry.target_exchange_rate * payment_entry.received_amount
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
@@ -417,6 +462,62 @@ class PaymentRequest(Document):
|
||||
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
|
||||
def _allocate_payment_request_to_pe_references(self, references):
|
||||
"""
|
||||
Allocate the Payment Request to the Payment Entry references based on\n
|
||||
- Allocated Amount.
|
||||
- Outstanding Amount of Payment Request.\n
|
||||
Payment Request is doc itself and references are the rows of Payment Entry.
|
||||
"""
|
||||
if len(references) == 1:
|
||||
references[0].payment_request = self.name
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
outstanding_amount = self.outstanding_amount
|
||||
|
||||
# to manage rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
NEW_ROW_ADDED = False
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
if outstanding_amount == 0:
|
||||
if not NEW_ROW_ADDED:
|
||||
break
|
||||
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
continue
|
||||
|
||||
# allocate the payment request to the row
|
||||
row.payment_request = self.name
|
||||
|
||||
if row.allocated_amount <= outstanding_amount:
|
||||
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
else:
|
||||
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
|
||||
row.allocated_amount = outstanding_amount
|
||||
outstanding_amount = 0
|
||||
|
||||
# create a new row without PR for remaining unallocated amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = None
|
||||
new_row.allocated_amount = remaining_allocated_amount
|
||||
|
||||
NEW_ROW_ADDED = True
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def make_payment_request(**args):
|
||||
@@ -447,11 +548,15 @@ def make_payment_request(**args):
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
)
|
||||
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Request is already created"))
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
@@ -465,6 +570,13 @@ def make_payment_request(**args):
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
party_type = args.get("party_type") or "Customer"
|
||||
party_account_currency = ref_doc.get("party_account_currency")
|
||||
|
||||
if not party_account_currency:
|
||||
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -473,6 +585,7 @@ def make_payment_request(**args):
|
||||
"payment_channel": gateway_account.get("payment_channel"),
|
||||
"payment_request_type": args.get("payment_request_type"),
|
||||
"currency": ref_doc.currency,
|
||||
"party_account_currency": party_account_currency,
|
||||
"grand_total": grand_total,
|
||||
"mode_of_payment": args.mode_of_payment,
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
@@ -480,7 +593,8 @@ def make_payment_request(**args):
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": party_type,
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
@@ -503,6 +617,8 @@ def make_payment_request(**args):
|
||||
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
if pr.get("__unsaved"):
|
||||
pr.insert(ignore_permissions=True)
|
||||
pr.submit()
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
@@ -524,9 +640,11 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
grand_total = flt(
|
||||
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
)
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -541,31 +659,27 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
"""
|
||||
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
||||
and get the summation of existing paid payment request for Phone payment channel.
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
existing_payment_request_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(grand_total)
|
||||
from `tabPayment Request`
|
||||
where
|
||||
reference_doctype = %s
|
||||
and reference_name = %s
|
||||
and docstatus = 1
|
||||
and (status != 'Paid'
|
||||
or (payment_channel = 'Phone'
|
||||
and status = 'Paid'))
|
||||
""",
|
||||
(ref_dt, ref_dn),
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(Sum(PR.grand_total))
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.run()
|
||||
)
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
@@ -612,41 +726,66 @@ def make_payment_entry(docname):
|
||||
return doc.create_payment_entry(submit=False).as_dict()
|
||||
|
||||
|
||||
def update_payment_req_status(doc, method):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
|
||||
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
"""
|
||||
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
for ref in doc.references:
|
||||
payment_request_name = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": ref.reference_doctype,
|
||||
"reference_name": ref.reference_name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
fields=[
|
||||
"name",
|
||||
"grand_total",
|
||||
"outstanding_amount",
|
||||
"payment_request_type",
|
||||
],
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
payment_request = referenced_payment_requests[ref.payment_request]
|
||||
pr_outstanding = payment_request["outstanding_amount"]
|
||||
|
||||
# update outstanding amount
|
||||
new_outstanding_amount = flt(
|
||||
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
|
||||
precision,
|
||||
)
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
# to handle same payment request for the multiple allocations
|
||||
payment_request["outstanding_amount"] = new_outstanding_amount
|
||||
|
||||
if not cancel and new_outstanding_amount < 0:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
|
||||
).format(ref.payment_request),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
if status != "Paid" and not ref_details.outstanding_amount:
|
||||
status = "Paid"
|
||||
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
|
||||
status = "Partially Paid"
|
||||
elif ref_details.outstanding_amount == ref_details.total_amount:
|
||||
if pay_req_doc.payment_request_type == "Outward":
|
||||
status = "Initiated"
|
||||
elif pay_req_doc.payment_request_type == "Inward":
|
||||
status = "Requested"
|
||||
# update status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
status = "Paid"
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
pay_req_doc.db_set("status", status)
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
@@ -730,3 +869,35 @@ def validate_payment(doc, method=None):
|
||||
doc.reference_docname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
return []
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
return [
|
||||
(
|
||||
pr.name,
|
||||
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||
)
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||
|
||||
|
||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||
|
||||
payment_method = [
|
||||
@@ -32,7 +37,7 @@ payment_method = [
|
||||
]
|
||||
|
||||
|
||||
class TestPaymentRequest(unittest.TestCase):
|
||||
class TestPaymentRequest(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
@@ -260,3 +265,262 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
# Try to make Payment Request more than SO amount, should give validation
|
||||
pr2.grand_total = 900
|
||||
self.assertRaises(frappe.ValidationError, pr2.save)
|
||||
|
||||
def test_conversion_on_foreign_currency_accounts(self):
|
||||
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
|
||||
po_doc.conversion_rate = 80
|
||||
po_doc.items[0].qty = 1
|
||||
po_doc.items[0].rate = 10
|
||||
po_doc.save().submit()
|
||||
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
def test_multiple_payment_if_partially_paid_for_same_currency(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 800)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
|
||||
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
|
||||
pi.credit_to = "Creditors - _TC"
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 100 USD -> 5000 INR
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
self.assertEqual(pr.outstanding_amount, 5000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 2000
|
||||
pe.references[0].allocated_amount = 2000
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 3000)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 3000)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
def test_single_payment_with_payment_term_for_same_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
|
||||
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Order",
|
||||
dn=po.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 20000)
|
||||
|
||||
# check 1st payment term
|
||||
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_single_payment_with_payment_term_for_multi_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
|
||||
)
|
||||
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=si.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 200 USD -> 10000 INR
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
self.assertEqual(pr.outstanding_amount, 10000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 10000)
|
||||
|
||||
# check 1st payment term
|
||||
# convert it via dollar and conversion_rate
|
||||
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
|
||||
def test_payment_cancel_process(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 800
|
||||
pe.references[0].allocated_amount = 800
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 200)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# cancelling PE
|
||||
pe.cancel()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Requested")
|
||||
self.assertEqual(pr.outstanding_amount, 1000)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
@@ -60,7 +60,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
)
|
||||
if gle_count > 5000:
|
||||
frappe.enqueue(
|
||||
make_reverse_gl_entries,
|
||||
process_cancellation,
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
@@ -71,9 +71,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
self.delete_closing_entries()
|
||||
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
def validate_future_closing_vouchers(self):
|
||||
if frappe.db.exists(
|
||||
@@ -86,12 +84,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
def delete_closing_entries(self):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == self.name
|
||||
).run()
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
@@ -166,14 +158,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if len(gl_entries + closing_entries) > 3000:
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
voucher_name=self.name,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.enqueue(
|
||||
process_closing_entries,
|
||||
process_gl_and_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
@@ -187,8 +172,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries, self.name)
|
||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
process_gl_and_closing_entries(
|
||||
gl_entries, closing_entries, self.name, self.company, self.posting_date
|
||||
)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
@@ -353,9 +339,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
if get_opening_entries:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
| gl_entry.is_opening
|
||||
== "Yes"
|
||||
( # noqa: UP034
|
||||
(gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date))
|
||||
| (gl_entry.is_opening == "Yes")
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
@@ -373,12 +360,16 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, voucher_name):
|
||||
def process_gl_and_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -386,26 +377,21 @@ def process_gl_entries(gl_entries, voucher_name):
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
try:
|
||||
if gl_entries + closing_entries:
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
def process_cancellation(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||
delete_closing_entries(voucher_no)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def delete_closing_entries(voucher_no):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == voucher_no
|
||||
).run()
|
||||
|
||||
@@ -194,7 +194,9 @@ function refresh_payments(d, frm) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
if (payment.closing_amount === 0) {
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
}
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
|
||||
@@ -40,6 +40,19 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("item_code", "items", function (doc) {
|
||||
return {
|
||||
query: "erpnext.accounts.doctype.pos_invoice.pos_invoice.item_query",
|
||||
filters: {
|
||||
has_variants: ["=", 0],
|
||||
is_sales_item: ["=", 1],
|
||||
disabled: ["=", 0],
|
||||
is_fixed_asset: ["=", 0],
|
||||
pos_profile: ["=", doc.pos_profile],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
@@ -15,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_multi_mode_option,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
@@ -188,7 +190,7 @@ class POSInvoice(SalesInvoice):
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
)
|
||||
|
||||
# run on validate method of selling controller
|
||||
@@ -449,7 +451,7 @@ class POSInvoice(SalesInvoice):
|
||||
if self.is_return and entry.amount > 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||
|
||||
if self.is_return:
|
||||
if self.is_return and self.docstatus != 0:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
@@ -837,3 +839,30 @@ def add_return_modes(doc, pos_profile):
|
||||
]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
if pos_profile := filters.get("pos_profile")[1]:
|
||||
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
|
||||
if item_groups := get_item_group(pos_profile):
|
||||
filters["item_group"] = ["in", tuple(item_groups)]
|
||||
|
||||
del filters["pos_profile"]
|
||||
|
||||
else:
|
||||
filters.pop("pos_profile", None)
|
||||
|
||||
return _item_query(doctype, txt, searchfield, start, page_len, filters, as_dict)
|
||||
|
||||
|
||||
def get_item_group(pos_profile):
|
||||
item_groups = []
|
||||
if pos_profile.get("item_groups"):
|
||||
# Get items based on the item groups defined in the POS profile
|
||||
for row in pos_profile.get("item_groups"):
|
||||
item_groups.append(row.item_group)
|
||||
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg = _(
|
||||
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||
msg += " "
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
"The original invoice should be consolidated before or along with the return invoice."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
||||
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -419,7 +419,8 @@
|
||||
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate"
|
||||
"label": "Rate",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -647,7 +648,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -709,4 +710,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,8 @@ class PricingRule(Document):
|
||||
if not self.priority:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||
frappe.bold(_("Priority")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,7 +195,7 @@ class PricingRule(Document):
|
||||
throw(
|
||||
_(
|
||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
||||
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
class TestPricingRule(unittest.TestCase):
|
||||
class TestPricingRule(FrappeTestCase):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
@@ -1128,6 +1131,12 @@ class TestPricingRule(unittest.TestCase):
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
|
||||
so.items[0].qty = 1
|
||||
del so.items[-1]
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
@@ -1311,6 +1320,69 @@ class TestPricingRule(unittest.TestCase):
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
@@ -657,6 +657,9 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
"qty": qty,
|
||||
|
||||
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_advance_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
|
||||
root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company(frm) {
|
||||
frm.set_value("party", "");
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
},
|
||||
party_type(frm) {
|
||||
frm.set_value("party", "");
|
||||
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
|
||||
party(frm) {
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
include_advance: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
if (typeof r.message === "string") {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
frm.set_value("receivable_payable_account", r.message[0]);
|
||||
frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"column_break_io6c",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"filter_section",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
@@ -141,12 +142,23 @@
|
||||
{
|
||||
"fieldname": "section_break_a8yx",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-11 10:56:51.699137",
|
||||
"modified": "2024-08-27 14:48:56.715320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -180,4 +192,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
||||
bank_cash_account: DF.Link | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
default_advance_account: DF.Link
|
||||
error_log: DF.LongText | None
|
||||
from_invoice_date: DF.Date | None
|
||||
from_payment_date: DF.Date | None
|
||||
@@ -101,6 +102,7 @@ def get_pr_instance(doc: str):
|
||||
"party_type",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"ignore_cr_dr_notes",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -383,10 +384,16 @@
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
@@ -133,6 +134,9 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.ignore_cr_dr_notes:
|
||||
filters.update({"ignore_cr_dr_notes": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
col, res = get_soa(filters)
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Promotional Scheme", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("for_price_list", "price_discount_slabs", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
selling: doc.selling,
|
||||
buying: doc.buying,
|
||||
currency: doc.currency,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.trigger("toggle_reqd_apply_on");
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
pricing_rule_fields = [
|
||||
"apply_on",
|
||||
@@ -51,6 +53,7 @@ price_discount_fields = [
|
||||
"discount_percentage",
|
||||
"validate_applied_rule",
|
||||
"apply_multiple_pricing_rules",
|
||||
"for_price_list",
|
||||
]
|
||||
|
||||
product_discount_fields = [
|
||||
@@ -63,6 +66,7 @@ product_discount_fields = [
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
"round_free_qty",
|
||||
]
|
||||
|
||||
|
||||
@@ -160,22 +164,50 @@ class PromotionalScheme(Document):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
transaction_exists = False
|
||||
docnames = []
|
||||
invalid_pricing_rule = self.get_invalid_pricing_rules()
|
||||
|
||||
# If user has changed applicable for
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
if not invalid_pricing_rule:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
if frappe.db.exists(
|
||||
"Pricing Rule Detail",
|
||||
{
|
||||
"pricing_rule": ["in", invalid_pricing_rule],
|
||||
"docstatus": ["<", 2],
|
||||
},
|
||||
):
|
||||
raise_for_transaction_exists(self.name)
|
||||
|
||||
for docname in docnames:
|
||||
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
|
||||
raise_for_transaction_exists(self.name)
|
||||
for doc in invalid_pricing_rule:
|
||||
frappe.delete_doc("Pricing Rule", doc)
|
||||
|
||||
if docnames and not transaction_exists:
|
||||
for docname in docnames:
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
frappe.msgprint(
|
||||
_("The following invalid Pricing Rules are deleted:")
|
||||
+ "<br><br><ul><li>"
|
||||
+ "</li><li>".join(invalid_pricing_rule)
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
def get_invalid_pricing_rules(self):
|
||||
pr = frappe.qb.DocType("Pricing Rule")
|
||||
conditions = []
|
||||
conditions.append(pr.promotional_scheme == self.name)
|
||||
|
||||
if self.applicable_for:
|
||||
applicable_for = frappe.scrub(self.applicable_for)
|
||||
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
|
||||
|
||||
conditions.append(
|
||||
(IfNull(pr.applicable_for, "") != self.applicable_for)
|
||||
| (
|
||||
(IfNull(pr.applicable_for, "") == self.applicable_for)
|
||||
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
|
||||
)
|
||||
)
|
||||
else:
|
||||
conditions.append(IfNull(pr.applicable_for, "") != "")
|
||||
|
||||
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
|
||||
@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_change_applicable_for_values_in_promotional_scheme(self):
|
||||
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
|
||||
)
|
||||
self.assertTrue(len(price_rules), 2)
|
||||
|
||||
ps.set("customer", [])
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule",
|
||||
filters={
|
||||
"promotional_scheme": ps.name,
|
||||
"applicable_for": "Customer",
|
||||
"customer": "_Test Customer",
|
||||
},
|
||||
)
|
||||
self.assertEqual(price_rules, [])
|
||||
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||
|
||||
def test_min_max_amount_configuration(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.price_discount_slabs[0].min_amount = 10
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"rate",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_11",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -120,6 +121,13 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.rate_or_discount!=\"Rate\"",
|
||||
"fieldname": "for_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -169,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-16 00:25:33.843996",
|
||||
"modified": "2024-07-23 12:33:46.574950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
@@ -177,4 +185,4 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class PromotionalSchemePriceDiscount(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
for_price_list: DF.Link | None
|
||||
max_amount: DF.Currency
|
||||
max_qty: DF.Float
|
||||
min_amount: DF.Currency
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"column_break_9",
|
||||
"free_item_uom",
|
||||
"free_item_rate",
|
||||
"round_free_qty",
|
||||
"section_break_12",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -181,12 +182,18 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-12 12:53:58.199108",
|
||||
"modified": "2024-07-22 17:25:07.880984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
@@ -195,4 +202,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
round_free_qty: DF.Check
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -561,11 +561,12 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm.custom_make_buttons = {
|
||||
"Purchase Invoice": "Return / Debit Note",
|
||||
"Payment Entry": "Payment",
|
||||
"Landed Cost Voucher": function () {
|
||||
frm.trigger("create_landed_cost_voucher");
|
||||
},
|
||||
};
|
||||
|
||||
if (frm.doc.update_stock) {
|
||||
frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher";
|
||||
}
|
||||
|
||||
frm.set_query("additional_discount_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -607,20 +608,6 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
});
|
||||
},
|
||||
|
||||
create_landed_cost_voucher: function (frm) {
|
||||
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
|
||||
lcv.company = frm.doc.company;
|
||||
|
||||
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice");
|
||||
lcv_receipt.receipt_document_type = "Purchase Invoice";
|
||||
lcv_receipt.receipt_document = frm.doc.name;
|
||||
lcv_receipt.supplier = frm.doc.supplier;
|
||||
lcv_receipt.grand_total = frm.doc.grand_total;
|
||||
lcv.purchase_receipts = [lcv_receipt];
|
||||
|
||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
||||
},
|
||||
|
||||
add_custom_buttons: function (frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(
|
||||
@@ -645,14 +632,40 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
|
||||
frm.add_custom_button(
|
||||
__("Landed Cost Voucher"),
|
||||
() => {
|
||||
frm.events.make_lcv(frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
make_lcv(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
|
||||
args: {
|
||||
doctype: frm.doc.doctype,
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__onload && frm.is_new()) {
|
||||
if (frm.doc.supplier) {
|
||||
if (frm.doc.__onload && frm.doc.supplier) {
|
||||
if (frm.is_new()) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.enable_apply_tds) {
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,6 +1271,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1630,7 +1631,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -285,7 +285,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.create_remarks()
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
validate_inter_company_party(
|
||||
@@ -322,10 +321,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def create_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.bill_no and self.bill_date:
|
||||
self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
|
||||
self.bill_no, formatdate(self.bill_date)
|
||||
)
|
||||
if self.bill_no:
|
||||
self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no)
|
||||
if self.bill_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
@@ -346,22 +346,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
||||
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
||||
po = qb.DocType("Purchase Order")
|
||||
po_with_tds = (
|
||||
qb.from_(po)
|
||||
.select(po.name)
|
||||
.where(
|
||||
po.docstatus.eq(1)
|
||||
& (po.name.isin(purchase_orders))
|
||||
& (po.apply_tds.eq(1))
|
||||
& (po.tax_withholding_category.notnull())
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
@@ -377,16 +361,16 @@ class PurchaseInvoice(BuyingController):
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold("Credit To")),
|
||||
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold(_("Credit To"))),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
||||
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
@@ -634,7 +618,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Order Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
@@ -655,7 +639,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Receipt Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
@@ -747,6 +731,9 @@ class PurchaseInvoice(BuyingController):
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def before_submit(self):
|
||||
self.create_remarks()
|
||||
|
||||
def on_submit(self):
|
||||
super().on_submit()
|
||||
|
||||
@@ -1262,7 +1249,11 @@ class PurchaseInvoice(BuyingController):
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
|
||||
@@ -2236,6 +2236,80 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 6300
|
||||
pr.plc_conversion_rate = 1
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.conversion_rate, 6300)
|
||||
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=3000,
|
||||
distribute_charges_based_on="Qty",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 1.1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_last_purchase_rate(self):
|
||||
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
|
||||
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 200)
|
||||
|
||||
pi2.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 100)
|
||||
|
||||
pi1.cancel()
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -958,12 +959,22 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -278,7 +278,6 @@ class SalesInvoice(SellingController):
|
||||
self.check_sales_order_on_hold_or_close("sales_order")
|
||||
self.validate_debit_to_acc()
|
||||
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
|
||||
self.add_remarks()
|
||||
self.validate_fixed_asset()
|
||||
self.set_income_account_for_fixed_assets()
|
||||
self.validate_item_cost_centers()
|
||||
@@ -341,6 +340,7 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
self.allow_write_off_only_on_pos()
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_accounts(self):
|
||||
@@ -422,6 +422,9 @@ class SalesInvoice(SellingController):
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
|
||||
def before_submit(self):
|
||||
self.add_remarks()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_pos_paid_amount()
|
||||
|
||||
@@ -514,7 +517,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
@@ -858,7 +861,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = (
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||
+ " "
|
||||
)
|
||||
msg += _(
|
||||
@@ -869,7 +872,7 @@ class SalesInvoice(SellingController):
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = (
|
||||
_("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
||||
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||
)
|
||||
+ " "
|
||||
)
|
||||
@@ -946,10 +949,11 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
self.remarks = _("Against Customer Order {0} dated {1}").format(
|
||||
self.po_no, formatdate(self.po_date)
|
||||
)
|
||||
if self.po_no:
|
||||
self.remarks = _("Against Customer Order {0}").format(self.po_no)
|
||||
if self.po_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
@@ -1018,6 +1022,10 @@ class SalesInvoice(SellingController):
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def allow_write_off_only_on_pos(self):
|
||||
if not self.is_pos and self.write_off_account:
|
||||
self.write_off_account = None
|
||||
|
||||
def validate_write_off_account(self):
|
||||
if flt(self.write_off_amount) and not self.write_off_account:
|
||||
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
|
||||
@@ -1316,6 +1324,10 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
@@ -1374,37 +1386,33 @@ class SalesInvoice(SellingController):
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -1479,6 +1487,10 @@ class SalesInvoice(SellingController):
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1492,7 +1504,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -2111,7 +2123,7 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
"doctype": "Sales Team",
|
||||
"field_map": {"incentives": "incentives"},
|
||||
|
||||
@@ -5,9 +5,10 @@ import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -3083,6 +3084,128 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
cust_doc.save()
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||
)
|
||||
si.department = "All Departments"
|
||||
si.save().submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"department": "All Departments",
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
dim.disabled = True
|
||||
dim.save()
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_cancel_with_common_party_advance_jv(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier")
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier").name
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(customer=customer)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="parent",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
|
||||
# cancel sales invoice
|
||||
si.cancel()
|
||||
|
||||
# check cancellation of journal entry
|
||||
jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
|
||||
self.assertEqual(jv_status, 2)
|
||||
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3758,6 +3881,130 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.update_outstanding_for_self = False
|
||||
pos_return.save().submit()
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gle)
|
||||
.select(gle.against_voucher)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
|
||||
)
|
||||
.run(as_list=1)
|
||||
)
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0][0], pos_return.return_against)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_foreign_currency_jv(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors USD",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
def test_invoice_remarks(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.po_no = "Test PO"
|
||||
si.po_date = nowdate()
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}")
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -38,6 +38,12 @@ frappe.ui.form.on("Subscription", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Force-Fetch Subscription Updates"),
|
||||
() => frm.trigger("force_fetch_subscription_updates"),
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Cancel Subscription"),
|
||||
() => frm.trigger("cancel_this_subscription"),
|
||||
@@ -82,4 +88,11 @@ frappe.ui.form.on("Subscription", {
|
||||
}
|
||||
});
|
||||
},
|
||||
force_fetch_subscription_updates: function (frm) {
|
||||
frm.call("force_fetch_subscription_updates").then((r) => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -717,6 +717,28 @@ class Subscription(Document):
|
||||
self.update_subscription_period(posting_date or nowdate())
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def force_fetch_subscription_updates(self):
|
||||
"""
|
||||
Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
|
||||
It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
|
||||
"""
|
||||
|
||||
# Don't process future subscriptions
|
||||
if nowdate() < self.current_invoice_start:
|
||||
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
|
||||
return
|
||||
|
||||
processing_date = None
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period":
|
||||
processing_date = self.current_invoice_start
|
||||
elif self.generate_invoice_at == "End of the current subscription period":
|
||||
processing_date = self.current_invoice_end
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||
|
||||
self.process(posting_date=processing_date)
|
||||
|
||||
|
||||
def is_prorate() -> int:
|
||||
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
|
||||
|
||||
@@ -521,6 +521,18 @@ class TestSubscription(FrappeTestCase):
|
||||
subscription.process(posting_date="2023-01-22")
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
def test_future_subscription(self):
|
||||
"""Force-Fetch should not process future subscriptions"""
|
||||
subscription = create_subscription(
|
||||
start_date=add_months(nowdate(), 1),
|
||||
submit_invoice=0,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
party="_Test Subscription Customer John Doe",
|
||||
)
|
||||
subscription.force_fetch_subscription_updates()
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -185,7 +185,7 @@ def get_tax_template(posting_date, args):
|
||||
conditions.append("(from_date is null) and (to_date is null)")
|
||||
|
||||
conditions.append(
|
||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
|
||||
)
|
||||
if "tax_category" in args.keys():
|
||||
del args["tax_category"]
|
||||
|
||||
@@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
@@ -375,12 +375,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
AND ja.party in %s
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
AND j.company = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -412,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
"""
|
||||
|
||||
if party_type == "Supplier":
|
||||
return []
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
conditions = []
|
||||
@@ -497,6 +502,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
@@ -604,8 +610,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
|
||||
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
)
|
||||
@@ -628,9 +632,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
)
|
||||
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
||||
|
||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
||||
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
||||
total_invoiced_amt = (
|
||||
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
||||
)
|
||||
|
||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
||||
@@ -639,6 +646,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
return tcs_amount
|
||||
|
||||
|
||||
def get_advance_adjusted_in_invoice(inv):
|
||||
advances_adjusted = 0
|
||||
for row in inv.get("advances", []):
|
||||
advances_adjusted += row.allocated_amount
|
||||
|
||||
return advances_adjusted
|
||||
|
||||
|
||||
def get_invoice_total_without_tcs(inv, tax_details):
|
||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||
|
||||
@@ -210,6 +210,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tcs_on_allocated_advance_payments(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
|
||||
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
|
||||
advances = si.get_advance_entries()
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
vouchers.append(si)
|
||||
|
||||
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
|
||||
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
|
||||
self.assertEqual(tcs_charged, 2000)
|
||||
|
||||
# cancel invoice and payments to avoid clashing
|
||||
for d in reversed(vouchers):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
|
||||
@@ -7,7 +7,9 @@ from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
@@ -363,3 +365,100 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
so2 = self.create_sales_order()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
# Allocation payment against Sales Order
|
||||
pe.paid_amount = 260
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so1.doctype, "reference_name": so1.name, "allocated_amount": 150},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so2.doctype, "reference_name": so2.name, "allocated_amount": 110},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so1.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
so2.reload()
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [(x.reference_name, x.allocated_amount) for x in unreconcile.allocations]
|
||||
self.assertListEqual(allocations, [(so1.name, 150), (so2.name, 110)])
|
||||
# unreconcile so2
|
||||
unreconcile.remove(unreconcile.allocations[0])
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so1.reload()
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
self.enable_advance_as_liability()
|
||||
so = self.create_sales_order()
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_amount = 1000
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 1000},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.insert().submit()
|
||||
|
||||
pr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Reconciliation",
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": so.customer,
|
||||
}
|
||||
)
|
||||
accounts = get_party_account("Customer", so.customer, so.company, True)
|
||||
pr.receivable_payable_account = accounts[0]
|
||||
pr.default_advance_account = accounts[1]
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:UNREC-{#####}",
|
||||
"creation": "2023-08-22 10:26:34.421423",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -58,11 +56,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified": "2024-10-10 12:03:50.022444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
||||
if not cost_center_allocation:
|
||||
return gl_map
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
# Validate budget against main cost center
|
||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||
|
||||
if cost_center and cost_center_allocation.get(cost_center):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
if not cost_center_allocation:
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
|
||||
return new_gl_map
|
||||
|
||||
|
||||
def get_cost_center_allocation_data(company, posting_date):
|
||||
par = frappe.qb.DocType("Cost Center Allocation")
|
||||
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
||||
def get_cost_center_allocation_data(company, posting_date, cost_center):
|
||||
cost_center_allocation = frappe.db.get_value(
|
||||
"Cost Center Allocation",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"company": company,
|
||||
"valid_from": ("<=", posting_date),
|
||||
"main_cost_center": cost_center,
|
||||
},
|
||||
pluck="name",
|
||||
order_by="valid_from desc",
|
||||
)
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(par)
|
||||
.inner_join(child)
|
||||
.on(par.name == child.parent)
|
||||
.select(par.main_cost_center, child.cost_center, child.percentage)
|
||||
.where(par.docstatus == 1)
|
||||
.where(par.company == company)
|
||||
.where(par.valid_from <= posting_date)
|
||||
.orderby(par.valid_from, order=frappe.qb.desc)
|
||||
).run(as_dict=True)
|
||||
if not cost_center_allocation:
|
||||
return []
|
||||
|
||||
cc_allocation = frappe._dict()
|
||||
for d in records:
|
||||
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage)
|
||||
records = frappe.db.get_all(
|
||||
"Cost Center Allocation Percentage",
|
||||
{"parent": cost_center_allocation},
|
||||
["cost_center", "percentage"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
return cc_allocation
|
||||
return records
|
||||
|
||||
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_party_details(
|
||||
pos_profile=None,
|
||||
):
|
||||
if not party:
|
||||
return {}
|
||||
return frappe._dict()
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||
return _get_party_details(
|
||||
|
||||
@@ -61,32 +61,10 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "payment_terms_template",
|
||||
@@ -162,6 +140,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
label: __("In Party Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "handle_employee_advances",
|
||||
label: __("Handle Employee Advances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -30,10 +30,7 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Supplier",
|
||||
"party": [self.supplier],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
|
||||
@@ -89,32 +89,10 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "customer_group",
|
||||
|
||||
@@ -50,6 +50,11 @@ class ReceivablePayableReport:
|
||||
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
|
||||
)
|
||||
|
||||
if not self.filters.range:
|
||||
self.filters.range = "30, 60, 90, 120"
|
||||
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
||||
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
self.set_defaults()
|
||||
@@ -112,6 +117,26 @@ class ReceivablePayableReport:
|
||||
|
||||
self.build_data()
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
@@ -123,24 +148,8 @@ class ReceivablePayableReport:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -208,6 +217,18 @@ class ReceivablePayableReport:
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
# Build and use a separate row for Employee Advances.
|
||||
# This allows Payments or Journals made against Emp Advance to be processed.
|
||||
if (
|
||||
not row
|
||||
and ple.against_voucher_type == "Employee Advance"
|
||||
and self.filters.handle_employee_advances
|
||||
):
|
||||
_d = self.build_voucher_dict(ple)
|
||||
_d.voucher_type = ple.against_voucher_type
|
||||
_d.voucher_no = ple.against_voucher_no
|
||||
row = self.voucher_balance[key] = _d
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
if self.filters.get("ignore_accounts"):
|
||||
@@ -289,8 +310,8 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
@@ -717,37 +738,22 @@ class ReceivablePayableReport:
|
||||
|
||||
# ageing buckets should not have amounts if due date is not reached
|
||||
if getdate(entry_date) > getdate(self.filters.report_date):
|
||||
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
|
||||
row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
|
||||
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
|
||||
|
||||
def get_ageing_data(self, entry_date, row):
|
||||
# [0-30, 30-60, 60-90, 90-120, 120-above]
|
||||
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
|
||||
if not (self.age_as_on and entry_date):
|
||||
return
|
||||
|
||||
row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0
|
||||
index = None
|
||||
|
||||
if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4):
|
||||
self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = (
|
||||
30,
|
||||
60,
|
||||
90,
|
||||
120,
|
||||
)
|
||||
|
||||
for i, days in enumerate(
|
||||
[self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]
|
||||
):
|
||||
if cint(row.age) <= cint(days):
|
||||
index = i
|
||||
break
|
||||
|
||||
if index is None:
|
||||
index = 4
|
||||
index = next(
|
||||
(i for i, days in enumerate(self.ranges) if cint(row.age) <= cint(days)), len(self.ranges)
|
||||
)
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
@@ -1059,6 +1065,7 @@ class ReceivablePayableReport:
|
||||
self.add_column(_("Debit Note"), fieldname="credit_note")
|
||||
self.add_column(_("Outstanding Amount"), fieldname="outstanding")
|
||||
|
||||
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
|
||||
self.setup_ageing_columns()
|
||||
|
||||
self.add_column(
|
||||
@@ -1117,34 +1124,26 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
|
||||
ranges = [*self.ranges, "Above"]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
label = f"{prev_range_value}-{curr_range_value}"
|
||||
self.add_column(label=label, fieldname="range" + str(idx + 1))
|
||||
|
||||
for i, label in enumerate(
|
||||
[
|
||||
"0-{range1}".format(range1=self.filters["range1"]),
|
||||
"{range1}-{range2}".format(
|
||||
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
|
||||
),
|
||||
"{range2}-{range3}".format(
|
||||
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
|
||||
),
|
||||
"{range3}-{range4}".format(
|
||||
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
|
||||
),
|
||||
_("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1),
|
||||
]
|
||||
):
|
||||
self.add_column(label=label, fieldname="range" + str(i + 1))
|
||||
self.ageing_column_labels.append(label)
|
||||
|
||||
if curr_range_value.isdigit():
|
||||
prev_range_value = cint(curr_range_value) + 1
|
||||
|
||||
def get_chart_data(self):
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
rows = []
|
||||
for row in self.data:
|
||||
row = frappe._dict(row)
|
||||
if not cint(row.bold):
|
||||
values = [row.range1, row.range2, row.range3, row.range4, row.range5]
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
rows.append({"values": [flt(val, precision) for val in values]})
|
||||
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
|
||||
rows.append({"values": values})
|
||||
|
||||
self.chart = {
|
||||
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
||||
|
||||
@@ -83,10 +83,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party": [self.customer],
|
||||
"report_date": add_days(today(), 2),
|
||||
"based_on_payment_terms": 0,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": False,
|
||||
}
|
||||
|
||||
@@ -116,10 +113,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
@@ -172,10 +166,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
@@ -266,10 +257,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 0,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -328,10 +316,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
@@ -397,10 +382,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
@@ -416,10 +398,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"group_by_party": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -493,10 +472,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_future_payments": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -555,10 +531,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"sales_person": sales_person.name,
|
||||
"show_sales_person": True,
|
||||
}
|
||||
@@ -575,10 +548,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -593,10 +563,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"customer_group": cus_group,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -618,10 +585,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"customer_group": cus_groups_list, # Use the list of customer groups
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -660,10 +624,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"party_account": self.debit_to,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -711,10 +672,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
@@ -754,10 +712,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer1, self.customer3],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
@@ -837,10 +792,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
report_ouput = execute(filters)[1]
|
||||
@@ -903,10 +855,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
{
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_future_payments": True,
|
||||
"in_party_currency": False,
|
||||
}
|
||||
@@ -965,10 +914,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
@@ -991,10 +937,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
|
||||
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
|
||||
@@ -104,25 +104,23 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.set_party_details(d)
|
||||
|
||||
def init_party_total(self, row):
|
||||
default_dict = {
|
||||
"invoiced": 0.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 0.0,
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
for i in self.range_numbers:
|
||||
range_key = f"range{i}"
|
||||
default_dict[range_key] = 0.0
|
||||
|
||||
self.party_total.setdefault(
|
||||
row.party,
|
||||
frappe._dict(
|
||||
{
|
||||
"invoiced": 0.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 0.0,
|
||||
"range1": 0.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
),
|
||||
frappe._dict(default_dict),
|
||||
)
|
||||
|
||||
def set_party_details(self, row):
|
||||
@@ -173,6 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_("Difference"), fieldname="diff")
|
||||
|
||||
self.setup_ageing_columns()
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
@@ -206,27 +205,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80
|
||||
)
|
||||
|
||||
def setup_ageing_columns(self):
|
||||
for i, label in enumerate(
|
||||
[
|
||||
"0-{range1}".format(range1=self.filters["range1"]),
|
||||
"{range1}-{range2}".format(
|
||||
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
|
||||
),
|
||||
"{range2}-{range3}".format(
|
||||
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
|
||||
),
|
||||
"{range3}-{range4}".format(
|
||||
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
|
||||
),
|
||||
"{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")),
|
||||
]
|
||||
):
|
||||
self.add_column(label=label, fieldname="range" + str(i + 1))
|
||||
|
||||
# Add column for total due amount
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
|
||||
def get_gl_balance(report_date, company):
|
||||
return frappe._dict(
|
||||
|
||||
@@ -27,10 +27,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
@@ -121,10 +118,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ def get_group_by_asset_category_data(filters):
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(
|
||||
@@ -68,7 +69,10 @@ def get_group_by_asset_category_data(filters):
|
||||
def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
condition += " and a.asset_category = %(asset_category)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
@@ -108,10 +112,26 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
@@ -119,6 +139,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -127,55 +148,77 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and name = %(asset)s"
|
||||
condition += " and a.name = %(asset)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT name,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.name,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by name
|
||||
end), 0) as cost_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -197,6 +240,7 @@ def get_group_by_asset_data(filters):
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
- flt(row.cost_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
@@ -223,9 +267,15 @@ def get_group_by_asset_data(filters):
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
@@ -255,7 +305,14 @@ def get_assets_for_grouped_by_category(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
@@ -271,11 +328,16 @@ def get_assets_for_grouped_by_category(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -283,9 +345,15 @@ def get_assets_for_grouped_by_category(filters):
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = " and a.name = '{}'".format(filters.get("asset"))
|
||||
condition = f" and a.name = '{filters.get('asset')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
@@ -315,7 +383,14 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{finance_book_filter} {condition}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
@@ -331,11 +406,16 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.name) as results
|
||||
group by results.name
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -389,6 +469,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Capitalized Asset"),
|
||||
"fieldname": "cost_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
|
||||
@@ -95,7 +95,7 @@ def execute(filters=None):
|
||||
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
@@ -221,7 +221,7 @@ def get_report_summary(
|
||||
], (net_asset - net_liability + net_equity)
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity):
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
|
||||
asset_data, liability_data, equity_data = [], [], []
|
||||
@@ -249,4 +249,8 @@ def get_chart_data(filters, columns, asset, liability, equity):
|
||||
else:
|
||||
chart["type"] = "line"
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
open_utility_report: function () {
|
||||
frappe.route_options = {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account: frappe.query_report.get_filter_value("account"),
|
||||
report_date: frappe.query_report.get_filter_value("report_date"),
|
||||
};
|
||||
frappe.open_in_new_tab = true;
|
||||
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -154,8 +154,8 @@ def get_payment_entries(filters):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -111,7 +111,7 @@ def execute(filters=None):
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
chart = get_chart_data(columns, data)
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
report_summary = get_report_summary(summary_data, company_currency)
|
||||
|
||||
@@ -252,7 +252,7 @@ def get_report_summary(summary_data, currency):
|
||||
return report_summary
|
||||
|
||||
|
||||
def get_chart_data(columns, data):
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
datasets = [
|
||||
{
|
||||
@@ -267,5 +267,7 @@ def get_chart_data(columns, data):
|
||||
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Bank Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
default: frappe.defaults.get_user_default("Company")
|
||||
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
|
||||
: "",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_account_list",
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "disabled", "=", 0],
|
||||
["Account", "company", "=", company],
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-07-30 17:20:07.570971",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-07-30 17:20:07.570971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cheques and Deposits Incorrectly cleared",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Entry",
|
||||
"report_name": "Cheques and Deposits Incorrectly cleared",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = build_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def build_payment_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
}
|
||||
)
|
||||
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": row.get("amount"),
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": row.get("amount"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_journal_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
"debit": row.get("debit_in_account_currency"),
|
||||
"credit": row.get("credit_in_account_currency"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_data(filters):
|
||||
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
|
||||
data = []
|
||||
for x in vouchers:
|
||||
if x.doctype == "Payment Entry":
|
||||
data.append(build_payment_entry_dict(x))
|
||||
elif x.doctype == "Journal Entry":
|
||||
data.append(build_journal_entry_dict(x))
|
||||
return data
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
doctype_name = ConstantColumn("Journal Entry")
|
||||
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
je.name,
|
||||
jea.debit_in_account_currency,
|
||||
jea.credit_in_account_currency,
|
||||
je.posting_date,
|
||||
je.clearance_date,
|
||||
)
|
||||
.where(
|
||||
je.docstatus.eq(1)
|
||||
& jea.account.eq(filters.account)
|
||||
& je.posting_date.gt(filters.report_date)
|
||||
& je.clearance_date.lte(filters.report_date)
|
||||
& (je.is_opening.isnull() | je.is_opening.eq("No"))
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
pe.clearance_date,
|
||||
)
|
||||
.where(
|
||||
pe.docstatus.eq(1)
|
||||
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
|
||||
& pe.posting_date.gt(filters.report_date)
|
||||
& pe.clearance_date.lte(filters.report_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
return journals + payments
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "payment_document",
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry",
|
||||
"label": _("Payment Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "payment_document",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
|
||||
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
|
||||
]
|
||||
@@ -104,7 +104,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
report_summary = get_bs_summary(
|
||||
report_summary, primitive_summary = get_bs_summary(
|
||||
companies,
|
||||
asset,
|
||||
liability,
|
||||
@@ -115,7 +115,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
|
||||
|
||||
return data, message, chart, report_summary
|
||||
|
||||
@@ -173,9 +173,9 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
if net_profit_loss:
|
||||
data.append(net_profit_loss)
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
|
||||
|
||||
report_summary = get_pl_summary(
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
)
|
||||
|
||||
@@ -469,10 +469,13 @@ def update_parent_account_names(accounts):
|
||||
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
account_key = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
name_to_account_map[d.name] = account_name
|
||||
account_key = d.account_name
|
||||
|
||||
d.account_key = account_key
|
||||
|
||||
name_to_account_map[d.name] = account_key
|
||||
|
||||
for account in accounts:
|
||||
if account.parent_account:
|
||||
@@ -505,33 +508,26 @@ def get_subsidiary_companies(company):
|
||||
|
||||
def get_accounts(root_type, companies):
|
||||
accounts = []
|
||||
added_accounts = []
|
||||
|
||||
for company in companies:
|
||||
for account in frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
):
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
|
||||
if account_key not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account_key)
|
||||
accounts.extend(
|
||||
frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
)
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
@@ -770,15 +766,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
def filter_accounts(accounts, depth=10):
|
||||
parent_children_map = {}
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[account_name] = d
|
||||
added_accounts = []
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
for d in accounts:
|
||||
if d.account_key in added_accounts:
|
||||
continue
|
||||
|
||||
added_accounts.append(d.account_key)
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[d.account_key] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account_name or None, []).append(d)
|
||||
|
||||
filtered_accounts = []
|
||||
|
||||
@@ -790,7 +788,7 @@ def filter_accounts(accounts, depth=10):
|
||||
for child in children:
|
||||
child.indent = level
|
||||
filtered_accounts.append(child)
|
||||
add_to_list(child.name, level + 1)
|
||||
add_to_list(child.account_key, level + 1)
|
||||
|
||||
add_to_list(None, 0)
|
||||
|
||||
|
||||
@@ -199,8 +199,7 @@ class General_Payment_Ledger_Comparison:
|
||||
dict(
|
||||
label=_("Voucher Type"),
|
||||
fieldname="voucher_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
fieldtype="Data",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
@@ -219,8 +218,7 @@ class General_Payment_Ledger_Comparison:
|
||||
dict(
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
fieldtype="Data",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -48,8 +48,9 @@
|
||||
<br>
|
||||
{% } %}
|
||||
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% if(data[i].bill_no) { %}
|
||||
{% if(data[i].remarks) { %}
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% } else if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</span>
|
||||
|
||||
@@ -209,6 +209,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_cr_dr_notes",
|
||||
label: __("Ignore System Generated Credit / Debit Notes"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ def execute(filters=None):
|
||||
if filters.get("party"):
|
||||
filters.party = frappe.parse_json(filters.get("party"))
|
||||
|
||||
if filters.get("voucher_no") and not filters.get("group_by"):
|
||||
filters.group_by = "Group by Voucher (Consolidated)"
|
||||
|
||||
validate_filters(filters, account_details)
|
||||
|
||||
validate_party(filters)
|
||||
@@ -236,6 +239,23 @@ def get_conditions(filters):
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if filters.get("ignore_cr_dr_notes"):
|
||||
system_generated_cr_dr_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
|
||||
"is_system_generated": 1,
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
if system_generated_cr_dr_journals:
|
||||
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
|
||||
x[0] for x in system_generated_cr_dr_journals
|
||||
]
|
||||
filters.update({"voucher_no_not_in": vouchers_to_ignore})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
|
||||
@@ -2,13 +2,32 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def test_foreign_account_balance_after_exchange_rate_revaluation(self):
|
||||
"""
|
||||
Checks the correctness of balance after exchange rate revaluation
|
||||
@@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
)
|
||||
)
|
||||
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
def test_ignore_cr_dr_notes_filter(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
cr_note = make_return_doc(si.doctype, si.name)
|
||||
cr_note.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = si.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = si.customer
|
||||
pr.receivable_payable_account = si.debit_to
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name]
|
||||
payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
system_generated_journal = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"voucher_type": "Credit Note",
|
||||
"is_system_generated": True,
|
||||
},
|
||||
fields=["name"],
|
||||
)
|
||||
self.assertEqual(len(system_generated_journal), 1)
|
||||
expected = set([si.name, cr_note.name, system_generated_journal[0].name])
|
||||
# Without ignore_cr_dr_notes
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Without ignore_cr_dr_notes
|
||||
expected = set([si.name, cr_note.name])
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("Start Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("End Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Invalid Ledger Entries"] = {
|
||||
filters: get_filters(),
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-09-09 12:31:25.295976",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-09 12:31:25.295976",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Invalid Ledger Entries",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Invalid Ledger Entries",
|
||||
"report_type": "Script Report",
|
||||
"roles": [],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
"""Return columns and data for the report.
|
||||
|
||||
This is the main entry point for the report. It accepts the filters as a
|
||||
dictionary and should return columns and data. It is called by the framework
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns() -> list[dict]:
|
||||
"""Return columns for the report.
|
||||
|
||||
One field definition per column, just like a DocType field definition.
|
||||
"""
|
||||
return [
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
active_vouchers = get_active_vouchers_for_period(filters)
|
||||
invalid_vouchers = identify_cancelled_vouchers(active_vouchers)
|
||||
|
||||
return invalid_vouchers
|
||||
|
||||
|
||||
def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]:
|
||||
cancelled_vouchers = []
|
||||
if active_vouchers:
|
||||
# Group by voucher types and use single query to identify cancelled vouchers
|
||||
vtypes = set([x.voucher_type for x in active_vouchers])
|
||||
|
||||
for _t in vtypes:
|
||||
_names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t]
|
||||
dt = qb.DocType(_t)
|
||||
non_active_vouchers = (
|
||||
qb.from_(dt)
|
||||
.select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no"))
|
||||
.where(dt.docstatus.ne(1) & dt.name.isin(_names))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if non_active_vouchers:
|
||||
cancelled_vouchers.extend(non_active_vouchers)
|
||||
return cancelled_vouchers
|
||||
|
||||
|
||||
def validate_filters(filters: dict | None = None):
|
||||
if not filters:
|
||||
frappe.throw(_("Filters missing"))
|
||||
|
||||
if not filters.company:
|
||||
frappe.throw(_("Company is mandatory"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("Start Date should be lower than End Date"))
|
||||
|
||||
|
||||
def build_query_filters(filters: dict | None = None) -> list:
|
||||
qb_filters = []
|
||||
if filters:
|
||||
if filters.account:
|
||||
qb_filters.append(qb.Field("account").isin(filters.account))
|
||||
|
||||
if filters.voucher_no:
|
||||
qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no))
|
||||
|
||||
return qb_filters
|
||||
|
||||
|
||||
def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]:
|
||||
uniq_vouchers = []
|
||||
|
||||
if filters:
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
qb_filters = build_query_filters(filters)
|
||||
|
||||
gl_vouchers = (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_type)
|
||||
.distinct()
|
||||
.select(gle.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0)
|
||||
& gle.company.eq(filters.company)
|
||||
& gle.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
pl_vouchers = (
|
||||
qb.from_(ple)
|
||||
.select(ple.voucher_type)
|
||||
.distinct()
|
||||
.select(ple.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
ple.delinked.eq(0)
|
||||
& ple.company.eq(filters.company)
|
||||
& ple.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
uniq_vouchers.extend(gl_vouchers)
|
||||
uniq_vouchers.extend(pl_vouchers)
|
||||
|
||||
return uniq_vouchers
|
||||
@@ -210,7 +210,7 @@ class PaymentLedger:
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True)
|
||||
dict(label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", hidden=True)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -59,11 +59,11 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
@@ -152,7 +152,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
return net_profit_loss
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss):
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
@@ -181,5 +181,7 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss):
|
||||
chart["type"] = "line"
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
|
||||
"""select distinct expense_account
|
||||
from `tabPurchase Invoice Item` where docstatus = 1
|
||||
and (expense_account is not null and expense_account != '')
|
||||
and parenttype='Purchase Invoice'
|
||||
and parent in (%s) order by expense_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple([inv.name for inv in invoice_list]),
|
||||
@@ -451,7 +452,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
"""
|
||||
select parent, expense_account, sum(base_net_amount) as amount
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
group by parent, expense_account
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
@@ -522,7 +523,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
"""
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user