Compare commits
1075 Commits
revert-356
...
welcome_pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91406bf84e | ||
|
|
2a575d9dc2 | ||
|
|
08d91ab831 | ||
|
|
083c82c206 | ||
|
|
ac650d2e7a | ||
|
|
5acbf3f262 | ||
|
|
3764c01028 | ||
|
|
9004721859 | ||
|
|
5e21e7cd1d | ||
|
|
21f94918a3 | ||
|
|
099468e3cf | ||
|
|
34bb64e41a | ||
|
|
fc016680c9 | ||
|
|
7045810bae | ||
|
|
a093dff039 | ||
|
|
097b9892dc | ||
|
|
855a0d3bec | ||
|
|
61778d5058 | ||
|
|
94ce43b0d5 | ||
|
|
f2b0ac6868 | ||
|
|
2be025311a | ||
|
|
08aaf22b2a | ||
|
|
2f7b3bbfad | ||
|
|
85ebaa3aed | ||
|
|
4940edc386 | ||
|
|
2dbdc402bb | ||
|
|
5976d0d13f | ||
|
|
0fadde037a | ||
|
|
2135b0132d | ||
|
|
41fb40e26f | ||
|
|
6f4f5be990 | ||
|
|
96363dbb07 | ||
|
|
7711744020 | ||
|
|
1db05c80e1 | ||
|
|
6664bc98a0 | ||
|
|
78905e35f2 | ||
|
|
d2f3286115 | ||
|
|
7bfd487b25 | ||
|
|
1d014122c1 | ||
|
|
b83d880d66 | ||
|
|
0e83190c19 | ||
|
|
1c78a5a9aa | ||
|
|
74272a2e28 | ||
|
|
328ba4b656 | ||
|
|
6bab0eeaa1 | ||
|
|
e6c302a397 | ||
|
|
88f7e57edf | ||
|
|
5898a6f48e | ||
|
|
055156d28a | ||
|
|
47ffa4983c | ||
|
|
1f02fb3bd9 | ||
|
|
769db0b3bc | ||
|
|
da54ab5b3d | ||
|
|
9a23ba53c5 | ||
|
|
93af6f6a1b | ||
|
|
c09807845f | ||
|
|
3c6e527dd1 | ||
|
|
623239d3f7 | ||
|
|
fae640c56f | ||
|
|
2ad62be2c7 | ||
|
|
0d5c8f03bd | ||
|
|
b9750f324b | ||
|
|
5cae2e79bd | ||
|
|
d3295c43e3 | ||
|
|
b4bcd9ba3f | ||
|
|
43061f4416 | ||
|
|
d739ab6ee3 | ||
|
|
dda4c5ec59 | ||
|
|
ce549ce9b2 | ||
|
|
846ae32d92 | ||
|
|
b34c03d306 | ||
|
|
c849a012d5 | ||
|
|
f39a9145e9 | ||
|
|
3e0a795028 | ||
|
|
2a77b50191 | ||
|
|
d398775715 | ||
|
|
5c09fdf941 | ||
|
|
1d93d66c30 | ||
|
|
034322c53f | ||
|
|
c2f8f1d028 | ||
|
|
0f065fe280 | ||
|
|
1b853857aa | ||
|
|
ae01d70b33 | ||
|
|
0077659e93 | ||
|
|
8ce6b8179e | ||
|
|
e1a94a9ba1 | ||
|
|
175870ce8d | ||
|
|
8eea4eb56e | ||
|
|
174f95d699 | ||
|
|
f809e12747 | ||
|
|
ccd2e2b086 | ||
|
|
b9c556c4a9 | ||
|
|
d2f03c8a65 | ||
|
|
5dbcf7d2b9 | ||
|
|
6fd1c1bca2 | ||
|
|
9a1588f1cc | ||
|
|
67980188a7 | ||
|
|
9b6eac23b6 | ||
|
|
b4dc2bdf28 | ||
|
|
0130aea2aa | ||
|
|
de910ab152 | ||
|
|
285963acdb | ||
|
|
cce96669f0 | ||
|
|
0ccb6d8242 | ||
|
|
69683776a5 | ||
|
|
1981f3837a | ||
|
|
25fe752185 | ||
|
|
5981c7e0ad | ||
|
|
58dc0e52e1 | ||
|
|
6bbe47c671 | ||
|
|
489a545bbb | ||
|
|
42df0d3d67 | ||
|
|
fbdfb8151c | ||
|
|
41eb2c9f5a | ||
|
|
fc6be5bfb9 | ||
|
|
0faffaa8db | ||
|
|
5114a9580d | ||
|
|
e48a90efe6 | ||
|
|
dc71623295 | ||
|
|
13ca474a46 | ||
|
|
f7865da4d2 | ||
|
|
6a7bdeffdf | ||
|
|
04bc353e7f | ||
|
|
75d95acb23 | ||
|
|
cd8ddae7c5 | ||
|
|
22cf1556cd | ||
|
|
24e1144de5 | ||
|
|
27e0dd9fdc | ||
|
|
0366928db5 | ||
|
|
d6a3b9a5c7 | ||
|
|
4f0bb45a8b | ||
|
|
2e7548462d | ||
|
|
f3282b1bb3 | ||
|
|
30aba9414c | ||
|
|
79fa562004 | ||
|
|
cb6da6ec59 | ||
|
|
79c6f0165b | ||
|
|
e599f75a51 | ||
|
|
2d8363a983 | ||
|
|
91e574609f | ||
|
|
0e517227ee | ||
|
|
1e72a43a8e | ||
|
|
39b598e0ec | ||
|
|
b6e6f2ef8d | ||
|
|
a7e0709ae8 | ||
|
|
3e1065a561 | ||
|
|
480903e3f4 | ||
|
|
a1ae4262c3 | ||
|
|
0d26f67be5 | ||
|
|
64d835374b | ||
|
|
112cfe6dfa | ||
|
|
2537f5674e | ||
|
|
d73daafe77 | ||
|
|
cd74c6c07f | ||
|
|
283c0a1c0f | ||
|
|
f032476a75 | ||
|
|
e92d4e9b4a | ||
|
|
5e38109481 | ||
|
|
e75b72ae2d | ||
|
|
92e503f75b | ||
|
|
dea802dc41 | ||
|
|
060da2c5bc | ||
|
|
4856e11549 | ||
|
|
a006b66e45 | ||
|
|
de433d8626 | ||
|
|
5c4df3e0e3 | ||
|
|
1c237f455e | ||
|
|
26e8b8f959 | ||
|
|
0d22fe02be | ||
|
|
3a2933db4d | ||
|
|
f07f4ce86f | ||
|
|
eb5c61be1e | ||
|
|
9d330a13ed | ||
|
|
592c7b5ff1 | ||
|
|
9895bc3246 | ||
|
|
2fdbe82835 | ||
|
|
67a0969b78 | ||
|
|
714b8289c1 | ||
|
|
5c2a949593 | ||
|
|
0a9187ea42 | ||
|
|
91927f2ada | ||
|
|
ffcbcd7397 | ||
|
|
25e78fb311 | ||
|
|
d40504b973 | ||
|
|
3f077479f4 | ||
|
|
6b75c03f34 | ||
|
|
35f72a17db | ||
|
|
fa2e5d5ab7 | ||
|
|
9bc5952dd5 | ||
|
|
740fe95ff3 | ||
|
|
afd7d59c73 | ||
|
|
d4218a88ad | ||
|
|
27d56461c5 | ||
|
|
ed4f498704 | ||
|
|
08bc33689c | ||
|
|
57538bd76a | ||
|
|
5c48074bed | ||
|
|
a7137514b6 | ||
|
|
e462edc628 | ||
|
|
c8d7433e30 | ||
|
|
1504ff8b6c | ||
|
|
447b73fa77 | ||
|
|
ce81ffd844 | ||
|
|
879d31a588 | ||
|
|
bb7bed4c1a | ||
|
|
91ca2309da | ||
|
|
388a42ec7e | ||
|
|
9b47617117 | ||
|
|
7a6db924d5 | ||
|
|
199071b773 | ||
|
|
40a6b5cefe | ||
|
|
a6b2cf3acd | ||
|
|
794edbb334 | ||
|
|
1449e38b39 | ||
|
|
abc5fdd47b | ||
|
|
f78b6d0081 | ||
|
|
8ed8698fdf | ||
|
|
0120588f5f | ||
|
|
8bf87ebfdf | ||
|
|
299e32befd | ||
|
|
5f75aea6fa | ||
|
|
6349b67df4 | ||
|
|
3c15feadf6 | ||
|
|
a33b75f2cd | ||
|
|
7c1417e199 | ||
|
|
8f1e00906f | ||
|
|
ab6e600b9e | ||
|
|
d1e877b6f0 | ||
|
|
4e7cccbdf0 | ||
|
|
6d9cebfee9 | ||
|
|
49be1190d9 | ||
|
|
8010a157b1 | ||
|
|
f31eb74234 | ||
|
|
723563c167 | ||
|
|
54ffe41b54 | ||
|
|
758ea7bfe1 | ||
|
|
2ed73c17c3 | ||
|
|
bb3bd02f53 | ||
|
|
56b26852f3 | ||
|
|
60eee564bf | ||
|
|
c9ae9df902 | ||
|
|
3fab6610cb | ||
|
|
3fdcd33b92 | ||
|
|
5b62bbe073 | ||
|
|
bf6bf79ae3 | ||
|
|
53926b0620 | ||
|
|
a77e9d36cc | ||
|
|
305d39f6a1 | ||
|
|
611c2bf775 | ||
|
|
b9b1717e96 | ||
|
|
9588bb7443 | ||
|
|
87e2309e8e | ||
|
|
9349bc77c5 | ||
|
|
9d29ec8eac | ||
|
|
8f04945cef | ||
|
|
4b75b6f309 | ||
|
|
3fd2778ae4 | ||
|
|
e93b927051 | ||
|
|
3de9fed230 | ||
|
|
991770ed4a | ||
|
|
a4ff76c920 | ||
|
|
d01f0f2e96 | ||
|
|
86bac2cf52 | ||
|
|
52f609e67a | ||
|
|
e48f8139eb | ||
|
|
7a381affce | ||
|
|
af52f21ece | ||
|
|
06f86ad5e0 | ||
|
|
86cac1e1d2 | ||
|
|
faf9f13215 | ||
|
|
c47adcfdd9 | ||
|
|
450949cadd | ||
|
|
0dc5e5c430 | ||
|
|
896b123fb1 | ||
|
|
e3104f1898 | ||
|
|
a7f921a557 | ||
|
|
ddd929ef9b | ||
|
|
87d02511a3 | ||
|
|
f10a93f6ee | ||
|
|
a34cb8a8dc | ||
|
|
96847db0ec | ||
|
|
6a7b45f689 | ||
|
|
2e22b019a0 | ||
|
|
e8f6c286d1 | ||
|
|
268c19e745 | ||
|
|
8f695123cd | ||
|
|
21e6db2bc7 | ||
|
|
1bdd43d0f6 | ||
|
|
c70abaa43a | ||
|
|
3f6ff8e0b7 | ||
|
|
5d9dde92fa | ||
|
|
6bf79f18c8 | ||
|
|
21c1141fdb | ||
|
|
552bbb1d46 | ||
|
|
7ec6909159 | ||
|
|
35be3ac5a1 | ||
|
|
98adfb4c9a | ||
|
|
3345165206 | ||
|
|
8601e5b3a4 | ||
|
|
124c0dbd88 | ||
|
|
d119d2ec32 | ||
|
|
f6e4ac2b62 | ||
|
|
c071523e35 | ||
|
|
0d95fc0f20 | ||
|
|
43f530b077 | ||
|
|
924911e743 | ||
|
|
45662fa646 | ||
|
|
e0c79d3b53 | ||
|
|
1e07f6eef9 | ||
|
|
cdf100d552 | ||
|
|
0e2fb1188a | ||
|
|
89e89de961 | ||
|
|
d77b0295fa | ||
|
|
a0fc68538f | ||
|
|
deb0d71294 | ||
|
|
7ab55b1bb2 | ||
|
|
670d9e5556 | ||
|
|
843e77e72d | ||
|
|
985ff9781b | ||
|
|
12a6f3b997 | ||
|
|
1ff80fcbee | ||
|
|
8b57979e9c | ||
|
|
75652799cd | ||
|
|
e023e33a15 | ||
|
|
d4cc9daca1 | ||
|
|
68df3f9729 | ||
|
|
fbb5058531 | ||
|
|
39ec11200e | ||
|
|
b30c1e1abf | ||
|
|
00878707ae | ||
|
|
db76e8a277 | ||
|
|
ae2c600223 | ||
|
|
ce25f9e8c9 | ||
|
|
3c7702c53c | ||
|
|
539cfd08f0 | ||
|
|
2d3fa8040c | ||
|
|
6b464edf84 | ||
|
|
f8e7bc08af | ||
|
|
7f062a71f1 | ||
|
|
713880aef0 | ||
|
|
627986efa1 | ||
|
|
64614cd915 | ||
|
|
57449589e7 | ||
|
|
581d98c5ae | ||
|
|
98e82e0d99 | ||
|
|
dd91a77fdd | ||
|
|
7e34468504 | ||
|
|
5bed119de9 | ||
|
|
cd28d15292 | ||
|
|
dbd3fdbb41 | ||
|
|
d138948c70 | ||
|
|
90bc0d6bd0 | ||
|
|
567f4c37fc | ||
|
|
000de4eddf | ||
|
|
c8e6e067ae | ||
|
|
f6c055cca9 | ||
|
|
8bdf280cfb | ||
|
|
e4b863af05 | ||
|
|
940b1d9e67 | ||
|
|
526f1d18fb | ||
|
|
704e6577e5 | ||
|
|
5fb92cbb49 | ||
|
|
3a21c90d10 | ||
|
|
baf5cddd1b | ||
|
|
936fb1decf | ||
|
|
18e3c67d97 | ||
|
|
f2eb3d0f94 | ||
|
|
ba6de0b4ff | ||
|
|
11cd163db7 | ||
|
|
4ed4b0240d | ||
|
|
de17eaef38 | ||
|
|
5169006085 | ||
|
|
4a7fc1506f | ||
|
|
3997aa77d4 | ||
|
|
47cb349362 | ||
|
|
ad33cd73e8 | ||
|
|
32863b4922 | ||
|
|
5740942de9 | ||
|
|
3866be4c2a | ||
|
|
e415cb2873 | ||
|
|
9db8769e65 | ||
|
|
b0c79a0467 | ||
|
|
e64b004eca | ||
|
|
4fb844ab70 | ||
|
|
c1dd06065b | ||
|
|
68ad62f7d0 | ||
|
|
fd91f2c2e0 | ||
|
|
0b36e7d10e | ||
|
|
11d5327d1b | ||
|
|
492ea3bcc8 | ||
|
|
ecba6ee183 | ||
|
|
8cc3df7c2c | ||
|
|
28dfc88789 | ||
|
|
21080afd92 | ||
|
|
b86747c9d4 | ||
|
|
2eea90a873 | ||
|
|
10a2191e3f | ||
|
|
f574ac11ea | ||
|
|
0218ca538f | ||
|
|
38805603db | ||
|
|
b717e2b5bf | ||
|
|
5435c641a2 | ||
|
|
93767eb7fc | ||
|
|
96035b87d5 | ||
|
|
559d914c0b | ||
|
|
758b31d895 | ||
|
|
e179499764 | ||
|
|
16bc1e228f | ||
|
|
a4be6b0f10 | ||
|
|
466734fb4b | ||
|
|
b483364649 | ||
|
|
dbc000d655 | ||
|
|
b65ee6c2db | ||
|
|
b86afb2964 | ||
|
|
edbefee10c | ||
|
|
49be740736 | ||
|
|
17585f08ba | ||
|
|
510543680b | ||
|
|
c9d5a62350 | ||
|
|
7276d593c3 | ||
|
|
04820b14da | ||
|
|
10529e1f5a | ||
|
|
38a612c62e | ||
|
|
5b1aa07ecb | ||
|
|
27ebf14f9d | ||
|
|
dedf24b86d | ||
|
|
b715453ae3 | ||
|
|
002bf77314 | ||
|
|
cd98be6088 | ||
|
|
a8df875820 | ||
|
|
17771a55fb | ||
|
|
bd3fc7c434 | ||
|
|
ab933df5bb | ||
|
|
2ab3d75274 | ||
|
|
3f09f811bf | ||
|
|
333f2a565b | ||
|
|
a93ae9c826 | ||
|
|
4f473eb090 | ||
|
|
ba15810639 | ||
|
|
f333d2724a | ||
|
|
bc8d05da0f | ||
|
|
11bd15e580 | ||
|
|
f31d07554d | ||
|
|
f83a100a8d | ||
|
|
e8eeeb16e2 | ||
|
|
652398fad2 | ||
|
|
ce36d1f668 | ||
|
|
05b07e098a | ||
|
|
1ddfaa7605 | ||
|
|
70c5df056d | ||
|
|
7805abbb2d | ||
|
|
c6d8f15b10 | ||
|
|
c6b024c34b | ||
|
|
b38d300a92 | ||
|
|
eeddeeeeb3 | ||
|
|
c850f46c0a | ||
|
|
26ee50269a | ||
|
|
371413a078 | ||
|
|
3698af834b | ||
|
|
c0642cf528 | ||
|
|
b71dafd1f1 | ||
|
|
caad4537c5 | ||
|
|
f999b75ed6 | ||
|
|
ecca9cb023 | ||
|
|
3173546d5c | ||
|
|
3b58055410 | ||
|
|
bc470591ac | ||
|
|
ee7da639e7 | ||
|
|
bc6cbb9e25 | ||
|
|
1c687a4afd | ||
|
|
d9ac7f9b87 | ||
|
|
4e58503075 | ||
|
|
c82cb379a5 | ||
|
|
148d466ae5 | ||
|
|
58d867503b | ||
|
|
f5761e7965 | ||
|
|
1c2148b637 | ||
|
|
e355dea4b5 | ||
|
|
49981fecc7 | ||
|
|
e36c8ce5be | ||
|
|
fd5c4e0a64 | ||
|
|
f9fa34ff40 | ||
|
|
50d294fd1e | ||
|
|
46ea814400 | ||
|
|
567c0ce1e8 | ||
|
|
dac9fd64a8 | ||
|
|
89d109e8d2 | ||
|
|
829387c2bc | ||
|
|
fff83bc847 | ||
|
|
ebdf1959fd | ||
|
|
a25f34c3d5 | ||
|
|
2341061852 | ||
|
|
4496a6760e | ||
|
|
dacf013170 | ||
|
|
523d2c38eb | ||
|
|
cf50bb45ad | ||
|
|
3db6ac5ac9 | ||
|
|
4f9242d699 | ||
|
|
ccf1920a78 | ||
|
|
5d87c06332 | ||
|
|
5dc8195d91 | ||
|
|
1d7dbd3456 | ||
|
|
56e7cc7e05 | ||
|
|
b1818137e7 | ||
|
|
804afaa647 | ||
|
|
d9d6856153 | ||
|
|
acc7322874 | ||
|
|
47bbb37291 | ||
|
|
025091161e | ||
|
|
bfa54d5335 | ||
|
|
ae424fdfed | ||
|
|
95543225cf | ||
|
|
e3d2a2c5bd | ||
|
|
506a5775f9 | ||
|
|
ba1f065765 | ||
|
|
1ea1bfebc4 | ||
|
|
c0b3b069b5 | ||
|
|
c87332d5da | ||
|
|
6628632fbb | ||
|
|
37895a361c | ||
|
|
70dd9d0671 | ||
|
|
f3363e813a | ||
|
|
f4a65cccc4 | ||
|
|
5695d6a5a6 | ||
|
|
0567243772 | ||
|
|
73cc1ba654 | ||
|
|
6e18bb6456 | ||
|
|
f119a1e115 | ||
|
|
cd42b26839 | ||
|
|
1bcb728c85 | ||
|
|
72bc5b3a11 | ||
|
|
5b06bd1af4 | ||
|
|
78bc712756 | ||
|
|
ee2d1fa36e | ||
|
|
389cadf157 | ||
|
|
ee3ce82ea8 | ||
|
|
7b516f8463 | ||
|
|
00a2e42a47 | ||
|
|
4ff53e1062 | ||
|
|
92ae9c2201 | ||
|
|
c1184585ed | ||
|
|
34b5e849a2 | ||
|
|
13febcac81 | ||
|
|
0587338435 | ||
|
|
7e94a1c51b | ||
|
|
5e1cd1f227 | ||
|
|
81cd7873d3 | ||
|
|
5e7b05e566 | ||
|
|
c47a37c3ab | ||
|
|
e79f80331d | ||
|
|
f28f8dc596 | ||
|
|
f368894d22 | ||
|
|
a5b626420d | ||
|
|
666d961875 | ||
|
|
1c2da92233 | ||
|
|
89aa6f0269 | ||
|
|
3fe75ce7d4 | ||
|
|
3558c3d24e | ||
|
|
d69af741c8 | ||
|
|
17d4ab36c0 | ||
|
|
11a9d4124f | ||
|
|
1691eee26e | ||
|
|
eead2bba9f | ||
|
|
fd2c272bed | ||
|
|
55a9537220 | ||
|
|
cb2bfabb6f | ||
|
|
341709aa0a | ||
|
|
2c1943c7e6 | ||
|
|
6830a8737a | ||
|
|
8c410c617c | ||
|
|
e1d6bf364e | ||
|
|
95c6f4d40d | ||
|
|
61be373800 | ||
|
|
62ca89b10f | ||
|
|
e6d66fe5b0 | ||
|
|
30554301c9 | ||
|
|
3f81e15672 | ||
|
|
1b8490dc98 | ||
|
|
1bc87a970a | ||
|
|
4867a767a2 | ||
|
|
4205f564a0 | ||
|
|
6f376cf103 | ||
|
|
db49d53aaf | ||
|
|
164df33419 | ||
|
|
5224f13db2 | ||
|
|
34d7fb388d | ||
|
|
1436040d4c | ||
|
|
4464b2a21b | ||
|
|
7f4dda1b06 | ||
|
|
3dd119eeea | ||
|
|
a785a6054e | ||
|
|
efb51526a9 | ||
|
|
8b4228d616 | ||
|
|
5389dabe19 | ||
|
|
82b36e2ec8 | ||
|
|
203b8ec872 | ||
|
|
740f283ec1 | ||
|
|
4bb6db86f8 | ||
|
|
2f6d0bdcee | ||
|
|
38b501e004 | ||
|
|
3c92686f0a | ||
|
|
b740cdfc00 | ||
|
|
d066b5cd04 | ||
|
|
cdc9b62688 | ||
|
|
8d3d9493f0 | ||
|
|
d048365da3 | ||
|
|
0d70ae2a21 | ||
|
|
8ccb8e3c5b | ||
|
|
7205fb9b97 | ||
|
|
ec7558b9e0 | ||
|
|
662ccd467c | ||
|
|
4077254b01 | ||
|
|
0b1e78e127 | ||
|
|
6b2dbdd394 | ||
|
|
e6abe1b77f | ||
|
|
93246043ec | ||
|
|
8f9ef4ef5b | ||
|
|
5b37919574 | ||
|
|
6b4a81ee48 | ||
|
|
653117c2a9 | ||
|
|
b84deec601 | ||
|
|
7069e2a5a0 | ||
|
|
cc36af57bd | ||
|
|
7a7d32db81 | ||
|
|
ec80dc6f09 | ||
|
|
1c5c310f5a | ||
|
|
59a2a04fcc | ||
|
|
1c033ce635 | ||
|
|
33f8f7d7b3 | ||
|
|
17ff395f9a | ||
|
|
de189c5f18 | ||
|
|
aeae8d646a | ||
|
|
76db0b63ba | ||
|
|
5d7dd9b0ec | ||
|
|
1084f0d97f | ||
|
|
9a9939cfb3 | ||
|
|
fd58bbff6b | ||
|
|
c21fd45883 | ||
|
|
610ead22e8 | ||
|
|
16498627ce | ||
|
|
3bc79eebe3 | ||
|
|
e7e3853f81 | ||
|
|
4b4d828260 | ||
|
|
3f5afb9cac | ||
|
|
23e56d3ec1 | ||
|
|
781e57f5bf | ||
|
|
3759a41b83 | ||
|
|
2f7b112736 | ||
|
|
e19a6f5dcb | ||
|
|
0218f11f47 | ||
|
|
b3f6d991b5 | ||
|
|
dd37f6cbd6 | ||
|
|
bccfd22fc0 | ||
|
|
ee83f94bb0 | ||
|
|
f8d4b19cb9 | ||
|
|
b4db5e9561 | ||
|
|
77deac4fb9 | ||
|
|
fbea61bbc6 | ||
|
|
07d2b896c1 | ||
|
|
3a3ffa2307 | ||
|
|
cfae52a40a | ||
|
|
1e1e4b93c1 | ||
|
|
9fb1533b8f | ||
|
|
55d5469740 | ||
|
|
9e791efc82 | ||
|
|
43e65d91ea | ||
|
|
7af3c3d0b6 | ||
|
|
ecaf0aba3c | ||
|
|
ed3bef1840 | ||
|
|
4004427892 | ||
|
|
3b246fd7e6 | ||
|
|
bccb718cc2 | ||
|
|
22ba12172f | ||
|
|
1c1e7380e3 | ||
|
|
ef19634a13 | ||
|
|
4e09de4db2 | ||
|
|
305c37917f | ||
|
|
2607847061 | ||
|
|
a0742c52bb | ||
|
|
e48dc0808d | ||
|
|
708eefb383 | ||
|
|
6270607c6d | ||
|
|
c545399b96 | ||
|
|
bd9ef74ef7 | ||
|
|
f0d4c4c180 | ||
|
|
a6ce20a0fc | ||
|
|
d3759b3971 | ||
|
|
3fa2a8c2d8 | ||
|
|
6daaf42b38 | ||
|
|
924cdef6d9 | ||
|
|
297c7e833c | ||
|
|
6b0b6404fc | ||
|
|
8f5b94f5fd | ||
|
|
692bfccb6e | ||
|
|
41b6b739c0 | ||
|
|
d95559a53c | ||
|
|
04400eb2e4 | ||
|
|
e4128a5c91 | ||
|
|
a93d7633d4 | ||
|
|
dab9688410 | ||
|
|
b8a83f57b7 | ||
|
|
23bc87f2aa | ||
|
|
8aff5a1dab | ||
|
|
ac9ad8ec36 | ||
|
|
8271a39cdb | ||
|
|
509061f05b | ||
|
|
4888d75e72 | ||
|
|
0d89bfacdb | ||
|
|
3b884efca9 | ||
|
|
d5fe1432f8 | ||
|
|
c084fe6b3f | ||
|
|
944244ceff | ||
|
|
b4bd978791 | ||
|
|
b5f6a1cc20 | ||
|
|
fd23bd0434 | ||
|
|
aa18b25a71 | ||
|
|
d631c7dffa | ||
|
|
72f577aad2 | ||
|
|
5f307f92e0 | ||
|
|
21c993a7b3 | ||
|
|
c973e3c746 | ||
|
|
bf08aa7529 | ||
|
|
f5027fdcaf | ||
|
|
596a14e34f | ||
|
|
6c11ca1b75 | ||
|
|
0340bfc90d | ||
|
|
1e8b8b5b29 | ||
|
|
1094319e3e | ||
|
|
0a6c565eb3 | ||
|
|
dd8c3d5462 | ||
|
|
8580287092 | ||
|
|
2268f7db43 | ||
|
|
59e54eabef | ||
|
|
7e4b6683e6 | ||
|
|
c16a5814d4 | ||
|
|
be5881280f | ||
|
|
7650b0073a | ||
|
|
d5aa0e325e | ||
|
|
ce9164ec69 | ||
|
|
e44615f52b | ||
|
|
176966daab | ||
|
|
bf84e0d441 | ||
|
|
872a23c77d | ||
|
|
361a357088 | ||
|
|
9c87997dae | ||
|
|
cbef6c30c3 | ||
|
|
d7ffad1dd3 | ||
|
|
49c61e7ebb | ||
|
|
06dcc4ed96 | ||
|
|
6a10ae662c | ||
|
|
5c820ecc20 | ||
|
|
12b459df8c | ||
|
|
44493707e2 | ||
|
|
b3a99e38cc | ||
|
|
2816076789 | ||
|
|
22eee472dd | ||
|
|
4f0aa54c09 | ||
|
|
b16f364866 | ||
|
|
d618aaef32 | ||
|
|
aeaf8fd89c | ||
|
|
3ef034dda8 | ||
|
|
407642869a | ||
|
|
46fe9ac5cd | ||
|
|
674af15696 | ||
|
|
af28f95c60 | ||
|
|
ef7fd7548c | ||
|
|
4d07e20b05 | ||
|
|
353d765140 | ||
|
|
828e647019 | ||
|
|
bb5387fa5d | ||
|
|
4f51c5a433 | ||
|
|
4badac8e9e | ||
|
|
98281341b9 | ||
|
|
0a17c78a36 | ||
|
|
30e4052a76 | ||
|
|
2f169575e9 | ||
|
|
a449a4be29 | ||
|
|
a939431d48 | ||
|
|
e686bb0739 | ||
|
|
8179d6a30d | ||
|
|
1e8f6c0840 | ||
|
|
6644311c8b | ||
|
|
dedb5e23f7 | ||
|
|
5448859254 | ||
|
|
e05b33a6c2 | ||
|
|
ab58c01a0f | ||
|
|
232dfad13a | ||
|
|
b77a808921 | ||
|
|
3df0c5e32f | ||
|
|
d54f52474a | ||
|
|
be5cb1aa17 | ||
|
|
fda2d2bd59 | ||
|
|
80e6c90740 | ||
|
|
da72bd9819 | ||
|
|
7e7737d692 | ||
|
|
0a49213338 | ||
|
|
fa3ab678e1 | ||
|
|
0a67a3a9c4 | ||
|
|
bbb6ebb84e | ||
|
|
84d4888f5f | ||
|
|
ce252a0d45 | ||
|
|
90a77030a7 | ||
|
|
904ca746a6 | ||
|
|
988d755906 | ||
|
|
9f1cf0bbb0 | ||
|
|
fe4161e4d7 | ||
|
|
497c83eb7e | ||
|
|
2a60884abc | ||
|
|
2f6d56dd62 | ||
|
|
0408b6d655 | ||
|
|
1e078d03bb | ||
|
|
1d1103f39c | ||
|
|
06821f9781 | ||
|
|
50cbdc778f | ||
|
|
a4d6f2eba6 | ||
|
|
86f453593a | ||
|
|
9e1736e027 | ||
|
|
5a952987a3 | ||
|
|
0498a31c42 | ||
|
|
b9e9204e52 | ||
|
|
5113a417a1 | ||
|
|
e832455790 | ||
|
|
6b9f9f9b0e | ||
|
|
7312827d4d | ||
|
|
f2edc91dc6 | ||
|
|
5d726ef037 | ||
|
|
af418d2342 | ||
|
|
77d08b6dbe | ||
|
|
3a00bf83d6 | ||
|
|
742df8a25e | ||
|
|
2d2f0f02d6 | ||
|
|
62c3ca8286 | ||
|
|
1f9ef6c48f | ||
|
|
d5bdd9387a | ||
|
|
490b64575b | ||
|
|
84ea0a828c | ||
|
|
0e14ea4e32 | ||
|
|
af75f6cea7 | ||
|
|
9cf645e07f | ||
|
|
f0a0f078fc | ||
|
|
881e95b440 | ||
|
|
36d26d40a0 | ||
|
|
a90fe25cc4 | ||
|
|
ebeb5e0cb7 | ||
|
|
63b126967e | ||
|
|
bcff4b0e5a | ||
|
|
c7186ff95c | ||
|
|
feafa956f7 | ||
|
|
1e20016059 | ||
|
|
4de7a4c571 | ||
|
|
f9ed8c10ab | ||
|
|
802c89ffb3 | ||
|
|
1894dc8197 | ||
|
|
da6bc1a13e | ||
|
|
2f638ae32a | ||
|
|
9655d78642 | ||
|
|
2868baebab | ||
|
|
df035f6b19 | ||
|
|
9e73af891d | ||
|
|
cde82bc0cc | ||
|
|
555c126eb9 | ||
|
|
9a993b0364 | ||
|
|
f37484c6fe | ||
|
|
f4f8df6cfe | ||
|
|
904f835d4a | ||
|
|
05c2198569 | ||
|
|
13c0c129df | ||
|
|
e8dff30973 | ||
|
|
11a9bd523d | ||
|
|
56e81ada56 | ||
|
|
b101dceb2a | ||
|
|
e745312a10 | ||
|
|
754eb6bdb6 | ||
|
|
d81d6069fb | ||
|
|
d9e7bc545e | ||
|
|
5c6e3269fb | ||
|
|
80fffbd64b | ||
|
|
b523c779f5 | ||
|
|
0e68da5a2a | ||
|
|
dde09cb959 | ||
|
|
6d121ae6e4 | ||
|
|
b4db25dd18 | ||
|
|
d1bccc8c65 | ||
|
|
3aead05f42 | ||
|
|
4e7deba2ad | ||
|
|
41b9e92868 | ||
|
|
b64ebc6fcc | ||
|
|
756dbe7ce8 | ||
|
|
e7d2bcf108 | ||
|
|
ed76ee3e16 | ||
|
|
dcfc86e3af | ||
|
|
39a1f4a4c1 | ||
|
|
ad758b8d85 | ||
|
|
edc38edabf | ||
|
|
92f845c0e1 | ||
|
|
96c5c7b1df | ||
|
|
ddbd7d8bbc | ||
|
|
8816039bd9 | ||
|
|
79e41e329e | ||
|
|
32965f1af9 | ||
|
|
a627d2a38c | ||
|
|
8aa47a13e3 | ||
|
|
98cfea6f63 | ||
|
|
32c35b87f9 | ||
|
|
016ed951da | ||
|
|
df090cbe87 | ||
|
|
6694175a51 | ||
|
|
4567474418 | ||
|
|
9d04af9ecc | ||
|
|
1b33afd699 | ||
|
|
0d12588583 | ||
|
|
8f2e5288ff | ||
|
|
175a7baa60 | ||
|
|
4fbff20954 | ||
|
|
b3d565c91f | ||
|
|
c1da3ddbbf | ||
|
|
8ab8230adf | ||
|
|
b26d70b527 | ||
|
|
4f941ac5c0 | ||
|
|
11126521c9 | ||
|
|
85e1c85b52 | ||
|
|
9d27a25e5f | ||
|
|
7ec9d76545 | ||
|
|
86744b6fbb | ||
|
|
5e9014be8c | ||
|
|
78fbd6452b | ||
|
|
2a24423ad2 | ||
|
|
d12c9b434e | ||
|
|
507c966aa7 | ||
|
|
93c0c26843 | ||
|
|
0444b98802 | ||
|
|
b5c1c725be | ||
|
|
c51e6dba8d | ||
|
|
5b6a9fcca9 | ||
|
|
df8c3f0888 | ||
|
|
9fb6fc5b9a | ||
|
|
e3afcc6945 | ||
|
|
86612b6c05 | ||
|
|
05e64b342a | ||
|
|
6086d1a99d | ||
|
|
28dd758aa3 | ||
|
|
29da1db516 | ||
|
|
433489a9e6 | ||
|
|
81f916b7d3 | ||
|
|
4b5454c752 | ||
|
|
47852803f0 | ||
|
|
07d748c290 | ||
|
|
77a29574a6 | ||
|
|
442e3f2aa2 | ||
|
|
50f83859db | ||
|
|
51848ee9d7 | ||
|
|
c32113918e | ||
|
|
6dd5117f61 | ||
|
|
6cb31a9770 | ||
|
|
fc38d45c35 | ||
|
|
254bab33da | ||
|
|
f9f662679f | ||
|
|
bb39a2cac7 | ||
|
|
d176d86e2c | ||
|
|
4673aa412e | ||
|
|
6e198188ff | ||
|
|
24d1bf275a | ||
|
|
8c374f57ed | ||
|
|
b497436d4f | ||
|
|
520268002f | ||
|
|
1c2fe085b5 | ||
|
|
490ec63114 | ||
|
|
f8273f7db6 | ||
|
|
f968f0f257 | ||
|
|
772f6ffd21 | ||
|
|
2c1ab569a7 | ||
|
|
033e4e84f5 | ||
|
|
18495ed624 | ||
|
|
15816c8afd | ||
|
|
315df7b2cf | ||
|
|
04aaadcb39 | ||
|
|
4911c3b5b7 | ||
|
|
ccefe96665 | ||
|
|
88f67e4786 | ||
|
|
fd6d86eefc | ||
|
|
028d19f32d | ||
|
|
d790710ae7 | ||
|
|
28dfbdda93 | ||
|
|
60b6afb470 | ||
|
|
8bfe865759 | ||
|
|
0a06241e7c | ||
|
|
d55c59f298 | ||
|
|
84459c7196 | ||
|
|
e37f98267b | ||
|
|
8652331d1c | ||
|
|
9eeaac0c3e | ||
|
|
c17ccb455d | ||
|
|
24f400b123 | ||
|
|
1250e56dd6 | ||
|
|
0990011e74 | ||
|
|
fd7be5da99 | ||
|
|
c142d89952 | ||
|
|
ac8b6bba5c | ||
|
|
270040303c | ||
|
|
6b6f4dd017 | ||
|
|
ff7ec977e6 | ||
|
|
16a23d9f0f | ||
|
|
603117eb6b | ||
|
|
3895c03ba9 | ||
|
|
bc40f3f425 | ||
|
|
be5fb94837 | ||
|
|
8ef257abbc | ||
|
|
df840cca75 | ||
|
|
24e7a21839 | ||
|
|
f143fe7dcc | ||
|
|
676ed6b881 | ||
|
|
043066a2c8 | ||
|
|
938f7d2266 | ||
|
|
9016baddca | ||
|
|
b07620aacf | ||
|
|
b186f8e9d7 | ||
|
|
db47e1b69c | ||
|
|
4f51dfe4c5 | ||
|
|
2d0dadd9ac | ||
|
|
ba4ab06ae3 | ||
|
|
2ee919220a | ||
|
|
8976e94a1d | ||
|
|
86a8b0b30f | ||
|
|
487c6018bf | ||
|
|
e7705327f0 | ||
|
|
e5b57ec965 | ||
|
|
17341adf1c | ||
|
|
011ac131cf | ||
|
|
5e9821dce2 | ||
|
|
a06017c2c3 | ||
|
|
e30c3eafef | ||
|
|
7591f1010b | ||
|
|
b65e58c1ae | ||
|
|
4ee163742a | ||
|
|
54935438e1 | ||
|
|
7973951c37 | ||
|
|
f68ab3dfff | ||
|
|
0bd4de4504 | ||
|
|
83a7584475 | ||
|
|
e1116bbbbb | ||
|
|
83c46085fb | ||
|
|
5155d5bfb2 | ||
|
|
eb1db5eaa3 | ||
|
|
75387bbaef | ||
|
|
752a92bd8b | ||
|
|
2e52a63b0d | ||
|
|
74619269f0 | ||
|
|
09872301bd | ||
|
|
6fe5264ae2 | ||
|
|
4364fb9628 | ||
|
|
4a14e9ea4e | ||
|
|
dbf7a479b6 | ||
|
|
a0131a96cb | ||
|
|
88647c63ba | ||
|
|
fd38e8e0af | ||
|
|
430b247dfc | ||
|
|
7ed8f59dc8 | ||
|
|
36de35c6d9 | ||
|
|
fcc8f9f164 | ||
|
|
d7bc192804 | ||
|
|
aea4315435 | ||
|
|
33604550ce | ||
|
|
27ce789023 | ||
|
|
37c1331aba | ||
|
|
3a898289b0 | ||
|
|
e7745033df | ||
|
|
ad31e02616 | ||
|
|
64f439600b | ||
|
|
6349f29aed | ||
|
|
45544c2b1e | ||
|
|
3593573ed2 | ||
|
|
cf9c065cf8 | ||
|
|
ec1607e825 | ||
|
|
e0bc437ddb | ||
|
|
53e4fee4db |
66
.eslintrc
66
.eslintrc
@@ -2,65 +2,32 @@
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
"es2022": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{ "SwitchCase": 1 }
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs"
|
||||
],
|
||||
"space-unary-ops": [
|
||||
"error",
|
||||
{ "words": true }
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"off"
|
||||
],
|
||||
"semi": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"camelcase": [
|
||||
"off"
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"warn"
|
||||
],
|
||||
"no-redeclare": [
|
||||
"warn"
|
||||
],
|
||||
"no-console": [
|
||||
"warn"
|
||||
],
|
||||
"no-extra-boolean-cast": [
|
||||
"off"
|
||||
],
|
||||
"no-control-regex": [
|
||||
"off"
|
||||
],
|
||||
"space-before-blocks": "warn",
|
||||
"keyword-spacing": "warn",
|
||||
"comma-spacing": "warn",
|
||||
"key-spacing": "warn"
|
||||
"indent": "off",
|
||||
"brace-style": "off",
|
||||
"no-mixed-spaces-and-tabs": "off",
|
||||
"no-useless-escape": "off",
|
||||
"space-unary-ops": ["error", { "words": true }],
|
||||
"linebreak-style": "off",
|
||||
"quotes": ["off"],
|
||||
"semi": "off",
|
||||
"camelcase": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-console": ["warn"],
|
||||
"no-extra-boolean-cast": ["off"],
|
||||
"no-control-regex": ["off"]
|
||||
},
|
||||
"root": true,
|
||||
"globals": {
|
||||
"frappe": true,
|
||||
"Vue": true,
|
||||
"SetVueGlobals": true,
|
||||
"erpnext": true,
|
||||
"hub": true,
|
||||
"$": true,
|
||||
@@ -97,8 +64,10 @@
|
||||
"is_null": true,
|
||||
"in_list": true,
|
||||
"has_common": true,
|
||||
"posthog": true,
|
||||
"has_words": true,
|
||||
"validate_email": true,
|
||||
"open_web_template_values_editor": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
"format_currency": true,
|
||||
@@ -154,7 +123,6 @@
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"onScan": true,
|
||||
"html2canvas": true,
|
||||
"extend_cscript": true,
|
||||
"localforage": true
|
||||
}
|
||||
|
||||
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -68,6 +68,6 @@ if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
||||
wait $wkpid
|
||||
|
||||
bench start &> bench_run_logs.txt &
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
CI=Yes bench build --app frappe &
|
||||
bench --site test_site reinstall --yes
|
||||
|
||||
22
.github/workflows/initiate_release.yml
vendored
22
.github/workflows/initiate_release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
stable-release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -30,3 +30,23 @@ jobs:
|
||||
head: version-${{ matrix.version }}-hotfix
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
beta-release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
with:
|
||||
route: POST /repos/{owner}/{repo}/pulls
|
||||
owner: frappe
|
||||
repo: erpnext
|
||||
title: |-
|
||||
"chore: release v15 beta"
|
||||
body: "Automated beta release."
|
||||
base: version-15-beta
|
||||
head: develop
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
9
.github/workflows/linters.yml
vendored
9
.github/workflows/linters.yml
vendored
@@ -9,21 +9,22 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep==0.97.0
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
||||
67
.github/workflows/patch.yml
vendored
67
.github/workflows/patch.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
@@ -43,14 +43,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: "gabrielfalcao/pyenv-action@v9"
|
||||
uses: "actions/setup-python@v4"
|
||||
with:
|
||||
versions: 3.10:latest, 3.7:latest
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -92,7 +92,6 @@ jobs:
|
||||
- name: Install
|
||||
run: |
|
||||
pip install frappe-bench
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
@@ -101,42 +100,60 @@ jobs:
|
||||
- name: Run Patch Tests
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://erpnext.com/files/v10-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
|
||||
bench remove-app payments --force
|
||||
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
|
||||
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
wget https://erpnext.com/files/v13-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.7')
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version-hotfix"
|
||||
|
||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
function update_to_version() {
|
||||
version=$1
|
||||
|
||||
git -C "apps/frappe" checkout -q -f $branch_name
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
branch_name="version-$version-hotfix"
|
||||
echo "Updating to v$version"
|
||||
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench setup env
|
||||
bench pip install -e ./apps/payments
|
||||
bench pip install -e ./apps/erpnext
|
||||
# Fetch and checkout branches
|
||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/frappe" checkout -q -f $branch_name
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
|
||||
bench --site test_site migrate
|
||||
done
|
||||
# Resetup env and install apps
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env
|
||||
bench pip install -e ./apps/erpnext
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 14
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env
|
||||
bench pip install -e ./apps/payments
|
||||
bench pip install -e ./apps/erpnext
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
bench --site test_site install-app payments
|
||||
|
||||
- name: Show bench output
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
cd ~/frappe-bench
|
||||
cat bench_start.log || true
|
||||
cd logs
|
||||
for f in ./*.log*; do
|
||||
echo "Printing log: $f";
|
||||
cat $f
|
||||
done
|
||||
|
||||
38
.github/workflows/release_notes.yml
vendored
Normal file
38
.github/workflows/release_notes.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# This action:
|
||||
#
|
||||
# 1. Generates release notes using github API.
|
||||
# 2. Strips unnecessary info like chore/style etc from notes.
|
||||
# 3. Updates release info.
|
||||
|
||||
# This action needs to be maintained on all branches that do releases.
|
||||
|
||||
name: 'Release Notes'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Tag of release like v13.0.0'
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
regen-notes:
|
||||
name: 'Regenerate release notes'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Update notes
|
||||
run: |
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
|
||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
|
||||
2
.github/workflows/semantic-commits.yml
vendored
2
.github/workflows/semantic-commits.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Check commit titles
|
||||
|
||||
14
.github/workflows/server-tests-mariadb.yml
vendored
14
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,11 +7,9 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
user:
|
||||
@@ -71,7 +69,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -125,6 +123,10 @@ jobs:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Show bench output
|
||||
if: ${{ always() }}
|
||||
run: cat ~/frappe-bench/bench_start.log || true
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
@@ -15,6 +15,8 @@ pull_request_rules:
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
|
||||
@@ -16,12 +16,31 @@ repos:
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.44.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
types_or: [javascript]
|
||||
args: ['--quiet']
|
||||
# Ignore any files that might contain jinja / bundles
|
||||
exclude: |
|
||||
(?x)^(
|
||||
erpnext/public/dist/.*|
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-bugbear',
|
||||
'flake8-tuple',
|
||||
]
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
"creation": "2020-07-17 11:25:34.593061",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
|
||||
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 12:24:49.144210",
|
||||
"modified": "2023-07-19 13:13:13.307073",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Variance",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Budget Variance Report",
|
||||
"roles": [],
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
"creation": "2020-07-17 11:25:34.448572",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
|
||||
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 12:33:48.888943",
|
||||
"modified": "2023-07-19 13:08:56.470390",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"roles": [],
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
|
||||
@@ -136,7 +136,7 @@ def convert_deferred_revenue_to_income(
|
||||
send_mail(deferred_process)
|
||||
|
||||
|
||||
def get_booking_dates(doc, item, posting_date=None):
|
||||
def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
|
||||
if not posting_date:
|
||||
posting_date = add_days(today(), -1)
|
||||
|
||||
@@ -146,39 +146,42 @@ def get_booking_dates(doc, item, posting_date=None):
|
||||
"deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
|
||||
)
|
||||
|
||||
prev_gl_entry = frappe.db.sql(
|
||||
"""
|
||||
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
|
||||
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
order by posting_date desc limit 1
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
if not prev_posting_date:
|
||||
prev_gl_entry = frappe.db.sql(
|
||||
"""
|
||||
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
|
||||
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
order by posting_date desc limit 1
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
prev_gl_via_je = frappe.db.sql(
|
||||
"""
|
||||
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
|
||||
WHERE p.name = c.parent and p.company=%s and c.account=%s
|
||||
and c.reference_type=%s and c.reference_name=%s
|
||||
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
prev_gl_via_je = frappe.db.sql(
|
||||
"""
|
||||
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
|
||||
WHERE p.name = c.parent and p.company=%s and c.account=%s
|
||||
and c.reference_type=%s and c.reference_name=%s
|
||||
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
|
||||
""",
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if prev_gl_via_je:
|
||||
if (not prev_gl_entry) or (
|
||||
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
|
||||
):
|
||||
prev_gl_entry = prev_gl_via_je
|
||||
if prev_gl_via_je:
|
||||
if (not prev_gl_entry) or (
|
||||
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
|
||||
):
|
||||
prev_gl_entry = prev_gl_via_je
|
||||
|
||||
if prev_gl_entry:
|
||||
start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
|
||||
else:
|
||||
start_date = item.service_start_date
|
||||
|
||||
if prev_gl_entry:
|
||||
start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
|
||||
else:
|
||||
start_date = item.service_start_date
|
||||
|
||||
start_date = getdate(add_days(prev_posting_date, 1))
|
||||
end_date = get_last_day(start_date)
|
||||
if end_date >= item.service_end_date:
|
||||
end_date = item.service_end_date
|
||||
@@ -338,12 +341,18 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
|
||||
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
|
||||
item,
|
||||
via_journal_entry,
|
||||
submit_journal_entry,
|
||||
book_deferred_entries_based_on,
|
||||
prev_posting_date=None,
|
||||
):
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(
|
||||
doc, item, posting_date=posting_date, prev_posting_date=prev_posting_date
|
||||
)
|
||||
if not (start_date and end_date):
|
||||
return
|
||||
|
||||
@@ -377,9 +386,12 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
if not amount:
|
||||
return
|
||||
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
@@ -388,7 +400,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
end_date,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
@@ -404,7 +416,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
end_date,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
@@ -418,7 +430,11 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
|
||||
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
|
||||
_book_deferred_revenue_or_expense(
|
||||
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
|
||||
item,
|
||||
via_journal_entry,
|
||||
submit_journal_entry,
|
||||
book_deferred_entries_based_on,
|
||||
prev_posting_date,
|
||||
)
|
||||
|
||||
via_journal_entry = cint(
|
||||
|
||||
@@ -1,67 +1,83 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on('Account', {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch('parent_account', 'report_type', 'report_type');
|
||||
frm.add_fetch('parent_account', 'root_type', 'root_type');
|
||||
frappe.ui.form.on("Account", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("parent_account", "report_type", "report_type");
|
||||
frm.add_fetch("parent_account", "root_type", "root_type");
|
||||
},
|
||||
onload: function(frm) {
|
||||
frm.set_query('parent_account', function(doc) {
|
||||
onload: function (frm) {
|
||||
frm.set_query("parent_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 1,
|
||||
"company": doc.company
|
||||
}
|
||||
is_group: 1,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frm.toggle_display('account_name', frm.is_new());
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display("account_name", frm.is_new());
|
||||
|
||||
// hide fields if group
|
||||
frm.toggle_display(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0);
|
||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||
|
||||
// disable fields
|
||||
frm.toggle_enable(['is_group', 'company'], false);
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
|
||||
if (cint(frm.doc.is_group) == 0) {
|
||||
frm.toggle_display('freeze_account', frm.doc.__onload
|
||||
&& frm.doc.__onload.can_freeze_account);
|
||||
frm.toggle_display(
|
||||
"freeze_account",
|
||||
frm.doc.__onload && frm.doc.__onload.can_freeze_account
|
||||
);
|
||||
}
|
||||
|
||||
// read-only for root accounts
|
||||
if (!frm.is_new()) {
|
||||
if (!frm.doc.parent_account) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root account and cannot be edited."));
|
||||
frm.set_intro(
|
||||
__("This is a root account and cannot be edited.")
|
||||
);
|
||||
} else {
|
||||
// credit days and type if customer or supplier
|
||||
frm.set_intro(null);
|
||||
frm.trigger('account_type');
|
||||
frm.trigger("account_type");
|
||||
// show / hide convert buttons
|
||||
frm.trigger('add_toolbar_buttons');
|
||||
frm.trigger("add_toolbar_buttons");
|
||||
}
|
||||
if (frm.has_perm('write')) {
|
||||
frm.add_custom_button(__('Merge Account'), function () {
|
||||
frm.trigger("merge_account");
|
||||
}, __('Actions'));
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
}, __('Actions'));
|
||||
if (frm.has_perm("write")) {
|
||||
frm.add_custom_button(
|
||||
__("Merge Account"),
|
||||
function () {
|
||||
frm.trigger("merge_account");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Update Account Name / Number"),
|
||||
function () {
|
||||
frm.trigger("update_account_number");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
account_type: function (frm) {
|
||||
if (frm.doc.is_group == 0) {
|
||||
frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax');
|
||||
frm.toggle_display('warehouse', frm.doc.account_type == 'Stock');
|
||||
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
|
||||
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
|
||||
}
|
||||
},
|
||||
add_toolbar_buttons: function(frm) {
|
||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
}, __('View'));
|
||||
add_toolbar_buttons: function (frm) {
|
||||
frm.add_custom_button(
|
||||
__("Chart of Accounts"),
|
||||
() => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
|
||||
if (frm.doc.is_group == 1) {
|
||||
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||
@@ -79,84 +95,88 @@ frappe.ui.form.on('Account', {
|
||||
frm.add_custom_button(__('General Ledger'), function () {
|
||||
frappe.route_options = {
|
||||
"account": frm.doc.name,
|
||||
"from_date": frappe.sys_defaults.year_start_date,
|
||||
"to_date": frappe.sys_defaults.year_end_date,
|
||||
"from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
"to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
"company": frm.doc.company
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(__('Convert to Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_ledger_to_group',
|
||||
callback: function() {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
}, __('Actions'));
|
||||
frm.add_custom_button(
|
||||
__("Convert to Group"),
|
||||
function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "convert_ledger_to_group",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
merge_account: function(frm) {
|
||||
merge_account: function (frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Merge with Existing Account'),
|
||||
title: __("Merge with Existing Account"),
|
||||
fields: [
|
||||
{
|
||||
"label" : "Name",
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"default": frm.doc.name
|
||||
}
|
||||
label: "Name",
|
||||
fieldname: "name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: frm.doc.name,
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
primary_action: function () {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.account.account.merge_account",
|
||||
args: {
|
||||
old: frm.doc.name,
|
||||
new: data.name,
|
||||
is_group: frm.doc.is_group,
|
||||
root_type: frm.doc.root_type,
|
||||
company: frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message) {
|
||||
frappe.set_route("Form", "Account", r.message);
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Merge')
|
||||
primary_action_label: __("Merge"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
update_account_number: function(frm) {
|
||||
update_account_number: function (frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Update Account Number / Name'),
|
||||
title: __("Update Account Number / Name"),
|
||||
fields: [
|
||||
{
|
||||
"label": "Account Name",
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"default": frm.doc.account_name
|
||||
label: "Account Name",
|
||||
fieldname: "account_name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default: frm.doc.account_name,
|
||||
},
|
||||
{
|
||||
"label": "Account Number",
|
||||
"fieldname": "account_number",
|
||||
"fieldtype": "Data",
|
||||
"default": frm.doc.account_number
|
||||
}
|
||||
label: "Account Number",
|
||||
fieldname: "account_number",
|
||||
fieldtype: "Data",
|
||||
default: frm.doc.account_number,
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
primary_action: function () {
|
||||
var data = d.get_values();
|
||||
if(data.account_number === frm.doc.account_number && data.account_name === frm.doc.account_name) {
|
||||
if (
|
||||
data.account_number === frm.doc.account_number &&
|
||||
data.account_name === frm.doc.account_name
|
||||
) {
|
||||
d.hide();
|
||||
return;
|
||||
}
|
||||
@@ -166,23 +186,29 @@ frappe.ui.form.on('Account', {
|
||||
args: {
|
||||
account_number: data.account_number,
|
||||
account_name: data.account_name,
|
||||
name: frm.doc.name
|
||||
name: frm.doc.name,
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message) {
|
||||
frappe.set_route("Form", "Account", r.message);
|
||||
} else {
|
||||
frm.set_value("account_number", data.account_number);
|
||||
frm.set_value("account_name", data.account_name);
|
||||
frm.set_value(
|
||||
"account_number",
|
||||
data.account_number
|
||||
);
|
||||
frm.set_value(
|
||||
"account_name",
|
||||
data.account_name
|
||||
);
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Update')
|
||||
primary_action_label: __("Update"),
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -192,7 +192,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-11 16:08:46.983677",
|
||||
"modified": "2023-07-20 18:18:44.405723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -243,7 +243,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAccountMergeError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Account(NestedSet):
|
||||
nsm_parent_field = "parent_account"
|
||||
|
||||
@@ -45,6 +49,7 @@ class Account(NestedSet):
|
||||
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_group_or_ledger()
|
||||
@@ -55,6 +60,20 @@ class Account(NestedSet):
|
||||
self.validate_account_currency()
|
||||
self.validate_root_company_and_sync_account_to_children()
|
||||
|
||||
def validate_parent_child_account_type(self):
|
||||
if self.parent_account:
|
||||
if self.account_type in [
|
||||
"Direct Income",
|
||||
"Indirect Income",
|
||||
"Current Asset",
|
||||
"Current Liability",
|
||||
"Direct Expense",
|
||||
"Indirect Expense",
|
||||
]:
|
||||
parent_account_type = frappe.db.get_value("Account", self.parent_account, ["account_type"])
|
||||
if parent_account_type == self.account_type:
|
||||
throw(_("Only Parent can be of type {0}").format(self.account_type))
|
||||
|
||||
def validate_parent(self):
|
||||
"""Fetch Parent Details and validate parent account"""
|
||||
if self.parent_account:
|
||||
@@ -445,25 +464,34 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new, is_group, root_type, company):
|
||||
def merge_account(old, new):
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
|
||||
if not new_account:
|
||||
throw(_("Account {0} does not exist").format(new))
|
||||
|
||||
if (new_account.is_group, new_account.root_type, new_account.company) != (
|
||||
cint(is_group),
|
||||
root_type,
|
||||
company,
|
||||
if (
|
||||
cint(new_account.is_group),
|
||||
new_account.root_type,
|
||||
new_account.company,
|
||||
cstr(new_account.account_currency),
|
||||
) != (
|
||||
cint(old_account.is_group),
|
||||
old_account.root_type,
|
||||
old_account.company,
|
||||
cstr(old_account.account_currency),
|
||||
):
|
||||
throw(
|
||||
_(
|
||||
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
|
||||
)
|
||||
msg=_(
|
||||
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
|
||||
),
|
||||
title=("Invalid Accounts"),
|
||||
exc=InvalidAccountMergeError,
|
||||
)
|
||||
|
||||
if is_group and new_account.parent_account == old:
|
||||
if old_account.is_group and new_account.parent_account == old:
|
||||
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
|
||||
|
||||
frappe.rename_doc("Account", old, new, merge=1, force=1)
|
||||
|
||||
@@ -194,8 +194,8 @@ frappe.treeview_settings["Account"] = {
|
||||
click: function(node, btn) {
|
||||
frappe.route_options = {
|
||||
"account": node.label,
|
||||
"from_date": frappe.sys_defaults.year_start_date,
|
||||
"to_date": frappe.sys_defaults.year_end_date,
|
||||
"from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
"to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
"company": frappe.treeview_settings['Account'].treeview.page.fields_dict.company.get_value()
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -437,12 +437,20 @@
|
||||
},
|
||||
"Sales": {
|
||||
"Sales from Other Regions": {
|
||||
"Sales from Other Region": {}
|
||||
"Sales from Other Region": {
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
},
|
||||
"Sales of same region": {
|
||||
"Management Consultancy Fees 1": {},
|
||||
"Sales Account": {},
|
||||
"Sales of I/C": {}
|
||||
"Management Consultancy Fees 1": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sales Account": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sales of I/C": {
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root_type": "Income"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -69,8 +69,7 @@
|
||||
"Persediaan Barang": {
|
||||
"Persediaan Barang": {
|
||||
"account_number": "1141.000",
|
||||
"account_type": "Stock",
|
||||
"is_group": 1
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Uang Muka Pembelian": {
|
||||
"Uang Muka Pembelian": {
|
||||
@@ -670,7 +669,8 @@
|
||||
},
|
||||
"Penjualan Barang Dagangan": {
|
||||
"Penjualan": {
|
||||
"account_number": "4110.000"
|
||||
"account_number": "4110.000",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Potongan Penjualan": {
|
||||
"account_number": "4130.000"
|
||||
|
||||
@@ -109,8 +109,7 @@
|
||||
}
|
||||
},
|
||||
"INVENTARIOS": {
|
||||
"account_type": "Stock",
|
||||
"is_group": 1
|
||||
"account_type": "Stock"
|
||||
}
|
||||
},
|
||||
"ACTIVO LARGO PLAZO": {
|
||||
@@ -398,10 +397,18 @@
|
||||
"INGRESOS POR SERVICIOS 1": {}
|
||||
},
|
||||
"VENTAS": {
|
||||
"VENTAS EXPORTACION": {},
|
||||
"VENTAS INMUEBLES": {},
|
||||
"VENTAS NACIONALES": {},
|
||||
"VENTAS NACIONALES AL DETAL": {}
|
||||
"VENTAS EXPORTACION": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"VENTAS INMUEBLES": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"VENTAS NACIONALES": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"VENTAS NACIONALES AL DETAL": {
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,75 +2,13 @@
|
||||
"country_code": "nl",
|
||||
"name": "Netherlands - Grootboekschema",
|
||||
"tree": {
|
||||
"FABRIKAGEREKENINGEN": {
|
||||
"is_group": 1,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"FINANCIELE REKENINGEN, KORTLOPENDE VORDERINGEN EN SCHULDEN": {
|
||||
"Bank": {
|
||||
"RABO Bank": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"KORTLOPENDE SCHULDEN": {
|
||||
"Af te dragen Btw-verlegd": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Afdracht loonheffing": {},
|
||||
"Btw af te dragen hoog": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw af te dragen laag": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw af te dragen overig": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw oude jaren": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen hoog": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen laag": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen overig": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw-afdracht": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Crediteuren": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Dividend": {},
|
||||
"Dividendbelasting": {},
|
||||
"Energiekosten 1": {},
|
||||
"Investeringsaftrek": {},
|
||||
"Loonheffing": {},
|
||||
"Overige te betalen posten": {},
|
||||
"Pensioenpremies 1": {},
|
||||
"Premie WIR": {},
|
||||
"Rekening-courant inkoopvereniging": {},
|
||||
"Rente": {},
|
||||
"Sociale lasten 1": {},
|
||||
"Stock Recieved niet gefactureerd": {
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Tanti\u00e8mes 1": {},
|
||||
"Te vorderen Btw-verlegd": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Telefoon/telefax 1": {},
|
||||
"Termijnen onderh. werk": {},
|
||||
"Vakantiedagen": {},
|
||||
"Vakantiegeld 1": {},
|
||||
"Vakantiezegels": {},
|
||||
"Vennootschapsbelasting": {},
|
||||
"Vooruit ontvangen bedr.": {}
|
||||
},
|
||||
},
|
||||
"LIQUIDE MIDDELEN": {
|
||||
"ABN-AMRO bank": {},
|
||||
"Bankbetaalkaarten": {},
|
||||
@@ -91,6 +29,110 @@
|
||||
},
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"TUSSENREKENINGEN": {
|
||||
"Betaalwijze cadeaubonnen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze chipknip": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze contant": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze pin": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland onbelast": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland verlegd": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Kassa 1": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Kassa 2": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Netto lonen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tegenrekening Inkopen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. autom. betalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. autom. loonbetalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. cadeaubonbetalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening balans": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening chipknip": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening correcties": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening pin": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Vraagposten": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
|
||||
"Emballage": {},
|
||||
"Gereed product 1": {},
|
||||
"Gereed product 2": {},
|
||||
"Goederen 1": {},
|
||||
"Goederen 2": {},
|
||||
"Goederen in consignatie": {},
|
||||
"Goederen onderweg": {},
|
||||
"Grondstoffen 1": {},
|
||||
"Grondstoffen 2": {},
|
||||
"Halffabrikaten 1": {},
|
||||
"Halffabrikaten 2": {},
|
||||
"Hulpstoffen 1": {},
|
||||
"Hulpstoffen 2": {},
|
||||
"Kantoorbenodigdheden": {},
|
||||
"Onderhanden werk": {},
|
||||
"Verpakkingsmateriaal": {},
|
||||
"Zegels": {},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"VORDERINGEN": {
|
||||
"Debiteuren": {
|
||||
"account_type": "Receivable"
|
||||
@@ -104,278 +146,299 @@
|
||||
"Voorziening dubieuze debiteuren": {}
|
||||
},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"INDIRECTE KOSTEN": {
|
||||
},
|
||||
"KORTLOPENDE SCHULDEN": {
|
||||
"Af te dragen Btw-verlegd": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Afdracht loonheffing": {},
|
||||
"Btw af te dragen hoog": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw af te dragen laag": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw af te dragen overig": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw oude jaren": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen hoog": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen laag": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw te vorderen overig": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Btw-afdracht": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Crediteuren": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Dividend": {},
|
||||
"Dividendbelasting": {},
|
||||
"Energiekosten 1": {},
|
||||
"Investeringsaftrek": {},
|
||||
"Loonheffing": {},
|
||||
"Overige te betalen posten": {},
|
||||
"Pensioenpremies 1": {},
|
||||
"Premie WIR": {},
|
||||
"Rekening-courant inkoopvereniging": {},
|
||||
"Rente": {},
|
||||
"Sociale lasten 1": {},
|
||||
"Stock Recieved niet gefactureerd": {
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Tanti\u00e8mes 1": {},
|
||||
"Te vorderen Btw-verlegd": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Telefoon/telefax 1": {},
|
||||
"Termijnen onderh. werk": {},
|
||||
"Vakantiedagen": {},
|
||||
"Vakantiegeld 1": {},
|
||||
"Vakantiezegels": {},
|
||||
"Vennootschapsbelasting": {},
|
||||
"Vooruit ontvangen bedr.": {},
|
||||
"is_group": 1,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"FABRIKAGEREKENINGEN": {
|
||||
"is_group": 1,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"KOSTENREKENINGEN": {
|
||||
"AFSCHRIJVINGEN": {
|
||||
"Aanhangwagens": {},
|
||||
"Aankoopkosten": {},
|
||||
"Aanloopkosten": {},
|
||||
"Auteursrechten": {},
|
||||
"Bedrijfsgebouwen": {},
|
||||
"Bedrijfsinventaris": {
|
||||
"root_type": "Expense",
|
||||
"INDIRECTE KOSTEN": {
|
||||
"is_group": 1,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"KOSTENREKENINGEN": {
|
||||
"AFSCHRIJVINGEN": {
|
||||
"Aanhangwagens": {},
|
||||
"Aankoopkosten": {},
|
||||
"Aanloopkosten": {},
|
||||
"Auteursrechten": {},
|
||||
"Bedrijfsgebouwen": {},
|
||||
"Bedrijfsinventaris": {
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Drankvergunningen": {},
|
||||
"Fabrieksinventaris": {
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Gebouwen": {},
|
||||
"Gereedschappen": {},
|
||||
"Goodwill": {},
|
||||
"Grondverbetering": {},
|
||||
"Heftrucks": {},
|
||||
"Kantine-inventaris": {},
|
||||
"Kantoorinventaris": {
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Kantoormachines": {},
|
||||
"Licenties": {},
|
||||
"Machines 1": {},
|
||||
"Magazijninventaris": {},
|
||||
"Octrooien": {},
|
||||
"Ontwikkelingskosten": {},
|
||||
"Pachtersinvestering": {},
|
||||
"Parkeerplaats": {},
|
||||
"Personenauto's": {
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Rijwielen en bromfietsen": {},
|
||||
"Tonnagevergunningen": {},
|
||||
"Verbouwingen": {},
|
||||
"Vergunningen": {},
|
||||
"Voorraadverschillen": {},
|
||||
"Vrachtauto's": {},
|
||||
"Winkels": {},
|
||||
"Woon-winkelhuis": {},
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Drankvergunningen": {},
|
||||
"Fabrieksinventaris": {
|
||||
"account_type": "Depreciation"
|
||||
"ALGEMENE KOSTEN": {
|
||||
"Accountantskosten": {},
|
||||
"Advieskosten": {},
|
||||
"Assuranties 1": {},
|
||||
"Bankkosten": {},
|
||||
"Juridische kosten": {},
|
||||
"Overige algemene kosten": {},
|
||||
"Toev. Ass. eigen risico": {}
|
||||
},
|
||||
"Gebouwen": {},
|
||||
"Gereedschappen": {},
|
||||
"Goodwill": {},
|
||||
"Grondverbetering": {},
|
||||
"Heftrucks": {},
|
||||
"Kantine-inventaris": {},
|
||||
"Kantoorinventaris": {
|
||||
"account_type": "Depreciation"
|
||||
"BEDRIJFSKOSTEN": {
|
||||
"Assuranties 2": {},
|
||||
"Energie (krachtstroom)": {},
|
||||
"Gereedschappen 1": {},
|
||||
"Hulpmaterialen 1": {},
|
||||
"Huur inventaris": {},
|
||||
"Huur machines": {},
|
||||
"Leasing invent.operational": {},
|
||||
"Leasing mach. operational": {},
|
||||
"Onderhoud inventaris": {},
|
||||
"Onderhoud machines": {},
|
||||
"Ophalen/vervoer afval": {},
|
||||
"Overige bedrijfskosten": {}
|
||||
},
|
||||
"Kantoormachines": {},
|
||||
"Licenties": {},
|
||||
"Machines 1": {},
|
||||
"Magazijninventaris": {},
|
||||
"Octrooien": {},
|
||||
"Ontwikkelingskosten": {},
|
||||
"Pachtersinvestering": {},
|
||||
"Parkeerplaats": {},
|
||||
"Personenauto's": {
|
||||
"account_type": "Depreciation"
|
||||
"FINANCIERINGSKOSTEN 1": {
|
||||
"Overige rentebaten": {},
|
||||
"Overige rentelasten": {},
|
||||
"Rente bankkrediet": {},
|
||||
"Rente huurkoopcontracten": {},
|
||||
"Rente hypotheek": {},
|
||||
"Rente leasecontracten": {},
|
||||
"Rente lening o/g": {},
|
||||
"Rente lening u/g": {}
|
||||
},
|
||||
"Rijwielen en bromfietsen": {},
|
||||
"Tonnagevergunningen": {},
|
||||
"Verbouwingen": {},
|
||||
"Vergunningen": {},
|
||||
"Voorraadverschillen": {},
|
||||
"Vrachtauto's": {},
|
||||
"Winkels": {},
|
||||
"Woon-winkelhuis": {},
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"ALGEMENE KOSTEN": {
|
||||
"Accountantskosten": {},
|
||||
"Advieskosten": {},
|
||||
"Assuranties 1": {},
|
||||
"Bankkosten": {},
|
||||
"Juridische kosten": {},
|
||||
"Overige algemene kosten": {},
|
||||
"Toev. Ass. eigen risico": {}
|
||||
},
|
||||
"BEDRIJFSKOSTEN": {
|
||||
"Assuranties 2": {},
|
||||
"Energie (krachtstroom)": {},
|
||||
"Gereedschappen 1": {},
|
||||
"Hulpmaterialen 1": {},
|
||||
"Huur inventaris": {},
|
||||
"Huur machines": {},
|
||||
"Leasing invent.operational": {},
|
||||
"Leasing mach. operational": {},
|
||||
"Onderhoud inventaris": {},
|
||||
"Onderhoud machines": {},
|
||||
"Ophalen/vervoer afval": {},
|
||||
"Overige bedrijfskosten": {}
|
||||
},
|
||||
"FINANCIERINGSKOSTEN 1": {
|
||||
"Overige rentebaten": {},
|
||||
"Overige rentelasten": {},
|
||||
"Rente bankkrediet": {},
|
||||
"Rente huurkoopcontracten": {},
|
||||
"Rente hypotheek": {},
|
||||
"Rente leasecontracten": {},
|
||||
"Rente lening o/g": {},
|
||||
"Rente lening u/g": {}
|
||||
},
|
||||
"HUISVESTINGSKOSTEN": {
|
||||
"Assurantie onroerend goed": {},
|
||||
"Belastingen onr. Goed": {},
|
||||
"Energiekosten": {},
|
||||
"Groot onderhoud onr. Goed": {},
|
||||
"Huur": {},
|
||||
"Huurwaarde woongedeelte": {},
|
||||
"Onderhoud onroerend goed": {},
|
||||
"Ontvangen huren": {},
|
||||
"Overige huisvestingskosten": {},
|
||||
"Pacht": {},
|
||||
"Schoonmaakkosten": {},
|
||||
"Toevoeging egalisatieres. Groot onderhoud": {}
|
||||
},
|
||||
"KANTOORKOSTEN": {
|
||||
"Administratiekosten": {},
|
||||
"Contributies/abonnementen": {},
|
||||
"Huur kantoorapparatuur": {},
|
||||
"Internetaansluiting": {},
|
||||
"Kantoorbenodigdh./drukw.": {},
|
||||
"Onderhoud kantoorinvent.": {},
|
||||
"Overige kantoorkosten": {},
|
||||
"Porti": {},
|
||||
"Telefoon/telefax": {}
|
||||
},
|
||||
"OVERIGE BATEN EN LASTEN": {
|
||||
"Betaalde schadevergoed.": {},
|
||||
"Boekverlies vaste activa": {},
|
||||
"Boekwinst van vaste activa": {},
|
||||
"K.O. regeling OB": {},
|
||||
"Kasverschillen": {},
|
||||
"Kosten loonbelasting": {},
|
||||
"Kosten omzetbelasting": {},
|
||||
"Nadelige koersverschillen": {},
|
||||
"Naheffing bedrijfsver.": {},
|
||||
"Ontvangen schadevergoed.": {},
|
||||
"Overige baten": {},
|
||||
"Overige lasten": {},
|
||||
"Voordelige koersverschil.": {}
|
||||
},
|
||||
"PERSONEELSKOSTEN": {
|
||||
"Autokostenvergoeding": {},
|
||||
"Bedrijfskleding": {},
|
||||
"Belastingvrije uitkeringen": {},
|
||||
"Bijzondere beloningen": {},
|
||||
"Congressen, seminars en symposia": {},
|
||||
"Gereedschapsgeld": {},
|
||||
"Geschenken personeel": {},
|
||||
"Gratificaties": {},
|
||||
"Inhouding pensioenpremies": {},
|
||||
"Inhouding sociale lasten": {},
|
||||
"Kantinekosten": {},
|
||||
"Lonen en salarissen": {},
|
||||
"Loonwerk": {},
|
||||
"Managementvergoedingen": {},
|
||||
"Opleidingskosten": {},
|
||||
"Oprenting stamrechtverpl.": {},
|
||||
"Overhevelingstoeslag": {},
|
||||
"Overige kostenverg.": {},
|
||||
"Overige personeelskosten": {},
|
||||
"Overige uitkeringen": {},
|
||||
"Pensioenpremies": {},
|
||||
"Provisie 1": {},
|
||||
"Reiskosten": {},
|
||||
"Rijwielvergoeding": {},
|
||||
"Sociale lasten": {},
|
||||
"Tanti\u00e8mes": {},
|
||||
"Thuiswerkers": {},
|
||||
"Toev. Backservice pens.verpl.": {},
|
||||
"Toevoeging pensioenverpl.": {},
|
||||
"Uitkering ziekengeld": {},
|
||||
"Uitzendkrachten": {},
|
||||
"Vakantiebonnen": {},
|
||||
"Vakantiegeld": {},
|
||||
"Vergoeding studiekosten": {},
|
||||
"Wervingskosten personeel": {}
|
||||
},
|
||||
"VERKOOPKOSTEN": {
|
||||
"Advertenties": {},
|
||||
"Afschrijving dubieuze deb.": {},
|
||||
"Beurskosten": {},
|
||||
"Etalagekosten": {},
|
||||
"Exportkosten": {},
|
||||
"Kascorrecties": {},
|
||||
"Overige verkoopkosten": {},
|
||||
"Provisie": {},
|
||||
"Reclame": {},
|
||||
"Reis en verblijfkosten": {},
|
||||
"Relatiegeschenken": {},
|
||||
"Representatiekosten": {},
|
||||
"Uitgaande vrachten": {},
|
||||
"Veilingkosten": {},
|
||||
"Verpakkingsmateriaal 1": {},
|
||||
"Websitekosten": {}
|
||||
},
|
||||
"VERVOERSKOSTEN": {
|
||||
"Assuranties auto's": {},
|
||||
"Brandstoffen": {},
|
||||
"Leasing auto's": {},
|
||||
"Onderhoud personenauto's": {},
|
||||
"Onderhoud vrachtauto's": {},
|
||||
"Overige vervoerskosten": {},
|
||||
"Priv\u00e9-gebruik auto's": {},
|
||||
"Wegenbelasting": {}
|
||||
},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"TUSSENREKENINGEN": {
|
||||
"Betaalwijze cadeaubonnen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze chipknip": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze contant": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Betaalwijze pin": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland onbelast": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen Nederland verlegd": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen binnen EU overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU hoog": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU laag": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Inkopen buiten EU overig": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Kassa 1": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Kassa 2": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Netto lonen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tegenrekening Inkopen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. autom. betalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. autom. loonbetalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrek. cadeaubonbetalingen": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening balans": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening chipknip": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening correcties": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Tussenrekening pin": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Vraagposten": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"root_type": "Asset"
|
||||
"HUISVESTINGSKOSTEN": {
|
||||
"Assurantie onroerend goed": {},
|
||||
"Belastingen onr. Goed": {},
|
||||
"Energiekosten": {},
|
||||
"Groot onderhoud onr. Goed": {},
|
||||
"Huur": {},
|
||||
"Huurwaarde woongedeelte": {},
|
||||
"Onderhoud onroerend goed": {},
|
||||
"Ontvangen huren": {},
|
||||
"Overige huisvestingskosten": {},
|
||||
"Pacht": {},
|
||||
"Schoonmaakkosten": {},
|
||||
"Toevoeging egalisatieres. Groot onderhoud": {}
|
||||
},
|
||||
"KANTOORKOSTEN": {
|
||||
"Administratiekosten": {},
|
||||
"Contributies/abonnementen": {},
|
||||
"Huur kantoorapparatuur": {},
|
||||
"Internetaansluiting": {},
|
||||
"Kantoorbenodigdh./drukw.": {},
|
||||
"Onderhoud kantoorinvent.": {},
|
||||
"Overige kantoorkosten": {},
|
||||
"Porti": {},
|
||||
"Telefoon/telefax": {}
|
||||
},
|
||||
"OVERIGE BATEN EN LASTEN": {
|
||||
"Betaalde schadevergoed.": {},
|
||||
"Boekverlies vaste activa": {},
|
||||
"Boekwinst van vaste activa": {},
|
||||
"K.O. regeling OB": {},
|
||||
"Kasverschillen": {},
|
||||
"Kosten loonbelasting": {},
|
||||
"Kosten omzetbelasting": {},
|
||||
"Nadelige koersverschillen": {},
|
||||
"Naheffing bedrijfsver.": {},
|
||||
"Ontvangen schadevergoed.": {},
|
||||
"Overige baten": {},
|
||||
"Overige lasten": {},
|
||||
"Voordelige koersverschil.": {}
|
||||
},
|
||||
"PERSONEELSKOSTEN": {
|
||||
"Autokostenvergoeding": {},
|
||||
"Bedrijfskleding": {},
|
||||
"Belastingvrije uitkeringen": {},
|
||||
"Bijzondere beloningen": {},
|
||||
"Congressen, seminars en symposia": {},
|
||||
"Gereedschapsgeld": {},
|
||||
"Geschenken personeel": {},
|
||||
"Gratificaties": {},
|
||||
"Inhouding pensioenpremies": {},
|
||||
"Inhouding sociale lasten": {},
|
||||
"Kantinekosten": {},
|
||||
"Lonen en salarissen": {},
|
||||
"Loonwerk": {},
|
||||
"Managementvergoedingen": {},
|
||||
"Opleidingskosten": {},
|
||||
"Oprenting stamrechtverpl.": {},
|
||||
"Overhevelingstoeslag": {},
|
||||
"Overige kostenverg.": {},
|
||||
"Overige personeelskosten": {},
|
||||
"Overige uitkeringen": {},
|
||||
"Pensioenpremies": {},
|
||||
"Provisie 1": {},
|
||||
"Reiskosten": {},
|
||||
"Rijwielvergoeding": {},
|
||||
"Sociale lasten": {},
|
||||
"Tanti\u00e8mes": {},
|
||||
"Thuiswerkers": {},
|
||||
"Toev. Backservice pens.verpl.": {},
|
||||
"Toevoeging pensioenverpl.": {},
|
||||
"Uitkering ziekengeld": {},
|
||||
"Uitzendkrachten": {},
|
||||
"Vakantiebonnen": {},
|
||||
"Vakantiegeld": {},
|
||||
"Vergoeding studiekosten": {},
|
||||
"Wervingskosten personeel": {}
|
||||
},
|
||||
"VERKOOPKOSTEN": {
|
||||
"Advertenties": {},
|
||||
"Afschrijving dubieuze deb.": {},
|
||||
"Beurskosten": {},
|
||||
"Etalagekosten": {},
|
||||
"Exportkosten": {},
|
||||
"Kascorrecties": {},
|
||||
"Overige verkoopkosten": {},
|
||||
"Provisie": {},
|
||||
"Reclame": {},
|
||||
"Reis en verblijfkosten": {},
|
||||
"Relatiegeschenken": {},
|
||||
"Representatiekosten": {},
|
||||
"Uitgaande vrachten": {},
|
||||
"Veilingkosten": {},
|
||||
"Verpakkingsmateriaal 1": {},
|
||||
"Websitekosten": {}
|
||||
},
|
||||
"VERVOERSKOSTEN": {
|
||||
"Assuranties auto's": {},
|
||||
"Brandstoffen": {},
|
||||
"Leasing auto's": {},
|
||||
"Onderhoud personenauto's": {},
|
||||
"Onderhoud vrachtauto's": {},
|
||||
"Overige vervoerskosten": {},
|
||||
"Priv\u00e9-gebruik auto's": {},
|
||||
"Wegenbelasting": {}
|
||||
},
|
||||
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
|
||||
"Betalingskort. crediteuren": {},
|
||||
"Garantiekosten": {},
|
||||
"Hulpmaterialen": {},
|
||||
"Inkomende vrachten": {
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Inkoop import buiten EU hoog": {},
|
||||
"Inkoop import buiten EU laag": {},
|
||||
"Inkoop import buiten EU overig": {},
|
||||
"Inkoopbonussen": {},
|
||||
"Inkoopkosten": {},
|
||||
"Inkoopprovisie": {},
|
||||
"Inkopen BTW verlegd": {},
|
||||
"Inkopen EU hoog tarief": {},
|
||||
"Inkopen EU laag tarief": {},
|
||||
"Inkopen EU overig": {},
|
||||
"Inkopen hoog": {},
|
||||
"Inkopen laag": {},
|
||||
"Inkopen nul": {},
|
||||
"Inkopen overig": {},
|
||||
"Invoerkosten": {},
|
||||
"Kosten inkoopvereniging": {},
|
||||
"Kostprijs omzet grondstoffen": {
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Kostprijs omzet handelsgoederen": {},
|
||||
"Onttrekking uitgev.garantie": {},
|
||||
"Priv\u00e9-gebruik goederen": {},
|
||||
"Stock aanpassing": {
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Tegenrekening inkoop": {},
|
||||
"Toev. Voorz. incour. grondst.": {},
|
||||
"Toevoeging garantieverpl.": {},
|
||||
"Toevoeging voorz. incour. handelsgoed.": {},
|
||||
"Uitbesteed werk": {},
|
||||
"Voorz. Incourourant grondst.": {},
|
||||
"Voorz.incour. handelsgoed.": {},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"root_type": "Expense"
|
||||
}
|
||||
},
|
||||
"VASTE ACTIVA, EIGEN VERMOGEN, LANGLOPEND VREEMD VERMOGEN EN VOORZIENINGEN": {
|
||||
"EIGEN VERMOGEN": {
|
||||
@@ -602,7 +665,7 @@
|
||||
"account_type": "Equity"
|
||||
}
|
||||
},
|
||||
"root_type": "Asset"
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"VERKOOPRESULTATEN": {
|
||||
"Diensten fabric. 0% niet-EU": {},
|
||||
@@ -627,67 +690,6 @@
|
||||
"Verleende Kredietbep. fabricage": {},
|
||||
"Verleende Kredietbep. handel": {},
|
||||
"root_type": "Income"
|
||||
},
|
||||
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
|
||||
"Betalingskort. crediteuren": {},
|
||||
"Garantiekosten": {},
|
||||
"Hulpmaterialen": {},
|
||||
"Inkomende vrachten": {
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Inkoop import buiten EU hoog": {},
|
||||
"Inkoop import buiten EU laag": {},
|
||||
"Inkoop import buiten EU overig": {},
|
||||
"Inkoopbonussen": {},
|
||||
"Inkoopkosten": {},
|
||||
"Inkoopprovisie": {},
|
||||
"Inkopen BTW verlegd": {},
|
||||
"Inkopen EU hoog tarief": {},
|
||||
"Inkopen EU laag tarief": {},
|
||||
"Inkopen EU overig": {},
|
||||
"Inkopen hoog": {},
|
||||
"Inkopen laag": {},
|
||||
"Inkopen nul": {},
|
||||
"Inkopen overig": {},
|
||||
"Invoerkosten": {},
|
||||
"Kosten inkoopvereniging": {},
|
||||
"Kostprijs omzet grondstoffen": {
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Kostprijs omzet handelsgoederen": {},
|
||||
"Onttrekking uitgev.garantie": {},
|
||||
"Priv\u00e9-gebruik goederen": {},
|
||||
"Stock aanpassing": {
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Tegenrekening inkoop": {},
|
||||
"Toev. Voorz. incour. grondst.": {},
|
||||
"Toevoeging garantieverpl.": {},
|
||||
"Toevoeging voorz. incour. handelsgoed.": {},
|
||||
"Uitbesteed werk": {},
|
||||
"Voorz. Incourourant grondst.": {},
|
||||
"Voorz.incour. handelsgoed.": {},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
|
||||
"Emballage": {},
|
||||
"Gereed product 1": {},
|
||||
"Gereed product 2": {},
|
||||
"Goederen 1": {},
|
||||
"Goederen 2": {},
|
||||
"Goederen in consignatie": {},
|
||||
"Goederen onderweg": {},
|
||||
"Grondstoffen 1": {},
|
||||
"Grondstoffen 2": {},
|
||||
"Halffabrikaten 1": {},
|
||||
"Halffabrikaten 2": {},
|
||||
"Hulpstoffen 1": {},
|
||||
"Hulpstoffen 2": {},
|
||||
"Kantoorbenodigdheden": {},
|
||||
"Onderhanden werk": {},
|
||||
"Verpakkingsmateriaal": {},
|
||||
"Zegels": {},
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
|
||||
from erpnext.accounts.doctype.account.account import (
|
||||
InvalidAccountMergeError,
|
||||
merge_account,
|
||||
update_account_number,
|
||||
)
|
||||
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
|
||||
|
||||
test_dependencies = ["Company"]
|
||||
@@ -47,49 +51,53 @@ class TestAccount(unittest.TestCase):
|
||||
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
|
||||
|
||||
def test_merge_account(self):
|
||||
if not frappe.db.exists("Account", "Current Assets - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Current Assets"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Application of Funds (Assets) - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Securities and Deposits"
|
||||
acc.parent_account = "Current Assets - _TC"
|
||||
acc.is_group = 1
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Earnest Money - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Earnest Money"
|
||||
acc.parent_account = "Securities and Deposits - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Cash In Hand - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Cash In Hand"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Current Assets - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Accumulated Depreciation"
|
||||
acc.parent_account = "Fixed Assets - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.account_type = "Accumulated Depreciation"
|
||||
acc.insert()
|
||||
create_account(
|
||||
account_name="Current Assets",
|
||||
is_group=1,
|
||||
parent_account="Application of Funds (Assets) - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
create_account(
|
||||
account_name="Securities and Deposits",
|
||||
is_group=1,
|
||||
parent_account="Current Assets - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
create_account(
|
||||
account_name="Earnest Money",
|
||||
parent_account="Securities and Deposits - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
create_account(
|
||||
account_name="Cash In Hand",
|
||||
is_group=1,
|
||||
parent_account="Current Assets - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
create_account(
|
||||
account_name="Receivable INR",
|
||||
parent_account="Current Assets - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="INR",
|
||||
)
|
||||
|
||||
create_account(
|
||||
account_name="Receivable USD",
|
||||
parent_account="Current Assets - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
|
||||
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
||||
|
||||
self.assertEqual(parent, "Securities and Deposits - _TC")
|
||||
|
||||
merge_account(
|
||||
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
|
||||
)
|
||||
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
|
||||
|
||||
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
||||
|
||||
# Parent account of the child account changes after merging
|
||||
@@ -98,30 +106,28 @@ class TestAccount(unittest.TestCase):
|
||||
# Old account doesn't exist after merging
|
||||
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
|
||||
|
||||
doc = frappe.get_doc("Account", "Current Assets - _TC")
|
||||
|
||||
# Raise error as is_group property doesn't match
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
InvalidAccountMergeError,
|
||||
merge_account,
|
||||
"Current Assets - _TC",
|
||||
"Accumulated Depreciation - _TC",
|
||||
doc.is_group,
|
||||
doc.root_type,
|
||||
doc.company,
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Account", "Capital Stock - _TC")
|
||||
|
||||
# Raise error as root_type property doesn't match
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
InvalidAccountMergeError,
|
||||
merge_account,
|
||||
"Capital Stock - _TC",
|
||||
"Softwares - _TC",
|
||||
doc.is_group,
|
||||
doc.root_type,
|
||||
doc.company,
|
||||
)
|
||||
|
||||
# Raise error as currency doesn't match
|
||||
self.assertRaises(
|
||||
InvalidAccountMergeError,
|
||||
merge_account,
|
||||
"Receivable INR - _TC",
|
||||
"Receivable USD - _TC",
|
||||
)
|
||||
|
||||
def test_account_sync(self):
|
||||
@@ -400,11 +406,20 @@ def create_account(**kwargs):
|
||||
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
|
||||
)
|
||||
if account:
|
||||
return account
|
||||
account = frappe.get_doc("Account", account)
|
||||
account.update(
|
||||
dict(
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
)
|
||||
)
|
||||
account.save()
|
||||
return account.name
|
||||
else:
|
||||
account = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
|
||||
@@ -14,10 +14,8 @@ class AccountClosingBalance(Document):
|
||||
pass
|
||||
|
||||
|
||||
def make_closing_entries(closing_entries, voucher_name):
|
||||
def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
company = closing_entries[0].get("company")
|
||||
closing_date = closing_entries[0].get("closing_date")
|
||||
|
||||
previous_closing_entries = get_previous_closing_entries(
|
||||
company, closing_date, accounting_dimensions
|
||||
|
||||
@@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
company: d.company,
|
||||
root_type: ["in", ["Asset", "Liability"]],
|
||||
is_group: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
|
||||
frappe.set_route("List", frm.doc.document_type);
|
||||
|
||||
@@ -39,6 +39,8 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
@@ -46,6 +48,14 @@ class AccountingDimension(Document):
|
||||
message += _("Please create a new Accounting Dimension if required.")
|
||||
frappe.throw(message)
|
||||
|
||||
def validate_dimension_defaults(self):
|
||||
companies = []
|
||||
for default in self.get("dimension_defaults"):
|
||||
if default.company not in companies:
|
||||
companies.append(default.company)
|
||||
else:
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
@@ -255,21 +265,28 @@ def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dimensions(with_cost_center_and_project=False):
|
||||
dimension_filters = frappe.db.sql(
|
||||
"""
|
||||
SELECT label, fieldname, document_type
|
||||
FROM `tabAccounting Dimension`
|
||||
WHERE disabled = 0
|
||||
""",
|
||||
as_dict=1,
|
||||
|
||||
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
p = frappe.qb.DocType("Accounting Dimension")
|
||||
dimension_filters = (
|
||||
frappe.qb.from_(p)
|
||||
.select(p.label, p.fieldname, p.document_type)
|
||||
.where(p.disabled == 0)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
default_dimensions = (
|
||||
frappe.qb.from_(c)
|
||||
.inner_join(p)
|
||||
.on(c.parent == p.name)
|
||||
.select(p.fieldname, c.company, c.default_dimension)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
default_dimensions = frappe.db.sql(
|
||||
"""SELECT p.fieldname, c.company, c.default_dimension
|
||||
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
|
||||
WHERE c.parent = p.name""",
|
||||
as_dict=1,
|
||||
)
|
||||
if isinstance(with_cost_center_and_project, str):
|
||||
if with_cost_center_and_project.lower().strip() == "true":
|
||||
with_cost_center_and_project = True
|
||||
else:
|
||||
with_cost_center_and_project = False
|
||||
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
|
||||
@@ -84,12 +84,22 @@ def create_dimension():
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
||||
frappe.get_doc(
|
||||
dimension = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
dimension.append(
|
||||
"dimension_defaults",
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Department",
|
||||
"default_dimension": "_Test Department - _TC",
|
||||
},
|
||||
)
|
||||
dimension.insert()
|
||||
dimension.save()
|
||||
else:
|
||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
||||
dimension.disabled = 0
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"reference_document",
|
||||
"default_dimension",
|
||||
"mandatory_for_bs",
|
||||
"mandatory_for_pl"
|
||||
"mandatory_for_pl",
|
||||
"column_break_lqns",
|
||||
"automatically_post_balancing_accounting_entry",
|
||||
"offsetting_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -50,6 +53,23 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Mandatory For Profit and Loss Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "automatically_post_balancing_accounting_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically post balancing accounting entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "offsetting_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Offsetting Account",
|
||||
"mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqns",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
|
||||
@@ -68,6 +68,16 @@ frappe.ui.form.on('Accounting Dimension Filter', {
|
||||
frm.refresh_field("dimensions");
|
||||
frm.trigger('setup_filters');
|
||||
},
|
||||
apply_restriction_on_values: function(frm) {
|
||||
/** If restriction on values is not applied, we should set "allow_or_restrict" to "Restrict" with an empty allowed dimension table.
|
||||
* Hence it's not "restricted" on any value.
|
||||
*/
|
||||
if (!frm.doc.apply_restriction_on_values) {
|
||||
frm.set_value("allow_or_restrict", "Restrict");
|
||||
frm.clear_table("dimensions");
|
||||
frm.refresh_field("dimensions");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Allowed Dimension', {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
"apply_restriction_on_values",
|
||||
"allow_or_restrict",
|
||||
"section_break_4",
|
||||
"accounts",
|
||||
@@ -24,94 +25,80 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounting Dimension",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_restriction_on_values == 1;",
|
||||
"fieldname": "allow_or_restrict",
|
||||
"fieldtype": "Select",
|
||||
"label": "Allow Or Restrict Dimension",
|
||||
"options": "Allow\nRestrict",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applicable On Account",
|
||||
"options": "Applicable On Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.accounting_dimension",
|
||||
"depends_on": "eval:doc.accounting_dimension && doc.apply_restriction_on_values",
|
||||
"fieldname": "dimensions",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applicable Dimension",
|
||||
"options": "Allowed Dimension",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"mandatory_depends_on": "eval:doc.apply_restriction_on_values == 1;",
|
||||
"options": "Allowed Dimension"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_filter_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Dimension Filter Help",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Dimension Filter Help"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-03 12:04:58.678402",
|
||||
"modified": "2023-06-07 14:59:41.869117",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -154,5 +141,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -8,6 +8,12 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class AccountingDimensionFilter(Document):
|
||||
def before_save(self):
|
||||
# If restriction is not applied on values, then remove all the dimensions and set allow_or_restrict to Restrict
|
||||
if not self.apply_restriction_on_values:
|
||||
self.allow_or_restrict = "Restrict"
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
@@ -44,12 +50,12 @@ def get_dimension_filter_map():
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -76,4 +82,5 @@ def build_map(map_object, dimension, account, filter_value, allow_or_restrict, i
|
||||
(dimension, account),
|
||||
{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
|
||||
)
|
||||
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
|
||||
if filter_value:
|
||||
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
|
||||
|
||||
@@ -64,6 +64,7 @@ def create_accounting_dimension_filter():
|
||||
"accounting_dimension": "Cost Center",
|
||||
"allow_or_restrict": "Allow",
|
||||
"company": "_Test Company",
|
||||
"apply_restriction_on_values": 1,
|
||||
"accounts": [
|
||||
{
|
||||
"applicable_on_account": "Sales - _TC",
|
||||
@@ -85,6 +86,7 @@ def create_accounting_dimension_filter():
|
||||
"doctype": "Accounting Dimension Filter",
|
||||
"accounting_dimension": "Department",
|
||||
"allow_or_restrict": "Allow",
|
||||
"apply_restriction_on_values": 1,
|
||||
"company": "_Test Company",
|
||||
"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
|
||||
"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
|
||||
|
||||
@@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frm.set_query("document_type", "closed_documents", () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_doctypes_for_closing",
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ClosedAccountingPeriod(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountingPeriod(Document):
|
||||
def validate(self):
|
||||
self.validate_overlap()
|
||||
@@ -65,3 +69,42 @@ class AccountingPeriod(Document):
|
||||
"closed_documents",
|
||||
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
|
||||
)
|
||||
|
||||
|
||||
def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
if doc.doctype == "Bank Clearance":
|
||||
return
|
||||
elif doc.doctype == "Asset":
|
||||
if doc.is_existing_asset:
|
||||
return
|
||||
else:
|
||||
date = doc.available_for_use_date
|
||||
elif doc.doctype == "Asset Repair":
|
||||
date = doc.completion_date
|
||||
else:
|
||||
date = doc.posting_date
|
||||
|
||||
ap = frappe.qb.DocType("Accounting Period")
|
||||
cd = frappe.qb.DocType("Closed Document")
|
||||
|
||||
accounting_period = (
|
||||
frappe.qb.from_(ap)
|
||||
.from_(cd)
|
||||
.select(ap.name)
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
& (date <= ap.end_date)
|
||||
)
|
||||
).run(as_dict=1)
|
||||
|
||||
if accounting_period:
|
||||
frappe.throw(
|
||||
_("You cannot create a {0} within the closed Accounting Period {1}").format(
|
||||
doc.doctype, frappe.bold(accounting_period[0]["name"])
|
||||
),
|
||||
ClosedAccountingPeriod,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import (
|
||||
ClosedAccountingPeriod,
|
||||
OverlapError,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
@@ -33,9 +35,9 @@ class TestAccountingPeriod(unittest.TestCase):
|
||||
ap1.save()
|
||||
|
||||
doc = create_sales_invoice(
|
||||
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
|
||||
do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
|
||||
)
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.submit)
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
|
||||
@@ -58,10 +58,14 @@
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa"
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -383,6 +387,33 @@
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
"fieldname": "enable_party_matching",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Automatic Party Matching"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_party_matching",
|
||||
"description": "Approximately match the description/party name against parties",
|
||||
"fieldname": "enable_fuzzy_matching",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Fuzzy Matching"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
|
||||
"fieldname": "ignore_account_closing_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Account Closing Balance"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -390,7 +421,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-13 18:47:46.430291",
|
||||
"modified": "2023-07-27 15:05:34.000264",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -14,21 +14,32 @@ from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
def validate(self):
|
||||
frappe.db.set_default(
|
||||
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
|
||||
)
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
frappe.db.set_default(
|
||||
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
|
||||
)
|
||||
if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template:
|
||||
frappe.db.set_default(
|
||||
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
|
||||
)
|
||||
clear_cache = True
|
||||
|
||||
if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
|
||||
frappe.db.set_default(
|
||||
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
|
||||
)
|
||||
clear_cache = True
|
||||
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
|
||||
@@ -8,9 +8,6 @@ frappe.ui.form.on('Bank', {
|
||||
},
|
||||
refresh: function(frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' };
|
||||
|
||||
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
|
||||
|
||||
if (frm.doc.__islocal) {
|
||||
@@ -105,7 +102,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
}
|
||||
|
||||
onScriptLoaded(me) {
|
||||
me.linkHandler = Plaid.create({
|
||||
me.linkHandler = Plaid.create({ // eslint-disable-line no-undef
|
||||
env: me.plaid_env,
|
||||
token: me.token,
|
||||
onSuccess: me.plaid_success
|
||||
|
||||
@@ -70,7 +70,6 @@ def make_bank_account(doctype, docname):
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
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
|
||||
|
||||
import erpnext
|
||||
@@ -22,167 +21,24 @@ class BankClearance(Document):
|
||||
if not self.account:
|
||||
frappe.throw(_("Account is mandatory to get payment entries"))
|
||||
|
||||
condition = ""
|
||||
if not self.include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
entries = []
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
|
||||
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
|
||||
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
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`
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
"to": self.to_date,
|
||||
"bank_account": self.bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
loan_disbursement.name.as_("payment_entry"),
|
||||
loan_disbursement.disbursed_amount.as_("credit"),
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_disbursement.clearance_date.isnull())
|
||||
|
||||
loan_disbursements = query.run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
.select(
|
||||
ConstantColumn("Loan Repayment").as_("payment_document"),
|
||||
loan_repayment.name.as_("payment_entry"),
|
||||
loan_repayment.amount_paid.as_("debit"),
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.clearance_date.as_("clearance_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_repayment.clearance_date.isnull())
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
query = query.orderby(loan_repayment.posting_date).orderby(
|
||||
loan_repayment.name, order=frappe.qb.desc
|
||||
)
|
||||
|
||||
loan_repayments = query.run(as_dict=True)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if self.include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pos_purchase_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
account.account_currency, 0 as debit
|
||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
||||
where
|
||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""",
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
# get entries from all the apps
|
||||
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
|
||||
entries += (
|
||||
frappe.get_attr(method_name)(
|
||||
self.from_date,
|
||||
self.to_date,
|
||||
self.account,
|
||||
self.bank_account,
|
||||
self.include_reconciled_entries,
|
||||
self.include_pos_transactions,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
entries = sorted(
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(pos_purchase_invoices)
|
||||
+ list(loan_disbursements)
|
||||
+ list(loan_repayments),
|
||||
entries,
|
||||
key=lambda k: getdate(k["posting_date"]),
|
||||
)
|
||||
|
||||
@@ -235,3 +91,111 @@ class BankClearance(Document):
|
||||
msgprint(_("Clearance Date updated"))
|
||||
else:
|
||||
msgprint(_("Clearance Date not mentioned"))
|
||||
|
||||
|
||||
def get_payment_entries_for_bank_clearance(
|
||||
from_date, to_date, account, bank_account, include_reconciled_entries, include_pos_transactions
|
||||
):
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
|
||||
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
|
||||
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
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`
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
"bank_account": bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pos_purchase_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
account.account_currency, 0 as debit
|
||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
||||
where
|
||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(pos_purchase_invoices)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -8,26 +8,75 @@ from frappe.utils import add_months, getdate
|
||||
|
||||
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.loan_management.doctype.loan.test_loan import (
|
||||
create_loan,
|
||||
create_loan_accounts,
|
||||
create_loan_type,
|
||||
create_repayment_entry,
|
||||
make_loan_disbursement_entry,
|
||||
)
|
||||
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
|
||||
|
||||
|
||||
class TestBankClearance(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
clear_payment_entries()
|
||||
clear_loan_transactions()
|
||||
make_bank_account()
|
||||
create_loan_accounts()
|
||||
create_loan_masters()
|
||||
add_transactions()
|
||||
|
||||
# Basic test case to test if bank clearance tool doesn't break
|
||||
# Detailed test can be added later
|
||||
@if_lending_app_not_installed
|
||||
def test_bank_clearance(self):
|
||||
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||
bank_clearance.from_date = add_months(getdate(), -1)
|
||||
bank_clearance.to_date = getdate()
|
||||
bank_clearance.get_payment_entries()
|
||||
self.assertEqual(len(bank_clearance.payment_entries), 1)
|
||||
|
||||
@if_lending_app_installed
|
||||
def test_bank_clearance_with_loan(self):
|
||||
from lending.loan_management.doctype.loan.test_loan import (
|
||||
create_loan,
|
||||
create_loan_accounts,
|
||||
create_loan_type,
|
||||
create_repayment_entry,
|
||||
make_loan_disbursement_entry,
|
||||
)
|
||||
|
||||
def create_loan_masters():
|
||||
create_loan_type(
|
||||
"Clearance Loan",
|
||||
2000000,
|
||||
13.5,
|
||||
25,
|
||||
0,
|
||||
5,
|
||||
"Cash",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
)
|
||||
|
||||
def make_loan():
|
||||
loan = create_loan(
|
||||
"_Test Customer",
|
||||
"Clearance Loan",
|
||||
280000,
|
||||
"Repay Over Number of Periods",
|
||||
20,
|
||||
applicant_type="Customer",
|
||||
)
|
||||
loan.submit()
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||
repayment_entry = create_repayment_entry(
|
||||
loan.name, "_Test Customer", getdate(), loan.loan_amount
|
||||
)
|
||||
repayment_entry.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
create_loan_accounts()
|
||||
create_loan_masters()
|
||||
make_loan()
|
||||
|
||||
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||
bank_clearance.from_date = add_months(getdate(), -1)
|
||||
@@ -36,6 +85,19 @@ class TestBankClearance(unittest.TestCase):
|
||||
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||
|
||||
|
||||
def clear_payment_entries():
|
||||
frappe.db.delete("Payment Entry")
|
||||
|
||||
|
||||
@if_lending_app_installed
|
||||
def clear_loan_transactions():
|
||||
for dt in [
|
||||
"Loan Disbursement",
|
||||
"Loan Repayment",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
|
||||
|
||||
def make_bank_account():
|
||||
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
|
||||
frappe.get_doc(
|
||||
@@ -49,42 +111,8 @@ def make_bank_account():
|
||||
).insert()
|
||||
|
||||
|
||||
def create_loan_masters():
|
||||
create_loan_type(
|
||||
"Clearance Loan",
|
||||
2000000,
|
||||
13.5,
|
||||
25,
|
||||
0,
|
||||
5,
|
||||
"Cash",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"_Test Bank Clearance - _TC",
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
)
|
||||
|
||||
|
||||
def add_transactions():
|
||||
make_payment_entry()
|
||||
make_loan()
|
||||
|
||||
|
||||
def make_loan():
|
||||
loan = create_loan(
|
||||
"_Test Customer",
|
||||
"Clearance Loan",
|
||||
280000,
|
||||
"Repay Over Number of Periods",
|
||||
20,
|
||||
applicant_type="Customer",
|
||||
)
|
||||
loan.submit()
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
|
||||
repayment_entry.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
|
||||
def make_payment_entry():
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
|
||||
onload: function (frm) {
|
||||
// Set default filter dates
|
||||
today = frappe.datetime.get_today()
|
||||
let today = frappe.datetime.get_today()
|
||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||
frm.doc.bank_statement_to_date = today;
|
||||
frm.trigger('bank_account');
|
||||
|
||||
@@ -9,13 +9,15 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import cint, flt
|
||||
from pypika.terms import Parameter
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
)
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
||||
|
||||
|
||||
class BankReconciliationTool(Document):
|
||||
@@ -140,6 +142,9 @@ def create_journal_entry_bts(
|
||||
second_account
|
||||
)
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append(
|
||||
@@ -149,6 +154,7 @@ def create_journal_entry_bts(
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -158,11 +164,10 @@ def create_journal_entry_bts(
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
"company": company,
|
||||
@@ -280,68 +285,68 @@ def auto_reconcile_vouchers(
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
document_types = ["payment_entry", "journal_entry"]
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
matched_transaction = []
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
document_types,
|
||||
["payment_entry", "journal_entry"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
vouchers = []
|
||||
for r in linked_payments:
|
||||
vouchers.append(
|
||||
{
|
||||
"payment_doctype": r[1],
|
||||
"payment_name": r[2],
|
||||
"amount": r[4],
|
||||
}
|
||||
)
|
||||
transaction = frappe.get_doc("Bank Transaction", transaction.name)
|
||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
matched_trans = 0
|
||||
for voucher in vouchers:
|
||||
gl_entry = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
dict(
|
||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
||||
),
|
||||
["credit", "debit"],
|
||||
as_dict=1,
|
||||
)
|
||||
gl_amount, transaction_amount = (
|
||||
(gl_entry.credit, transaction.deposit)
|
||||
if gl_entry.credit > 0
|
||||
else (gl_entry.debit, transaction.withdrawal)
|
||||
)
|
||||
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
|
||||
transaction.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": allocated_amount,
|
||||
|
||||
if not linked_payments:
|
||||
continue
|
||||
|
||||
vouchers = list(
|
||||
map(
|
||||
lambda entry: {
|
||||
"payment_doctype": entry.get("doctype"),
|
||||
"payment_name": entry.get("name"),
|
||||
"amount": entry.get("paid_amount"),
|
||||
},
|
||||
linked_payments,
|
||||
)
|
||||
matched_transaction.append(str(transaction.name))
|
||||
transaction.save()
|
||||
transaction.update_allocations()
|
||||
matched_transaction_len = len(set(matched_transaction))
|
||||
if matched_transaction_len == 0:
|
||||
frappe.msgprint(_("No matching references found for auto reconciliation"))
|
||||
elif matched_transaction_len == 1:
|
||||
frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
|
||||
else:
|
||||
frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
|
||||
)
|
||||
|
||||
updated_transaction = reconcile_vouchers(transaction.name, json.dumps(vouchers))
|
||||
|
||||
if updated_transaction.status == "Reconciled":
|
||||
reconciled.add(updated_transaction.name)
|
||||
elif flt(transaction.unallocated_amount) != flt(updated_transaction.unallocated_amount):
|
||||
# Partially reconciled (status = Unreconciled & unallocated amount changed)
|
||||
partially_reconciled.add(updated_transaction.name)
|
||||
|
||||
alert_message, indicator = get_auto_reconcile_message(partially_reconciled, reconciled)
|
||||
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
return reconciled, partially_reconciled
|
||||
|
||||
return frappe.get_doc("Bank Transaction", transaction.name)
|
||||
|
||||
def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||
"""Returns alert message and indicator for auto reconciliation depending on result state."""
|
||||
alert_message, indicator = "", "blue"
|
||||
if not partially_reconciled and not reconciled:
|
||||
alert_message = _("No matches occurred via auto reconciliation")
|
||||
return alert_message, indicator
|
||||
|
||||
indicator = "green"
|
||||
if reconciled:
|
||||
alert_message += _("{0} Transaction(s) Reconciled").format(len(reconciled))
|
||||
alert_message += "<br>"
|
||||
|
||||
if partially_reconciled:
|
||||
alert_message += _("{0} {1} Partially Reconciled").format(
|
||||
len(partially_reconciled),
|
||||
_("Transactions") if len(partially_reconciled) > 1 else _("Transaction"),
|
||||
)
|
||||
|
||||
return alert_message, indicator
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -387,19 +392,13 @@ def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher[1], voucher[2])
|
||||
amount = None
|
||||
for row in rows:
|
||||
if row["gl_account"] == gl_account:
|
||||
amount = row["total"]
|
||||
break
|
||||
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount:
|
||||
l = list(voucher)
|
||||
l[3] -= amount
|
||||
copied.append(tuple(l))
|
||||
else:
|
||||
copied.append(voucher)
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
voucher["paid_amount"] -= amount
|
||||
|
||||
copied.append(voucher)
|
||||
return copied
|
||||
|
||||
|
||||
@@ -415,8 +414,7 @@ def check_matching(
|
||||
to_reference_date,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
# combine all types of vouchers
|
||||
subquery = get_queries(
|
||||
queries = get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
@@ -428,6 +426,7 @@ def check_matching(
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
)
|
||||
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||
@@ -438,20 +437,13 @@ def check_matching(
|
||||
}
|
||||
|
||||
matching_vouchers = []
|
||||
for query in queries:
|
||||
matching_vouchers.extend(frappe.db.sql(query, filters, as_dict=True))
|
||||
|
||||
matching_vouchers.extend(
|
||||
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
|
||||
return (
|
||||
sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
|
||||
)
|
||||
|
||||
for query in subquery:
|
||||
matching_vouchers.extend(
|
||||
frappe.db.sql(
|
||||
query,
|
||||
filters,
|
||||
)
|
||||
)
|
||||
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
|
||||
|
||||
|
||||
def get_queries(
|
||||
bank_account,
|
||||
@@ -505,6 +497,8 @@ def get_matching_queries(
|
||||
to_reference_date,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
|
||||
if "payment_entry" in document_types:
|
||||
query = get_pe_matching_query(
|
||||
exact_match,
|
||||
@@ -531,12 +525,12 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match)
|
||||
query = get_si_matching_query(exact_match, currency)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
if "purchase_invoice" in document_types:
|
||||
query = get_pi_matching_query(exact_match)
|
||||
query = get_pi_matching_query(exact_match, currency)
|
||||
queries.append(query)
|
||||
|
||||
if "bank_transaction" in document_types:
|
||||
@@ -546,128 +540,52 @@ def get_matching_queries(
|
||||
return queries
|
||||
|
||||
|
||||
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
|
||||
vouchers = []
|
||||
|
||||
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
|
||||
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
|
||||
|
||||
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
|
||||
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_bt_matching_query(exact_match, transaction):
|
||||
# get matching bank transaction query
|
||||
# find bank transactions in the same bank account with opposite sign
|
||||
# same bank account must have same company and currency
|
||||
bt = frappe.qb.DocType("Bank Transaction")
|
||||
|
||||
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
||||
amount_equality = getattr(bt, field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
|
||||
|
||||
return f"""
|
||||
|
||||
SELECT
|
||||
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1) AS rank,
|
||||
'Bank Transaction' AS doctype,
|
||||
name,
|
||||
unallocated_amount AS paid_amount,
|
||||
reference_number AS reference_no,
|
||||
date AS reference_date,
|
||||
party,
|
||||
party_type,
|
||||
date AS posting_date,
|
||||
currency
|
||||
FROM
|
||||
`tabBank Transaction`
|
||||
WHERE
|
||||
status != 'Reconciled'
|
||||
AND name != '{transaction.name}'
|
||||
AND bank_account = '{transaction.bank_account}'
|
||||
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
|
||||
|
||||
def get_ld_matching_query(bank_account, exact_match, filters):
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
||||
matching_party = loan_disbursement.applicant_type == filters.get(
|
||||
"party_type"
|
||||
) and loan_disbursement.applicant == filters.get("party")
|
||||
|
||||
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
|
||||
|
||||
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Disbursement").as_("doctype"),
|
||||
loan_disbursement.name,
|
||||
loan_disbursement.disbursed_amount,
|
||||
loan_disbursement.reference_number,
|
||||
loan_disbursement.reference_date,
|
||||
loan_disbursement.applicant_type,
|
||||
loan_disbursement.disbursement_date,
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account == bank_account)
|
||||
ref_rank = (
|
||||
frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
|
||||
)
|
||||
unallocated_rank = (
|
||||
frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
|
||||
)
|
||||
|
||||
if exact_match:
|
||||
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
||||
else:
|
||||
query.where(loan_disbursement.disbursed_amount > 0.0)
|
||||
|
||||
vouchers = query.run(as_list=True)
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_lr_matching_query(bank_account, exact_match, filters):
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
||||
matching_party = loan_repayment.applicant_type == filters.get(
|
||||
"party_type"
|
||||
) and loan_repayment.applicant == filters.get("party")
|
||||
|
||||
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
|
||||
|
||||
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
|
||||
party_condition = (
|
||||
(bt.party_type == transaction.party_type)
|
||||
& (bt.party == transaction.party)
|
||||
& bt.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
frappe.qb.from_(bt)
|
||||
.select(
|
||||
rank + rank1 + 1,
|
||||
ConstantColumn("Loan Repayment").as_("doctype"),
|
||||
loan_repayment.name,
|
||||
loan_repayment.amount_paid,
|
||||
loan_repayment.reference_number,
|
||||
loan_repayment.reference_date,
|
||||
loan_repayment.applicant_type,
|
||||
loan_repayment.posting_date,
|
||||
(ref_rank + amount_rank + party_rank + unallocated_rank + 1).as_("rank"),
|
||||
ConstantColumn("Bank Transaction").as_("doctype"),
|
||||
bt.name,
|
||||
bt.unallocated_amount.as_("paid_amount"),
|
||||
bt.reference_number.as_("reference_no"),
|
||||
bt.date.as_("reference_date"),
|
||||
bt.party,
|
||||
bt.party_type,
|
||||
bt.date.as_("posting_date"),
|
||||
bt.currency,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.payment_account == bank_account)
|
||||
.where(bt.status != "Reconciled")
|
||||
.where(bt.name != transaction.name)
|
||||
.where(bt.bank_account == transaction.bank_account)
|
||||
.where(amount_condition)
|
||||
.where(bt.docstatus == 1)
|
||||
)
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
if exact_match:
|
||||
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
||||
else:
|
||||
query.where(loan_repayment.amount_paid > 0.0)
|
||||
|
||||
vouchers = query.run()
|
||||
|
||||
return vouchers
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_pe_matching_query(
|
||||
@@ -681,45 +599,56 @@ def get_pe_matching_query(
|
||||
to_reference_date,
|
||||
):
|
||||
# get matching payment entries query
|
||||
if transaction.deposit > 0.0:
|
||||
currency_field = "paid_to_account_currency as currency"
|
||||
else:
|
||||
currency_field = "paid_from_account_currency as currency"
|
||||
filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
|
||||
order_by = " posting_date"
|
||||
filter_by_reference_no = ""
|
||||
to_from = "to" if transaction.deposit > 0.0 else "from"
|
||||
currency_field = f"paid_{to_from}_account_currency"
|
||||
payment_type = "Receive" if transaction.deposit > 0.0 else "Pay"
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
|
||||
ref_condition = pe.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = pe.paid_amount == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
|
||||
|
||||
party_condition = (
|
||||
(pe.party_type == transaction.party_type)
|
||||
& (pe.party == transaction.party)
|
||||
& pe.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
filter_by_date = pe.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " reference_date"
|
||||
filter_by_date = pe.reference_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount,
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
getattr(pe, currency_field).as_("currency"),
|
||||
)
|
||||
.where(pe.docstatus == 1)
|
||||
.where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
|
||||
.where(pe.clearance_date.isnull())
|
||||
.where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(filter_by_date)
|
||||
.orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
SELECT
|
||||
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Payment Entry' as doctype,
|
||||
name,
|
||||
paid_amount,
|
||||
reference_no,
|
||||
reference_date,
|
||||
party,
|
||||
party_type,
|
||||
posting_date,
|
||||
{currency_field}
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
docstatus = 1
|
||||
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
||||
AND ifnull(clearance_date, '') = ""
|
||||
AND {account_from_to} = %(bank_account)s
|
||||
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
{filter_by_date}
|
||||
{filter_by_reference_no}
|
||||
order by{order_by}
|
||||
"""
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_je_matching_query(
|
||||
@@ -736,100 +665,121 @@ def get_je_matching_query(
|
||||
# So one bank could have both types of bank accounts like asset and liability
|
||||
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
|
||||
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
||||
order_by = " je.posting_date"
|
||||
filter_by_reference_no = ""
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
ref_condition = je.cheque_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
|
||||
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " je.cheque_date"
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
SELECT
|
||||
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1) AS rank ,
|
||||
'Journal Entry' AS doctype,
|
||||
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
(ref_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Journal Entry").as_("doctype"),
|
||||
je.name,
|
||||
jea.{cr_or_dr}_in_account_currency AS paid_amount,
|
||||
je.cheque_no AS reference_no,
|
||||
je.cheque_date AS reference_date,
|
||||
je.pay_to_recd_from AS party,
|
||||
getattr(jea, amount_field).as_("paid_amount"),
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("reference_date"),
|
||||
je.pay_to_recd_from.as_("party"),
|
||||
jea.party_type,
|
||||
je.posting_date,
|
||||
jea.account_currency AS currency
|
||||
FROM
|
||||
`tabJournal Entry Account` AS jea
|
||||
JOIN
|
||||
`tabJournal Entry` AS je
|
||||
ON
|
||||
jea.parent = je.name
|
||||
WHERE
|
||||
je.docstatus = 1
|
||||
AND je.voucher_type NOT IN ('Opening Entry')
|
||||
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
|
||||
AND jea.account = %(bank_account)s
|
||||
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
AND je.docstatus = 1
|
||||
{filter_by_date}
|
||||
{filter_by_reference_no}
|
||||
order by {order_by}
|
||||
"""
|
||||
jea.account_currency.as_("currency"),
|
||||
)
|
||||
.where(je.docstatus == 1)
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == Parameter("%(bank_account)s"))
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(je.docstatus == 1)
|
||||
.where(filter_by_date)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match):
|
||||
def get_si_matching_query(exact_match, currency):
|
||||
# get matching sales invoice query
|
||||
return f"""
|
||||
SELECT
|
||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Sales Invoice' as doctype,
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
amount_equality = sip.amount == Parameter("%(amount)s")
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
|
||||
party_condition = si.customer == Parameter("%(party)s")
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sip)
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount as paid_amount,
|
||||
'' as reference_no,
|
||||
'' as reference_date,
|
||||
si.customer as party,
|
||||
'Customer' as party_type,
|
||||
sip.amount.as_("paid_amount"),
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
si.posting_date,
|
||||
si.currency
|
||||
si.currency,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sip.clearance_date.isnull())
|
||||
.where(sip.account == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
FROM
|
||||
`tabSales Invoice Payment` as sip
|
||||
JOIN
|
||||
`tabSales Invoice` as si
|
||||
ON
|
||||
sip.parent = si.name
|
||||
WHERE
|
||||
si.docstatus = 1
|
||||
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
||||
AND sip.account = %(bank_account)s
|
||||
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
return str(query)
|
||||
|
||||
|
||||
def get_pi_matching_query(exact_match):
|
||||
def get_pi_matching_query(exact_match, currency):
|
||||
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||
return f"""
|
||||
SELECT
|
||||
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Purchase Invoice' as doctype,
|
||||
name,
|
||||
paid_amount,
|
||||
'' as reference_no,
|
||||
'' as reference_date,
|
||||
supplier as party,
|
||||
'Supplier' as party_type,
|
||||
posting_date,
|
||||
currency
|
||||
FROM
|
||||
`tabPurchase Invoice`
|
||||
WHERE
|
||||
docstatus = 1
|
||||
AND is_paid = 1
|
||||
AND ifnull(clearance_date, '') = ""
|
||||
AND cash_bank_account = %(bank_account)s
|
||||
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
|
||||
|
||||
party_condition = purchase_invoice.supplier == Parameter("%(party)s")
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_invoice)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Purchase Invoice").as_("doctype"),
|
||||
purchase_invoice.name,
|
||||
purchase_invoice.paid_amount,
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
purchase_invoice.supplier.as_("party"),
|
||||
ConstantColumn("Supplier").as_("party_type"),
|
||||
purchase_invoice.posting_date,
|
||||
purchase_invoice.currency,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.is_paid == 1)
|
||||
.where(purchase_invoice.clearance_date.isnull())
|
||||
.where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
|
||||
.where(amount_condition)
|
||||
.where(purchase_invoice.currency == currency)
|
||||
)
|
||||
|
||||
return str(query)
|
||||
|
||||
@@ -1,9 +1,100 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, today
|
||||
|
||||
class TestBankReconciliationTool(unittest.TestCase):
|
||||
pass
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
get_bank_transactions,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
q = qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_bank_account(self):
|
||||
bank = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank",
|
||||
"bank_name": "HDFC",
|
||||
}
|
||||
).save()
|
||||
|
||||
self.bank_account = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "HDFC _current_",
|
||||
"bank": bank,
|
||||
"is_company_account": True,
|
||||
"account": self.bank, # account from Chart of Accounts
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.name
|
||||
)
|
||||
|
||||
def test_auto_reconcile(self):
|
||||
# make payment
|
||||
from_date = add_days(today(), -1)
|
||||
to_date = today()
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
posting_date=from_date,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=100,
|
||||
).save()
|
||||
payment.reference_no = "123"
|
||||
payment = payment.save().submit()
|
||||
|
||||
# make bank transaction
|
||||
bank_transaction = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": to_date,
|
||||
"deposit": 100,
|
||||
"bank_account": self.bank_account,
|
||||
"reference_number": "123",
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
# assert API output pre reconciliation
|
||||
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
self.assertEqual(len(transactions), 1)
|
||||
self.assertEqual(transactions[0].name, bank_transaction.name)
|
||||
|
||||
# auto reconcile
|
||||
auto_reconcile_vouchers(
|
||||
bank_account=self.bank_account,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=False,
|
||||
)
|
||||
|
||||
# assert API output post reconciliation
|
||||
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
self.assertEqual(len(transactions), 0)
|
||||
|
||||
178
erpnext/accounts/doctype/bank_transaction/auto_match_party.py
Normal file
178
erpnext/accounts/doctype/bank_transaction/auto_match_party.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from typing import Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
"""
|
||||
Matches by Account/IBAN and then by Party Name/Description sequentially.
|
||||
Returns when a result is obtained.
|
||||
|
||||
Result (if present) is of the form: (Party Type, Party,)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
result = None
|
||||
result = AutoMatchbyAccountIBAN(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
if not result and fuzzy_matching_enabled:
|
||||
result = AutoMatchbyPartyNameDescription(
|
||||
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
|
||||
).match()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AutoMatchbyAccountIBAN:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self):
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
return None
|
||||
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
|
||||
def match_account_in_party(self) -> Union[Tuple, None]:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
|
||||
return or_filters
|
||||
|
||||
|
||||
class AutoMatchbyPartyNameDescription:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
# fuzzy search by customer/supplier & employee
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
|
||||
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
if not self.get(field):
|
||||
continue
|
||||
|
||||
result, skip = self.fuzzy_search_and_return_result(party, names, field)
|
||||
if result or skip:
|
||||
break
|
||||
|
||||
if result or skip:
|
||||
# Skip If: It was hard to distinguish between close matches and so match is None
|
||||
# OR if the right match was found
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
|
||||
skip = False
|
||||
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
return None, skip
|
||||
|
||||
return (
|
||||
party,
|
||||
party_name,
|
||||
), skip
|
||||
|
||||
def process_fuzzy_result(self, result: Union[list, None]):
|
||||
"""
|
||||
If there are multiple valid close matches return None as result may be faulty.
|
||||
Return the result only if one accurate match stands out.
|
||||
|
||||
Returns: Result, Skip (whether or not to discontinue matching)
|
||||
"""
|
||||
PARTY, SCORE, CUTOFF = 0, 1, 80
|
||||
|
||||
if not result or not len(result):
|
||||
return None, False
|
||||
|
||||
first_result = result[0]
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
# If multiple matches with the same score, return None but discontinue matching
|
||||
# Matches were found but were too close to distinguish between
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
return None, True
|
||||
|
||||
return first_result[PARTY], True
|
||||
else:
|
||||
return None, False
|
||||
|
||||
|
||||
def get_parties_in_order(deposit: float) -> list:
|
||||
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
|
||||
if flt(deposit) > 0:
|
||||
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
|
||||
|
||||
return parties
|
||||
@@ -13,10 +13,11 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
});
|
||||
},
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__('Unreconcile Transaction'), () => {
|
||||
frm.call('remove_payment_entries')
|
||||
.then( () => frm.refresh() );
|
||||
});
|
||||
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||
frm.call("remove_payment_entries").then(() => frm.refresh());
|
||||
});
|
||||
}
|
||||
},
|
||||
bank_account: function (frm) {
|
||||
set_bank_statement_filter(frm);
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
"unallocated_amount",
|
||||
"party_section",
|
||||
"party_type",
|
||||
"party"
|
||||
"party",
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -63,7 +67,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nPending\nSettled\nUnreconciled\nReconciled"
|
||||
"options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_account",
|
||||
@@ -202,11 +206,30 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction Type",
|
||||
"length": 50
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3czf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name/Account Holder (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Account No. (Bank Statement)"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-29 18:36:50.475964",
|
||||
"modified": "2023-06-06 13:58:12.821411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -260,4 +283,4 @@
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ class BankTransaction(StatusUpdater):
|
||||
self.clear_linked_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
@@ -146,6 +149,26 @@ class BankTransaction(StatusUpdater):
|
||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||
)
|
||||
|
||||
def auto_set_party(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
||||
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if result:
|
||||
party_type, party = result
|
||||
frappe.db.set_value(
|
||||
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
@@ -320,14 +343,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
|
||||
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if doctype in [
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Invoice",
|
||||
"Expense Claim",
|
||||
"Loan Repayment",
|
||||
"Loan Disbursement",
|
||||
]:
|
||||
if doctype in get_doctypes_for_bank_reconciliation():
|
||||
if (
|
||||
doctype == "Payment Entry"
|
||||
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
|
||||
class TestAutoMatchParty(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_bank_account()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_party_name(self):
|
||||
create_supplier_for_match(supplier_name="Jackson Ella W.")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban="DE04000000003716545346",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
def test_match_by_description(self):
|
||||
create_supplier_for_match(supplier_name="Microsoft")
|
||||
doc = create_bank_transaction(
|
||||
description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
|
||||
withdrawal=1200,
|
||||
transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
|
||||
party_name="",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Microsoft")
|
||||
|
||||
def test_skip_match_if_multiple_close_results(self):
|
||||
create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
|
||||
create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
|
||||
|
||||
doc = create_bank_transaction(
|
||||
description="Paracetamol Consignment, SINV-0009",
|
||||
withdrawal=24.85,
|
||||
transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
|
||||
party_name="Adithya Medical & General",
|
||||
)
|
||||
|
||||
# Mapping is skipped as both Supplier names have the same match score
|
||||
self.assertEqual(doc.party_type, None)
|
||||
self.assertEqual(doc.party, None)
|
||||
|
||||
|
||||
def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
|
||||
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
|
||||
# Update related Bank Account details
|
||||
if not (iban or account_no):
|
||||
return
|
||||
|
||||
frappe.db.set_value(
|
||||
dt="Bank Account",
|
||||
dn={"party": supplier_name},
|
||||
field={"iban": iban, "bank_account_no": account_no},
|
||||
)
|
||||
return
|
||||
|
||||
# Create Supplier and Bank Account for the same
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.supplier_group = "Services"
|
||||
supplier.supplier_type = "Company"
|
||||
supplier.insert()
|
||||
|
||||
if not frappe.db.exists("Bank", "TestBank"):
|
||||
bank = frappe.new_doc("Bank")
|
||||
bank.bank_name = "TestBank"
|
||||
bank.insert(ignore_if_duplicate=True)
|
||||
|
||||
if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
|
||||
bank_account = frappe.new_doc("Bank Account")
|
||||
bank_account.account_name = supplier.name
|
||||
bank_account.bank = "TestBank"
|
||||
bank_account.iban = iban
|
||||
bank_account.bank_account_no = account_no
|
||||
bank_account.party_type = "Supplier"
|
||||
bank_account.party = supplier.name
|
||||
bank_account.insert()
|
||||
|
||||
|
||||
def create_bank_transaction(
|
||||
description=None,
|
||||
withdrawal=0,
|
||||
deposit=0,
|
||||
transaction_id=None,
|
||||
party_name=None,
|
||||
account_no=None,
|
||||
iban=None,
|
||||
):
|
||||
doc = frappe.new_doc("Bank Transaction")
|
||||
doc.update(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
|
||||
"date": nowdate(),
|
||||
"withdrawal": withdrawal,
|
||||
"deposit": deposit,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"transaction_id": transaction_id,
|
||||
"bank_party_name": party_name,
|
||||
"bank_party_account_number": account_no,
|
||||
"bank_party_iban": iban,
|
||||
}
|
||||
)
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
doc.reload()
|
||||
|
||||
return doc
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_paymen
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
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.tests.utils import if_lending_app_installed
|
||||
|
||||
test_dependencies = ["Item", "Cost Center"]
|
||||
|
||||
@@ -23,14 +24,13 @@ test_dependencies = ["Item", "Cost Center"]
|
||||
class TestBankTransaction(FrappeTestCase):
|
||||
def setUp(self):
|
||||
for dt in [
|
||||
"Loan Repayment",
|
||||
"Bank Transaction",
|
||||
"Payment Entry",
|
||||
"Payment Entry Reference",
|
||||
"POS Profile",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
|
||||
clear_loan_transactions()
|
||||
make_pos_profile()
|
||||
add_transactions()
|
||||
add_vouchers()
|
||||
@@ -47,7 +47,7 @@ class TestBankTransaction(FrappeTestCase):
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
|
||||
self.assertTrue(linked_payments[0]["party"] == "Conrad Electronic")
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
def test_reconcile(self):
|
||||
@@ -93,7 +93,7 @@ class TestBankTransaction(FrappeTestCase):
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertTrue(linked_payments[0][3])
|
||||
self.assertTrue(linked_payments[0]["paid_amount"])
|
||||
|
||||
# Check error if already reconciled
|
||||
def test_already_reconciled(self):
|
||||
@@ -160,8 +160,9 @@ class TestBankTransaction(FrappeTestCase):
|
||||
is not None
|
||||
)
|
||||
|
||||
@if_lending_app_installed
|
||||
def test_matching_loan_repayment(self):
|
||||
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
|
||||
from lending.loan_management.doctype.loan.test_loan import create_loan_accounts
|
||||
|
||||
create_loan_accounts()
|
||||
bank_account = frappe.get_doc(
|
||||
@@ -187,7 +188,12 @@ class TestBankTransaction(FrappeTestCase):
|
||||
repayment_entry = create_loan_and_repayment()
|
||||
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
|
||||
self.assertEqual(linked_payments[0][2], repayment_entry.name)
|
||||
self.assertEqual(linked_payments[0]["name"], repayment_entry.name)
|
||||
|
||||
|
||||
@if_lending_app_installed
|
||||
def clear_loan_transactions():
|
||||
frappe.db.delete("Loan Repayment")
|
||||
|
||||
|
||||
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
@@ -400,16 +406,18 @@ def add_vouchers():
|
||||
si.submit()
|
||||
|
||||
|
||||
@if_lending_app_installed
|
||||
def create_loan_and_repayment():
|
||||
from erpnext.loan_management.doctype.loan.test_loan import (
|
||||
from lending.loan_management.doctype.loan.test_loan import (
|
||||
create_loan,
|
||||
create_loan_type,
|
||||
create_repayment_entry,
|
||||
make_loan_disbursement_entry,
|
||||
)
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
from lending.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
|
||||
create_loan_type(
|
||||
|
||||
@@ -70,7 +70,7 @@ frappe.ui.form.on('Cost Center', {
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let data = d.get_values();
|
||||
if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) {
|
||||
d.hide();
|
||||
return;
|
||||
@@ -91,8 +91,8 @@ frappe.ui.form.on('Cost Center', {
|
||||
if(r.message) {
|
||||
frappe.set_route("Form", "Cost Center", r.message);
|
||||
} else {
|
||||
me.frm.set_value("cost_center_name", data.cost_center_name);
|
||||
me.frm.set_value("cost_center_number", data.cost_center_number);
|
||||
frm.set_value("cost_center_name", data.cost_center_name);
|
||||
frm.set_value("cost_center_number", data.cost_center_number);
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Dunning", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("sales_invoice", () => {
|
||||
frm.set_query("sales_invoice", "overdue_payments", () => {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
outstanding_amount: [">", 0],
|
||||
status: "Overdue"
|
||||
},
|
||||
@@ -22,14 +23,24 @@ frappe.ui.form.on("Dunning", {
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||
frm.set_query("customer_address", erpnext.queries.address_query);
|
||||
frm.set_query("company_address", erpnext.queries.company_address_query);
|
||||
|
||||
// cannot add rows manually, only via button "Fetch Overdue Payments"
|
||||
frm.set_df_property("overdue_payments", "cannot_add_rows", true);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
|
||||
frm.set_df_property(
|
||||
"sales_invoice",
|
||||
"read_only",
|
||||
frm.doc.__islocal ? 0 : 1
|
||||
);
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
|
||||
frm.add_custom_button(__("Resolve"), () => {
|
||||
frm.set_value("status", "Resolved");
|
||||
@@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", {
|
||||
__("Payment"),
|
||||
function () {
|
||||
frm.events.make_payment_entry(frm);
|
||||
},__("Create")
|
||||
}, __("Create")
|
||||
);
|
||||
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
if(frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(__('Ledger'), function() {
|
||||
frappe.route_options = {
|
||||
"voucher_no": frm.doc.name,
|
||||
"from_date": frm.doc.posting_date,
|
||||
"to_date": frm.doc.posting_date,
|
||||
"company": frm.doc.company,
|
||||
"show_cancelled_entries": frm.doc.docstatus === 2
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __('View'));
|
||||
if (frm.doc.docstatus === 0) {
|
||||
frm.add_custom_button(__("Fetch Overdue Payments"), () => {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
source_doctype: "Sales Invoice",
|
||||
date_field: "due_date",
|
||||
target: frm,
|
||||
setters: {
|
||||
customer: frm.doc.customer || undefined,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
status: "Overdue",
|
||||
company: frm.doc.company
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "payment_schedule",
|
||||
child_columns: ["due_date", "outstanding"],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
|
||||
|
||||
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
|
||||
},
|
||||
overdue_days: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Dunning Type",
|
||||
{
|
||||
start_day: ["<", frm.doc.overdue_days],
|
||||
end_day: [">=", frm.doc.overdue_days],
|
||||
},
|
||||
"dunning_type",
|
||||
(r) => {
|
||||
if (r) {
|
||||
frm.set_value("dunning_type", r.dunning_type);
|
||||
} else {
|
||||
frm.set_value("dunning_type", "");
|
||||
frm.set_value("rate_of_interest", "");
|
||||
frm.set_value("dunning_fee", "");
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
frm.set_value("company_address", r && r.message || "");
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.fields_dict.currency) {
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
|
||||
if (!frm.doc.currency) {
|
||||
frm.set_value("currency", company_currency);
|
||||
}
|
||||
|
||||
if (frm.doc.currency == company_currency) {
|
||||
frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const company_doc = frappe.get_doc(":Company", frm.doc.company);
|
||||
if (company_doc.default_letter_head) {
|
||||
if (frm.fields_dict.letter_head) {
|
||||
frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
currency: function (frm) {
|
||||
// this.set_dynamic_labels();
|
||||
const company_currency = erpnext.get_currency(frm.doc.company);
|
||||
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
|
||||
if (frm.doc.currency && frm.doc.currency !== company_currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
transaction_date: frm.doc.posting_date,
|
||||
from_currency: frm.doc.currency,
|
||||
to_currency: company_currency,
|
||||
args: "for_selling"
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Fetching exchange rates ..."),
|
||||
callback: function(r) {
|
||||
const exchange_rate = flt(r.message);
|
||||
if (exchange_rate != frm.doc.conversion_rate) {
|
||||
frm.set_value("conversion_rate", exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.trigger("conversion_rate");
|
||||
}
|
||||
},
|
||||
customer: (frm) => {
|
||||
erpnext.utils.get_party_details(frm);
|
||||
},
|
||||
conversion_rate: function (frm) {
|
||||
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
|
||||
frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
customer_address: function (frm) {
|
||||
erpnext.utils.get_address_display(frm, "customer_address");
|
||||
},
|
||||
company_address: function (frm) {
|
||||
erpnext.utils.get_address_display(frm, "company_address");
|
||||
},
|
||||
dunning_type: function (frm) {
|
||||
frm.trigger("get_dunning_letter_text");
|
||||
@@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", {
|
||||
if (frm.doc.dunning_type) {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
|
||||
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
|
||||
args: {
|
||||
dunning_type: frm.doc.dunning_type,
|
||||
language: frm.doc.language,
|
||||
@@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", {
|
||||
});
|
||||
}
|
||||
},
|
||||
due_date: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
},
|
||||
posting_date: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
},
|
||||
rate_of_interest: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
outstanding_amount: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
},
|
||||
interest_amount: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
frm.trigger("calculate_interest");
|
||||
},
|
||||
dunning_fee: function (frm) {
|
||||
frm.trigger("calculate_interest_and_amount");
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
sales_invoice: function (frm) {
|
||||
frm.trigger("calculate_overdue_days");
|
||||
overdue_payments_add: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
overdue_payments_remove: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
},
|
||||
calculate_overdue_days: function (frm) {
|
||||
if (frm.doc.posting_date && frm.doc.due_date) {
|
||||
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||
frm.doc.due_date,
|
||||
"days"
|
||||
);
|
||||
frm.set_value("overdue_days", overdue_days);
|
||||
}
|
||||
frm.doc.overdue_payments.forEach((row) => {
|
||||
if (frm.doc.posting_date && row.due_date) {
|
||||
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||
row.due_date,
|
||||
"days"
|
||||
);
|
||||
frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
|
||||
}
|
||||
});
|
||||
},
|
||||
calculate_interest_and_amount: function (frm) {
|
||||
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
|
||||
const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
|
||||
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
|
||||
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
|
||||
frm.set_value("interest_amount", interest_amount);
|
||||
frm.set_value("dunning_amount", dunning_amount);
|
||||
frm.set_value("grand_total", grand_total);
|
||||
calculate_interest: function (frm) {
|
||||
frm.doc.overdue_payments.forEach((row) => {
|
||||
const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
|
||||
const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
|
||||
frappe.model.set_value(row.doctype, row.name, "interest", interest);
|
||||
});
|
||||
},
|
||||
calculate_totals: function (frm) {
|
||||
const total_interest = frm.doc.overdue_payments
|
||||
.reduce((prev, cur) => prev + cur.interest, 0);
|
||||
const total_outstanding = frm.doc.overdue_payments
|
||||
.reduce((prev, cur) => prev + cur.outstanding, 0);
|
||||
const dunning_amount = total_interest + frm.doc.dunning_fee;
|
||||
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
|
||||
const grand_total = total_outstanding + dunning_amount;
|
||||
|
||||
function setWithPrecison(field, value) {
|
||||
frm.set_value(field, flt(value, precision(field)));
|
||||
}
|
||||
|
||||
setWithPrecison("total_outstanding", total_outstanding);
|
||||
setWithPrecison("total_interest", total_interest);
|
||||
setWithPrecison("dunning_amount", dunning_amount);
|
||||
setWithPrecison("base_dunning_amount", base_dunning_amount);
|
||||
setWithPrecison("grand_total", grand_total);
|
||||
},
|
||||
make_payment_entry: function (frm) {
|
||||
return frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||
args: {
|
||||
dt: frm.doc.doctype,
|
||||
dn: frm.doc.name,
|
||||
@@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Overdue Payment", {
|
||||
interest: function (frm) {
|
||||
frm.trigger("calculate_totals");
|
||||
}
|
||||
});
|
||||
@@ -2,49 +2,60 @@
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 1,
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"sales_invoice",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"outstanding_amount",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"due_date",
|
||||
"overdue_days",
|
||||
"status",
|
||||
"section_break_9",
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"column_break_18",
|
||||
"company_address_display",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"interest_amount",
|
||||
"section_break_12",
|
||||
"dunning_amount",
|
||||
"grand_total",
|
||||
"income_account",
|
||||
"overdue_payments",
|
||||
"section_break_28",
|
||||
"total_interest",
|
||||
"dunning_fee",
|
||||
"column_break_17",
|
||||
"status",
|
||||
"printing_setting_section",
|
||||
"dunning_amount",
|
||||
"base_dunning_amount",
|
||||
"section_break_32",
|
||||
"spacer",
|
||||
"column_break_33",
|
||||
"total_outstanding",
|
||||
"grand_total",
|
||||
"printing_settings_section",
|
||||
"language",
|
||||
"body_text",
|
||||
"column_break_22",
|
||||
"letter_head",
|
||||
"closing_text",
|
||||
"accounting_details_section",
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@@ -60,32 +71,17 @@
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"options": "DUNN-.MM.-.YY.-"
|
||||
"options": "DUNN-.MM.-.YY.-",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Sales Invoice",
|
||||
"options": "Sales Invoice",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer_name",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.outstanding_amount",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Outstanding Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -94,13 +90,8 @@
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Overdue Days",
|
||||
"read_only": 1
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
@@ -112,16 +103,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Dunning Type",
|
||||
"options": "Dunning Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Interest Amount",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
"options": "Dunning Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
@@ -134,6 +116,7 @@
|
||||
"fieldname": "dunning_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Fee",
|
||||
"options": "currency",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
@@ -144,36 +127,24 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "printing_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Printing Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Language",
|
||||
"options": "Language"
|
||||
"options": "Language",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Letter Head",
|
||||
"options": "Letter Head"
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -183,14 +154,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "body_text",
|
||||
"fieldtype": "Text Editor",
|
||||
@@ -201,13 +164,6 @@
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Closing Text"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.due_date",
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
@@ -222,26 +178,24 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.address_display",
|
||||
"fieldname": "address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_display",
|
||||
"fieldname": "contact_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_mobile",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
@@ -249,18 +203,12 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.company_address_display",
|
||||
"fieldname": "company_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Company Address",
|
||||
"label": "Company Address Display",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.contact_email",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
@@ -268,18 +216,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total",
|
||||
"options": "currency",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -290,33 +238,150 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nResolved\nUnresolved\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Dunning Amount",
|
||||
"options": "Draft\nResolved\nUnresolved\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "For dunning fee and interest",
|
||||
"fetch_from": "dunning_type.income_account",
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Income Account",
|
||||
"options": "Account"
|
||||
"options": "Account",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_payments",
|
||||
"fieldtype": "Table",
|
||||
"label": "Overdue Payments",
|
||||
"options": "Overdue Payment"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_28",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_interest",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Interest",
|
||||
"options": "currency",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_outstanding",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Outstanding",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "dunning_type.cost_center",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "printing_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Printing Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_32",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "spacer",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Spacer",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.conversion_rate",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Rate",
|
||||
"label": "Conversion Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "base_dunning_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Dunning Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-03 16:24:01.677026",
|
||||
"modified": "2023-06-15 15:46:53.865712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
|
||||
@@ -1,131 +1,150 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
"""
|
||||
# Accounting
|
||||
|
||||
1. Payment of outstanding invoices with dunning amount
|
||||
|
||||
- Debit full amount to bank
|
||||
- Credit invoiced amount to receivables
|
||||
- Credit dunning amount to interest and similar revenue
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
class Dunning(AccountsController):
|
||||
def validate(self):
|
||||
self.validate_overdue_days()
|
||||
self.validate_amount()
|
||||
if not self.income_account:
|
||||
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
|
||||
self.validate_same_currency()
|
||||
self.validate_overdue_payments()
|
||||
self.validate_totals()
|
||||
self.set_party_details()
|
||||
self.set_dunning_level()
|
||||
|
||||
def validate_overdue_days(self):
|
||||
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
|
||||
def validate_same_currency(self):
|
||||
"""
|
||||
Throw an error if invoice currency differs from dunning currency.
|
||||
"""
|
||||
for row in self.overdue_payments:
|
||||
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
|
||||
if invoice_currency != self.currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
|
||||
).format(row.sales_invoice, invoice_currency, self.currency)
|
||||
)
|
||||
|
||||
def validate_amount(self):
|
||||
amounts = calculate_interest_and_amount(
|
||||
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
|
||||
def validate_overdue_payments(self):
|
||||
daily_interest = self.rate_of_interest / 100 / 365
|
||||
|
||||
for row in self.overdue_payments:
|
||||
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
|
||||
row.interest = row.outstanding * daily_interest * row.overdue_days
|
||||
|
||||
def validate_totals(self):
|
||||
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
|
||||
self.total_interest = sum(row.interest for row in self.overdue_payments)
|
||||
self.dunning_amount = self.total_interest + self.dunning_fee
|
||||
self.base_dunning_amount = self.dunning_amount * self.conversion_rate
|
||||
self.grand_total = self.total_outstanding + self.dunning_amount
|
||||
|
||||
def set_party_details(self):
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
|
||||
party_details = _get_party_details(
|
||||
self.customer,
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
doctype=self.doctype,
|
||||
company=self.company,
|
||||
posting_date=self.get("posting_date"),
|
||||
fetch_payment_terms_template=False,
|
||||
party_address=self.customer_address,
|
||||
company_address=self.get("company_address"),
|
||||
)
|
||||
if self.interest_amount != amounts.get("interest_amount"):
|
||||
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
|
||||
if self.dunning_amount != amounts.get("dunning_amount"):
|
||||
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
|
||||
if self.grand_total != amounts.get("grand_total"):
|
||||
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
|
||||
for field in [
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"company_address",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
]:
|
||||
self.set(field, party_details.get(field))
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
self.set("company_address_display", get_address_display(self.company_address))
|
||||
|
||||
def on_cancel(self):
|
||||
if self.dunning_amount:
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def make_gl_entries(self):
|
||||
if not self.dunning_amount:
|
||||
return
|
||||
gl_entries = []
|
||||
invoice_fields = [
|
||||
"project",
|
||||
"cost_center",
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"conversion_rate",
|
||||
"cost_center",
|
||||
]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
def set_dunning_level(self):
|
||||
for row in self.overdue_payments:
|
||||
past_dunnings = frappe.get_all(
|
||||
"Overdue Payment",
|
||||
filters={
|
||||
"payment_schedule": row.payment_schedule,
|
||||
"parent": ("!=", row.parent),
|
||||
"docstatus": 1,
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project,
|
||||
},
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
make_gl_entries(
|
||||
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list(
|
||||
"Dunning",
|
||||
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
|
||||
ignore_permissions=True,
|
||||
)
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
|
||||
|
||||
dunning.status = "Resolved" if resolve else "Unresolved"
|
||||
dunning.save()
|
||||
|
||||
|
||||
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
interest_amount = 0
|
||||
grand_total = flt(outstanding_amount) + flt(dunning_fee)
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total += flt(interest_amount)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
"interest_amount": interest_amount,
|
||||
"grand_total": grand_total,
|
||||
"dunning_amount": dunning_amount,
|
||||
}
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
dunning = frappe.qb.DocType("Dunning")
|
||||
overdue_payment = frappe.qb.DocType("Overdue Payment")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(dunning)
|
||||
.join(overdue_payment)
|
||||
.on(overdue_payment.parent == dunning.name)
|
||||
.select(dunning.name)
|
||||
.where(
|
||||
(dunning.status == state)
|
||||
& (dunning.docstatus != 2)
|
||||
& (overdue_payment.sales_invoice == sales_invoice)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "dunning",
|
||||
"non_standard_fieldnames": {
|
||||
"Journal Entry": "reference_name",
|
||||
"Payment Entry": "reference_name",
|
||||
},
|
||||
"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
|
||||
}
|
||||
@@ -1,162 +1,197 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
|
||||
from erpnext import get_default_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 (
|
||||
unlink_payment_on_cancel_of_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
create_dunning as create_dunning_from_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
create_sales_invoice_against_cost_center,
|
||||
)
|
||||
|
||||
test_dependencies = ["Company", "Cost Center"]
|
||||
|
||||
class TestDunning(unittest.TestCase):
|
||||
|
||||
class TestDunning(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
create_dunning_type()
|
||||
create_dunning_type_with_zero_interest_rate()
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
|
||||
create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
def tearDownClass(cls):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
super().tearDownClass()
|
||||
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
|
||||
def test_dunning_without_fees(self):
|
||||
dunning = create_dunning(overdue_days=20)
|
||||
|
||||
def test_dunning_with_zero_interest_rate(self):
|
||||
dunning = create_dunning_with_zero_interest_rate()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
|
||||
)
|
||||
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
|
||||
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
|
||||
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
|
||||
self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
|
||||
self.assertEqual(round(dunning.total_interest, 2), 0.00)
|
||||
self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
|
||||
self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
|
||||
self.assertEqual(round(dunning.grand_total, 2), 100.00)
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
|
||||
order by account asc""",
|
||||
dunning.name,
|
||||
as_dict=1,
|
||||
)
|
||||
self.assertTrue(gl_entries)
|
||||
expected_values = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
|
||||
)
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
def test_dunning_with_fees_and_interest(self):
|
||||
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
|
||||
|
||||
def test_payment_entry(self):
|
||||
dunning = create_dunning()
|
||||
self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
|
||||
self.assertEqual(round(dunning.total_interest, 2), 0.41)
|
||||
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
|
||||
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
|
||||
self.assertEqual(round(dunning.grand_total, 2), 110.41)
|
||||
|
||||
def test_dunning_with_payment_entry(self):
|
||||
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
|
||||
dunning.submit()
|
||||
pe = get_payment_entry("Dunning", dunning.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = dunning.currency
|
||||
pe.paid_to_account_currency = dunning.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
|
||||
self.assertEqual(si_doc.outstanding_amount, 0)
|
||||
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_amount = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
def test_dunning_and_payment_against_partially_due_invoice(self):
|
||||
"""
|
||||
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
|
||||
"""
|
||||
create_payment_terms_template_for_dunning()
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
|
||||
sales_invoice.submit()
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
|
||||
self.assertEqual(len(dunning.overdue_payments), 1)
|
||||
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
|
||||
|
||||
dunning.submit()
|
||||
pe = get_payment_entry("Dunning", dunning.name)
|
||||
pe.reference_no, pe.reference_date = "2", nowdate()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
sales_invoice.load_from_db()
|
||||
dunning.load_from_db()
|
||||
|
||||
self.assertEqual(sales_invoice.status, "Partly Paid")
|
||||
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
# Test impact on cancellation of PE
|
||||
pe.cancel()
|
||||
sales_invoice.reload()
|
||||
dunning.reload()
|
||||
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
posting_date=posting_date, qty=1, rate=100
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = "First Notice"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
|
||||
if dunning_type_name:
|
||||
dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
|
||||
dunning.dunning_type = dunning_type.name
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.income_account = dunning_type.income_account
|
||||
dunning.cost_center = dunning_type.cost_center
|
||||
|
||||
return dunning.save()
|
||||
|
||||
|
||||
def create_dunning_with_zero_interest_rate():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status="Overdue"
|
||||
)
|
||||
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
def create_dunning_type(title, fee, interest, is_default):
|
||||
company = "_Test Company"
|
||||
if frappe.db.exists("Dunning Type", f"{title} - _TC"):
|
||||
return
|
||||
|
||||
|
||||
def create_dunning_type():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = "First Notice"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 8
|
||||
dunning_type.dunning_type = title
|
||||
dunning_type.company = company
|
||||
dunning_type.is_default = is_default
|
||||
dunning_type.dunning_fee = fee
|
||||
dunning_type.rate_of_interest = interest
|
||||
dunning_type.income_account = get_income_account(company)
|
||||
dunning_type.cost_center = get_default_cost_center(company)
|
||||
dunning_type.append(
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
|
||||
},
|
||||
)
|
||||
dunning_type.save()
|
||||
dunning_type.insert()
|
||||
|
||||
|
||||
def create_dunning_type_with_zero_interest_rate():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 0
|
||||
dunning_type.append(
|
||||
"dunning_letter_text",
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice ",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
|
||||
},
|
||||
def get_income_account(company):
|
||||
return (
|
||||
frappe.get_value("Company", company, "default_income_account")
|
||||
or frappe.get_all(
|
||||
"Account",
|
||||
filters={"is_group": 0, "company": company},
|
||||
or_filters={
|
||||
"report_type": "Profit and Loss",
|
||||
"account_type": ("in", ("Income Account", "Temporary")),
|
||||
},
|
||||
limit=1,
|
||||
pluck="name",
|
||||
)[0]
|
||||
)
|
||||
dunning_type.save()
|
||||
|
||||
|
||||
def create_payment_terms_template_for_dunning():
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
|
||||
|
||||
create_payment_term("_Test Payment Term 1 for Dunning")
|
||||
create_payment_term("_Test Payment Term 2 for Dunning")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50 for Dunning",
|
||||
"allocate_payment_based_on_payment_terms": 1,
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "_Test Payment Term 1 for Dunning",
|
||||
"invoice_portion": 50.00,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 5,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "_Test Payment Term 2 for Dunning",
|
||||
"invoice_portion": 50.00,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Dunning Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
frappe.ui.form.on("Dunning Type", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("income_account", () => {
|
||||
return {
|
||||
filters: {
|
||||
root_type: "Income",
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:dunning_type",
|
||||
"beta": 1,
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dunning_type",
|
||||
"overdue_interval_section",
|
||||
"start_day",
|
||||
"column_break_4",
|
||||
"end_day",
|
||||
"is_default",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"section_break_6",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"text_block_section",
|
||||
"dunning_letter_text"
|
||||
"dunning_letter_text",
|
||||
"section_break_9",
|
||||
"income_account",
|
||||
"column_break_13",
|
||||
"cost_center"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -45,10 +48,6 @@
|
||||
"fieldtype": "Table",
|
||||
"options": "Dunning Letter Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -57,33 +56,62 @@
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_interval_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Overdue Interval"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_day",
|
||||
"fieldtype": "Int",
|
||||
"label": "Start Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_day",
|
||||
"fieldtype": "Int",
|
||||
"label": "End Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "rate_of_interest",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_default",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "income_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Income Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-07-15 17:14:17.835074",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Dunning",
|
||||
"link_fieldname": "dunning_type"
|
||||
}
|
||||
],
|
||||
"modified": "2021-11-13 00:25:35.659283",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
"naming_rule": "By script",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class DunningType(Document):
|
||||
pass
|
||||
def autoname(self):
|
||||
company_abbr = frappe.get_value("Company", self.company, "abbr")
|
||||
self.name = f"{self.dunning_type} - {company_abbr}"
|
||||
|
||||
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal file
36
erpnext/accounts/doctype/dunning_type/test_records.json
Normal file
@@ -0,0 +1,36 @@
|
||||
[
|
||||
{
|
||||
"doctype": "Dunning Type",
|
||||
"dunning_type": "_Test First Notice",
|
||||
"company": "_Test Company",
|
||||
"is_default": 1,
|
||||
"dunning_fee": 0.0,
|
||||
"rate_of_interest": 0.0,
|
||||
"dunning_letter_text": [
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
}
|
||||
],
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"doctype": "Dunning Type",
|
||||
"dunning_type": "_Test Second Notice",
|
||||
"company": "_Test Company",
|
||||
"is_default": 0,
|
||||
"dunning_fee": 10.0,
|
||||
"rate_of_interest": 10.0,
|
||||
"dunning_letter_text": [
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "We have still not received payment for our invoice",
|
||||
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
|
||||
}
|
||||
],
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
]
|
||||
@@ -37,7 +37,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
|
||||
validate_rounding_loss: function(frm) {
|
||||
let allowance = frm.doc.rounding_loss_allowance;
|
||||
if (!(allowance > 0 && allowance < 1)) {
|
||||
if (!(allowance >= 0 && allowance < 1)) {
|
||||
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -100,15 +100,16 @@
|
||||
},
|
||||
{
|
||||
"default": "0.05",
|
||||
"description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||
"description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||
"fieldname": "rounding_loss_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rounding Loss Allowance"
|
||||
"label": "Rounding Loss Allowance",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-12 21:02:09.818208",
|
||||
"modified": "2023-06-20 07:29:06.972434",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document):
|
||||
self.set_total_gain_loss()
|
||||
|
||||
def validate_rounding_loss_allowance(self):
|
||||
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
|
||||
if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1):
|
||||
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
@@ -93,6 +93,12 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
return True
|
||||
|
||||
def fetch_and_calculate_accounts_data(self):
|
||||
accounts = self.get_accounts_data()
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
self.append("accounts", acc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self):
|
||||
self.validate_mandatory()
|
||||
@@ -186,7 +192,7 @@ class ExchangeRateRevaluation(Document):
|
||||
# round off balance based on currency precision
|
||||
# and consider debit-credit difference allowance
|
||||
currency_precision = get_currency_precision()
|
||||
rounding_loss_allowance = rounding_loss_allowance or 0.05
|
||||
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
|
||||
@@ -252,8 +258,8 @@ class ExchangeRateRevaluation(Document):
|
||||
new_balance_in_base_currency = 0
|
||||
new_balance_in_account_currency = 0
|
||||
|
||||
current_exchange_rate = calculate_exchange_rate_using_last_gle(
|
||||
company, d.account, d.party_type, d.party
|
||||
current_exchange_rate = (
|
||||
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0
|
||||
)
|
||||
|
||||
gain_loss = new_balance_in_account_currency - (
|
||||
@@ -373,6 +379,24 @@ class ExchangeRateRevaluation(Document):
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(journal_account)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||
"credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
|
||||
# Base currency has balance
|
||||
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
|
||||
@@ -388,22 +412,22 @@ class ExchangeRateRevaluation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(journal_account)
|
||||
journal_entry_accounts.append(journal_account)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
|
||||
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||
"credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_total_debit_credit()
|
||||
@@ -552,7 +576,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance=None
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
@@ -3,6 +3,296 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
class TestExchangeRateRevaluation(unittest.TestCase):
|
||||
pass
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_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.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.set_system_and_company_settings()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
# set number and currency precision
|
||||
system_settings = frappe.get_doc("System Settings")
|
||||
system_settings.float_precision = 2
|
||||
system_settings.currency_precision = 2
|
||||
system_settings.save()
|
||||
|
||||
# Using Exchange Gain/Loss account for unrealized as well.
|
||||
company_doc = frappe.get_doc("Company", self.company)
|
||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||
company_doc.save()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_01_revaluation_of_forex_balance(self):
|
||||
"""
|
||||
Test Forex account balance and Journal creation post Revaluation
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
row = err.accounts[0]
|
||||
row.new_exchange_rate = 85
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
|
||||
self.assertEqual(je.total_debit, 8500.0)
|
||||
self.assertEqual(je.total_credit, 8500.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=["sum(debit)-sum(credit) as balance"],
|
||||
)[0]
|
||||
self.assertEqual(acc_balance.balance, 8500.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_02_accounts_only_with_base_currency_balance(self):
|
||||
"""
|
||||
Test Revaluation on Forex account with balance only in base currency
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.source_exchange_rate = 85
|
||||
pe.received_amount = 8500
|
||||
pe.save().submit()
|
||||
|
||||
# Cancel the auto created gain/loss JE to simulate balance only in base currency
|
||||
je = frappe.db.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
|
||||
)[0]
|
||||
frappe.get_doc("Journal Entry", je).cancel()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||
self.assertEqual(len(je.accounts), 2)
|
||||
# Only base currency fields will be posted to
|
||||
for acc in je.accounts:
|
||||
self.assertEqual(acc.debit_in_account_currency, 0)
|
||||
self.assertEqual(acc.credit_in_account_currency, 0)
|
||||
|
||||
self.assertEqual(je.total_debit, 500.0)
|
||||
self.assertEqual(je.total_credit, 500.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency
|
||||
self.assertEqual(acc_balance.balance, 0.0)
|
||||
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_03_accounts_only_with_account_currency_balance(self):
|
||||
"""
|
||||
Test Revaluation on Forex account with balance only in account currency
|
||||
"""
|
||||
precision = frappe.db.get_single_value("System Settings", "currency_precision")
|
||||
|
||||
# posting on previous date to make sure that ERR picks up the Payment entry's exchange
|
||||
# rate while calculating gain/loss for account currency balance
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=add_days(today(), -1),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account should have balance only in account currency
|
||||
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||
self.assertEqual(len(je.accounts), 2)
|
||||
# Only account currency fields will be posted to
|
||||
for acc in je.accounts:
|
||||
self.assertEqual(flt(acc.debit, precision), 0.0)
|
||||
self.assertEqual(flt(acc.credit, precision), 0.0)
|
||||
|
||||
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency post revaluation
|
||||
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_04_get_account_details_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
|
||||
get_account_details,
|
||||
)
|
||||
|
||||
account_details = get_account_details(
|
||||
self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
|
||||
)
|
||||
# not checking for new exchange rate and balances as it is dependent on live exchange rates
|
||||
expected_data = {
|
||||
"account_currency": "USD",
|
||||
"balance_in_base_currency": 8000.0,
|
||||
"balance_in_account_currency": 100.0,
|
||||
"current_exchange_rate": 80.0,
|
||||
"zero_balance": False,
|
||||
"new_balance_in_account_currency": 100.0,
|
||||
}
|
||||
|
||||
for key, val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"fieldname": "current_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Current Exchange Rate",
|
||||
"precision": "9",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -92,6 +93,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "New Exchange Rate",
|
||||
"precision": "9",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -147,7 +149,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:52.915295",
|
||||
"modified": "2023-06-22 12:39:56.446722",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation Account",
|
||||
|
||||
@@ -13,7 +13,7 @@ class TestFinanceBook(unittest.TestCase):
|
||||
finance_book = create_finance_book()
|
||||
|
||||
# create jv entry
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
|
||||
jv = make_journal_entry("_Test Bank - _TC", "Debtors - _TC", 100, save=False)
|
||||
|
||||
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"})
|
||||
|
||||
|
||||
@@ -8,17 +8,6 @@ frappe.ui.form.on('Fiscal Year', {
|
||||
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1));
|
||||
}
|
||||
},
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
|
||||
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
|
||||
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
|
||||
} else {
|
||||
frm.set_intro("");
|
||||
}
|
||||
},
|
||||
set_as_default: function(frm) {
|
||||
return frm.call('set_as_default');
|
||||
},
|
||||
year_start_date: function(frm) {
|
||||
if (!frm.doc.is_short_year) {
|
||||
let year_end_date =
|
||||
|
||||
@@ -4,28 +4,12 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _, msgprint
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
|
||||
class FiscalYear(Document):
|
||||
@frappe.whitelist()
|
||||
def set_as_default(self):
|
||||
frappe.db.set_single_value("Global Defaults", "current_fiscal_year", self.name)
|
||||
global_defaults = frappe.get_doc("Global Defaults")
|
||||
global_defaults.check_permission("write")
|
||||
global_defaults.on_update()
|
||||
|
||||
# clear cache
|
||||
frappe.clear_cache()
|
||||
|
||||
msgprint(
|
||||
_(
|
||||
"{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
|
||||
).format(self.name)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
@@ -68,13 +52,6 @@ class FiscalYear(Document):
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
global_defaults = frappe.get_doc("Global Defaults")
|
||||
if global_defaults.current_fiscal_year == self.name:
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
|
||||
).format(self.name)
|
||||
)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
"finance_book",
|
||||
"to_rename",
|
||||
"due_date",
|
||||
"is_cancelled"
|
||||
"is_cancelled",
|
||||
"transaction_currency",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -253,15 +257,40 @@
|
||||
"fieldname": "is_cancelled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Transaction Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Transaction Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Transaction Currency",
|
||||
"options": "transaction_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_transaction_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Transaction Currency",
|
||||
"options": "transaction_currency"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"modified": "2020-04-07 16:22:33.766994",
|
||||
"links": [],
|
||||
"modified": "2023-08-16 21:38:44.072267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -290,5 +319,6 @@
|
||||
"quick_entry": 1,
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -58,6 +58,13 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.get_cached_value("Account", self.account, "account_type") not in [
|
||||
"Receivable",
|
||||
"Payable",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
@@ -56,7 +57,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2022-01-18 21:11:23.105589",
|
||||
"modified": "2023-07-09 18:11:23.105589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@@ -102,4 +103,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@@ -50,6 +50,8 @@ frappe.ui.form.on("Journal Entry", {
|
||||
frm.trigger("make_inter_company_journal_entry");
|
||||
}, __('Make'));
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
make_inter_company_journal_entry: function(frm) {
|
||||
@@ -264,11 +266,11 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
}
|
||||
|
||||
if(jvd.party_type && jvd.party) {
|
||||
var party_field = "";
|
||||
let party_field = "";
|
||||
if(jvd.reference_type.indexOf("Sales")===0) {
|
||||
var party_field = "customer";
|
||||
party_field = "customer";
|
||||
} else if (jvd.reference_type.indexOf("Purchase")===0) {
|
||||
var party_field = "supplier";
|
||||
party_field = "supplier";
|
||||
}
|
||||
|
||||
if (party_field) {
|
||||
@@ -368,7 +370,7 @@ cur_frm.cscript.update_totals = function(doc) {
|
||||
td += flt(accounts[i].debit, precision("debit", accounts[i]));
|
||||
tc += flt(accounts[i].credit, precision("credit", accounts[i]));
|
||||
}
|
||||
var doc = locals[doc.doctype][doc.name];
|
||||
doc = locals[doc.doctype][doc.name];
|
||||
doc.total_debit = td;
|
||||
doc.total_credit = tc;
|
||||
doc.difference = flt((td - tc), precision("difference"));
|
||||
@@ -575,7 +577,7 @@ $.extend(erpnext.journal_entry, {
|
||||
};
|
||||
if(!frm.doc.multi_currency) {
|
||||
$.extend(filters, {
|
||||
account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
account_currency: ['in', [frappe.get_doc(":Company", frm.doc.company).default_currency, null]]
|
||||
});
|
||||
}
|
||||
return { filters: filters };
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
@@ -533,13 +534,22 @@
|
||||
"label": "Process Deferred Accounting",
|
||||
"options": "Process Deferred Accounting",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_system_generated == 1;",
|
||||
"fieldname": "is_system_generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-01 14:58:59.286591",
|
||||
"modified": "2023-08-10 14:32:22.366895",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
@@ -87,15 +88,16 @@ class JournalEntry(AccountsController):
|
||||
self.update_invoice_discounting()
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super(JournalEntry, self).on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
@@ -326,12 +328,10 @@ class JournalEntry(AccountsController):
|
||||
d.db_update()
|
||||
|
||||
def unlink_asset_reference(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
@@ -370,6 +370,15 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
|
||||
journal_entry_for_scrap = frappe.db.get_value(
|
||||
"Asset", d.reference_name, "journal_entry_for_scrap"
|
||||
)
|
||||
|
||||
if journal_entry_for_scrap == self.name:
|
||||
frappe.throw(
|
||||
_("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.")
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -401,6 +410,15 @@ class JournalEntry(AccountsController):
|
||||
d.idx, d.account
|
||||
)
|
||||
)
|
||||
elif (
|
||||
d.party_type
|
||||
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
|
||||
d.idx, d.account, d.party_type
|
||||
)
|
||||
)
|
||||
|
||||
def check_credit_limit(self):
|
||||
customers = list(
|
||||
@@ -483,11 +501,12 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
if not against_entries:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Journal Entry {0} does not have account {1} or already matched against other voucher"
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
if self.voucher_type != "Exchange Gain Or Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Journal Entry {0} does not have account {1} or already matched against other voucher"
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
else:
|
||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||
valid = False
|
||||
@@ -570,7 +589,9 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
|
||||
if against_voucher[0] != cstr(d.party) or party_account != d.account:
|
||||
if (
|
||||
against_voucher[0] != cstr(d.party) or party_account != d.account
|
||||
) and self.voucher_type != "Exchange Gain Or Loss":
|
||||
frappe.throw(
|
||||
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
||||
d.idx,
|
||||
@@ -752,18 +773,23 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
):
|
||||
|
||||
# Modified to include the posting date for which to retreive the exchange rate
|
||||
d.exchange_rate = get_exchange_rate(
|
||||
self.posting_date,
|
||||
d.account,
|
||||
d.account_currency,
|
||||
self.company,
|
||||
d.reference_type,
|
||||
d.reference_name,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.exchange_rate,
|
||||
)
|
||||
ignore_exchange_rate = False
|
||||
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
|
||||
ignore_exchange_rate = True
|
||||
|
||||
if not ignore_exchange_rate:
|
||||
# Modified to include the posting date for which to retreive the exchange rate
|
||||
d.exchange_rate = get_exchange_rate(
|
||||
self.posting_date,
|
||||
d.account,
|
||||
d.account_currency,
|
||||
self.company,
|
||||
d.reference_type,
|
||||
d.reference_name,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.exchange_rate,
|
||||
)
|
||||
|
||||
if not d.exchange_rate:
|
||||
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
|
||||
@@ -771,6 +797,9 @@ class JournalEntry(AccountsController):
|
||||
def create_remarks(self):
|
||||
r = []
|
||||
|
||||
if self.flags.skip_remarks_creation:
|
||||
return
|
||||
|
||||
if self.user_remark:
|
||||
r.append(_("Note: {0}").format(self.user_remark))
|
||||
|
||||
@@ -919,6 +948,8 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self, difference_account=None):
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
@@ -13,6 +14,7 @@ from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
|
||||
class TestJournalEntry(unittest.TestCase):
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_journal_entry_with_against_jv(self):
|
||||
jv_invoice = frappe.copy_doc(test_records[2])
|
||||
base_jv = frappe.copy_doc(test_records[0])
|
||||
@@ -43,7 +45,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
frappe.db.sql(
|
||||
"""select name from `tabJournal Entry Account`
|
||||
where account = %s and docstatus = 1 and parent = %s""",
|
||||
("_Test Receivable - _TC", test_voucher.name),
|
||||
("Debtors - _TC", test_voucher.name),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -273,7 +275,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.submit()
|
||||
|
||||
# create jv in USD, but account currency in INR
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
|
||||
jv = make_journal_entry("_Test Bank - _TC", "Debtors - _TC", 100, save=False)
|
||||
|
||||
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Receivable - _TC",
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
@@ -70,7 +70,7 @@
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Receivable - _TC",
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
@@ -284,7 +284,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-26 20:03:10.906259",
|
||||
"modified": "2023-06-16 14:11:13.507807",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,7 +28,7 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
if(!frm.doc.multi_currency) {
|
||||
$.extend(filters, {
|
||||
account_currency: frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
account_currency: ['in', [frappe.get_doc(":Company", frm.doc.company).default_currency, null]]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,6 @@ def start_merge(docname):
|
||||
merge_account(
|
||||
row.account,
|
||||
ledger_merge.account,
|
||||
ledger_merge.is_group,
|
||||
ledger_merge.root_type,
|
||||
ledger_merge.company,
|
||||
)
|
||||
row.db_set("merged", 1)
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -141,12 +141,12 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
)
|
||||
|
||||
if points_to_redeem > loyalty_program_details.loyalty_points:
|
||||
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
|
||||
frappe.throw(_("You don't have enough Loyalty Points to redeem"))
|
||||
|
||||
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||
|
||||
if loyalty_amount > ref_doc.grand_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Grand Total."))
|
||||
if loyalty_amount > ref_doc.rounded_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
|
||||
|
||||
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
|
||||
ref_doc.loyalty_amount = loyalty_amount
|
||||
|
||||
170
erpnext/accounts/doctype/overdue_payment/overdue_payment.json
Normal file
170
erpnext/accounts/doctype/overdue_payment/overdue_payment.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-15 18:34:27.172906",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sales_invoice",
|
||||
"payment_schedule",
|
||||
"dunning_level",
|
||||
"payment_term",
|
||||
"section_break_15",
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"overdue_days",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"section_break_16",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
"paid_amount",
|
||||
"discounted_amount",
|
||||
"interest"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "payment_term",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Term",
|
||||
"options": "Payment Term",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_15",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "payment_term.description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "invoice_portion",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Invoice Portion",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "payment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "payment_amount",
|
||||
"fieldname": "outstanding",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Outstanding",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_amount",
|
||||
"fieldname": "paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "discounted_amount",
|
||||
"fieldname": "discounted_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Discounted Amount",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Sales Invoice",
|
||||
"options": "Sales Invoice",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_schedule",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Schedule",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "overdue_days",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Overdue Days",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "dunning_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Dunning Level",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "interest",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Interest",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-23 13:48:27.898830",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Overdue Payment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Pledge(Document):
|
||||
class OverduePayment(Document):
|
||||
pass
|
||||
@@ -6,7 +6,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"account"
|
||||
"account",
|
||||
"advance_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -22,14 +23,20 @@
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"label": "Default Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-04 12:31:02.994197",
|
||||
"modified": "2023-06-06 14:15:42.053150",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Party Account",
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
{% include "erpnext/public/js/controllers/accounts.js" %}
|
||||
frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_validations("Payment Entry");
|
||||
erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@@ -106,12 +108,11 @@ frappe.ui.form.on('Payment Entry', {
|
||||
});
|
||||
|
||||
frm.set_query("reference_doctype", "references", function() {
|
||||
let doctypes = ["Journal Entry"];
|
||||
if (frm.doc.party_type == "Customer") {
|
||||
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
|
||||
doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
|
||||
} else if (frm.doc.party_type == "Supplier") {
|
||||
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
||||
} else {
|
||||
var doctypes = ["Journal Entry"];
|
||||
doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -122,13 +123,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
|
||||
|
||||
payment_term_list = payment_term_list.map(pt => pt.payment_term);
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
'name': ['in', payment_term_list]
|
||||
'reference': child.reference_name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +153,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -164,6 +164,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
frm.trigger('party');
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
@@ -286,6 +287,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
},
|
||||
|
||||
mode_of_payment: function(frm) {
|
||||
erpnext.accounts.pos.get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){
|
||||
let payment_account_field = frm.doc.payment_type == "Receive" ? "paid_to" : "paid_from";
|
||||
frm.set_value(payment_account_field, account);
|
||||
})
|
||||
},
|
||||
|
||||
party_type: function(frm) {
|
||||
|
||||
let party_types = Object.keys(frappe.boot.party_account_types);
|
||||
@@ -528,15 +536,21 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
source_exchange_rate: function(frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (frm.doc.paid_amount) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
@@ -545,6 +559,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
target_exchange_rate: function(frm) {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.received_amount) {
|
||||
frm.set_value("base_received_amount",
|
||||
@@ -554,9 +569,14 @@ frappe.ui.form.on('Payment Entry', {
|
||||
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
|
||||
@@ -612,7 +632,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
get_outstanding_invoice: function(frm) {
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
@@ -642,12 +662,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
btn_text = "Get Outstanding Invoices";
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
btn_text = "Get Outstanding Orders";
|
||||
}
|
||||
|
||||
frappe.prompt(fields, function(filters){
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
frm.events.validate_filters_data(frm, filters);
|
||||
frm.doc.cost_center = filters.cost_center;
|
||||
frm.events.get_outstanding_documents(frm, filters);
|
||||
}, __("Filters"), __("Get Outstanding Documents"));
|
||||
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
|
||||
}, __("Filters"), __(btn_text));
|
||||
},
|
||||
|
||||
get_outstanding_invoices: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, true, false);
|
||||
},
|
||||
|
||||
get_outstanding_orders: function(frm) {
|
||||
frm.events.get_outstanding_invoices_or_orders(frm, false, true);
|
||||
},
|
||||
|
||||
validate_filters_data: function(frm, filters) {
|
||||
@@ -673,7 +710,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
},
|
||||
|
||||
get_outstanding_documents: function(frm, filters) {
|
||||
get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
frm.clear_table("references");
|
||||
|
||||
if(!frm.doc.party) {
|
||||
@@ -697,6 +734,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
args[key] = filters[key];
|
||||
}
|
||||
|
||||
if (get_outstanding_invoices) {
|
||||
args["get_outstanding_invoices"] = true;
|
||||
}
|
||||
else if (get_orders_to_be_billed) {
|
||||
args["get_orders_to_be_billed"] = true;
|
||||
}
|
||||
|
||||
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
||||
|
||||
return frappe.call({
|
||||
@@ -708,7 +752,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if(r.message) {
|
||||
var total_positive_outstanding = 0;
|
||||
var total_negative_outstanding = 0;
|
||||
|
||||
$.each(r.message, function(i, d) {
|
||||
var c = frm.add_child("references");
|
||||
c.reference_doctype = d.voucher_type;
|
||||
@@ -719,6 +762,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
c.bill_no = d.bill_no;
|
||||
c.payment_term = d.payment_term;
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
|
||||
if(flt(d.outstanding_amount) > 0)
|
||||
@@ -848,12 +892,18 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
set_total_allocated_amount: function(frm) {
|
||||
let exchange_rate = 1;
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
exchange_rate = frm.doc.source_exchange_rate;
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
exchange_rate = frm.doc.target_exchange_rate;
|
||||
}
|
||||
var total_allocated_amount = 0.0;
|
||||
var base_total_allocated_amount = 0.0;
|
||||
$.each(frm.doc.references || [], function(i, row) {
|
||||
if (row.allocated_amount) {
|
||||
total_allocated_amount += flt(row.allocated_amount);
|
||||
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate),
|
||||
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(exchange_rate),
|
||||
precision("base_paid_amount"));
|
||||
}
|
||||
});
|
||||
@@ -872,12 +922,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if(frm.doc.payment_type == "Receive"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
|
||||
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||
} else if (frm.doc.payment_type == "Pay"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
|
||||
unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions
|
||||
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@@ -1077,7 +1127,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (tax.charge_type === 'On Net Total') {
|
||||
tax.charge_type = 'On Paid Amount';
|
||||
}
|
||||
me.frm.add_child("taxes", tax);
|
||||
frm.add_child("taxes", tax);
|
||||
}
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
@@ -1193,7 +1243,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
|
||||
} else {
|
||||
tax.grand_total_fraction_for_current_item =
|
||||
me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
|
||||
frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
|
||||
tax.tax_fraction_for_current_item;
|
||||
}
|
||||
|
||||
@@ -1240,7 +1290,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
});
|
||||
|
||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||
$.each(frm.doc["taxes"] || [], function(i, tax) {
|
||||
let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
|
||||
|
||||
// Adjust divisional loss to the last item
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"party_type",
|
||||
"party",
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -48,7 +49,8 @@
|
||||
"base_received_amount",
|
||||
"base_received_amount_after_tax",
|
||||
"section_break_14",
|
||||
"get_outstanding_invoice",
|
||||
"get_outstanding_invoices",
|
||||
"get_outstanding_orders",
|
||||
"references",
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
@@ -355,12 +357,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoice",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "references",
|
||||
"fieldtype": "Table",
|
||||
@@ -728,12 +724,33 @@
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoices",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoices"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_orders",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Orders"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "company.book_advance_payments_in_separate_party_account",
|
||||
"fieldname": "book_advance_payments_in_separate_party_account",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:52:30.478523",
|
||||
"modified": "2023-06-23 18:07:38.023010",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||
make_purchase_invoice,
|
||||
@@ -30,6 +31,16 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
|
||||
journals = []
|
||||
if voucher_type and voucher_no:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
)
|
||||
return journals
|
||||
|
||||
def test_payment_entry_against_order(self):
|
||||
so = make_sales_order()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
@@ -590,21 +601,15 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.target_exchange_rate = 45.263
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 94.80,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
|
||||
self.assertEqual(flt(pe.difference_amount, 2), 0.0)
|
||||
self.assertEqual(flt(pe.unallocated_amount, 2), 0.0)
|
||||
|
||||
# the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them
|
||||
# payment entry will not be generating difference amount
|
||||
self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74)
|
||||
|
||||
def test_payment_entry_retrieves_last_exchange_rate(self):
|
||||
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||
save_new_records,
|
||||
@@ -697,7 +702,50 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe2.submit()
|
||||
|
||||
# create return entry against si1
|
||||
create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
|
||||
# create JE(credit note) manually against si1 and cr_note
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": si1.company,
|
||||
"voucher_type": "Credit Note",
|
||||
"posting_date": nowdate(),
|
||||
}
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": si1.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si1.customer,
|
||||
"debit": 0,
|
||||
"credit": 100,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si1.doctype,
|
||||
"reference_name": si1.name,
|
||||
"cost_center": si1.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": cr_note.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": cr_note.customer,
|
||||
"debit": 100,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": 100,
|
||||
"credit_in_account_currency": 0,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": cr_note.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
self.assertEqual(si1_outstanding, -100)
|
||||
|
||||
@@ -791,33 +839,28 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
pe.source_exchange_rate = 55
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": -500,
|
||||
},
|
||||
)
|
||||
pe.save()
|
||||
|
||||
self.assertEqual(pe.unallocated_amount, 0)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 500)
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||
["_Test Receivable USD - _TC", 0, 5500, si.name],
|
||||
["_Test Bank USD - _TC", 5500, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 0, 500, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
# Exchange gain/loss should have been posted through a journal
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe)
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
@@ -931,7 +974,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(pe.cost_center, si.cost_center)
|
||||
self.assertEqual(flt(expected_account_balance), account_balance)
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
|
||||
|
||||
def test_multi_currency_payment_entry_with_taxes(self):
|
||||
payment_entry = create_payment_entry(
|
||||
@@ -1037,6 +1080,188 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
def test_details_update_on_reference_table(self):
|
||||
so = make_sales_order(
|
||||
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
|
||||
)
|
||||
so.conversion_rate = 50
|
||||
so.submit()
|
||||
pe = get_payment_entry("Sales Order", so.name)
|
||||
pe.references.clear()
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_from_account_currency = "INR"
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
expected_response = {
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
"exchange_rate": 1.0,
|
||||
"due_date": None,
|
||||
"bill_no": None,
|
||||
}
|
||||
self.assertDictEqual(ref_details, expected_response)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_on_payment_terms(self):
|
||||
"""
|
||||
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si1.payment_terms_template = "Test Receivable Template"
|
||||
si1.save().submit()
|
||||
|
||||
si1.reload()
|
||||
pe = get_payment_entry(si1.doctype, si1.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si1.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 400
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.references[1].allocated_amount = 200
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si1.cancel()
|
||||
si1.delete()
|
||||
|
||||
# Validate allocation on foreign currency
|
||||
si2 = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.save().submit()
|
||||
|
||||
si2.reload()
|
||||
pe = get_payment_entry(si2.doctype, si2.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si2.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.references[1].allocated_amount = 100
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si2.cancel()
|
||||
si2.delete()
|
||||
|
||||
# Validate allocation in base/company currency on a foreign currency document
|
||||
# when invoice is made is foreign currency, but posted to base/company currency debtors account
|
||||
si3 = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si3.payment_terms_template = "Test Receivable Template"
|
||||
si3.save().submit()
|
||||
|
||||
si3.reload()
|
||||
pe = get_payment_entry(si3.doctype, si3.name).save()
|
||||
# Allocated amount should be equal to payment term outstanding
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
for idx, ref in enumerate(pe.references):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 16000
|
||||
pe.references[0].allocated_amount = 8000
|
||||
pe.references[1].allocated_amount = 8000
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si3.cancel()
|
||||
si3.delete()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_shouldnt_misfire(self):
|
||||
"""
|
||||
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 0
|
||||
template.save()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si.payment_terms_template = "Test Receivable Template"
|
||||
si.save().submit()
|
||||
|
||||
si.reload()
|
||||
pe = get_payment_entry(si.doctype, si.name).save()
|
||||
# There will no term based allocation
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].payment_term, None)
|
||||
self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
|
||||
pe.save()
|
||||
|
||||
# specify a term
|
||||
pe.references[0].payment_term = template.terms[0].payment_term
|
||||
# no validation error should be thrown
|
||||
pe.save()
|
||||
|
||||
pe.paid_amount = si.grand_total + 1
|
||||
pe.references[0].allocated_amount = si.grand_total + 1
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 1
|
||||
template.save()
|
||||
|
||||
def test_allocation_validation_for_sales_order(self):
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items[0].rate = 99.55
|
||||
so.save().submit()
|
||||
self.assertGreater(so.rounded_total, 0.0)
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_amount = 45.55
|
||||
pe.references[0].allocated_amount = 45.55
|
||||
pe.save().submit()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
# No validation error should be thrown here.
|
||||
pe.save().submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, so.rounded_total)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1126,3 +1351,17 @@ def create_payment_terms_template_with_discount(
|
||||
def create_payment_term(name):
|
||||
if not frappe.db.exists("Payment Term", name):
|
||||
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
|
||||
|
||||
|
||||
def create_customer(name="_Test Customer 2 USD", currency="USD"):
|
||||
customer = None
|
||||
if frappe.db.exists("Customer", name):
|
||||
customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.default_currency = currency
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
customer = customer.name
|
||||
return customer
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"outstanding_amount",
|
||||
"allocated_amount",
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss"
|
||||
"exchange_gain_loss",
|
||||
"account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -101,12 +102,18 @@
|
||||
"label": "Exchange Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-12 12:31:44.919895",
|
||||
"modified": "2023-06-08 07:40:38.487874",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"party_type",
|
||||
"party",
|
||||
"due_date",
|
||||
"voucher_detail_no",
|
||||
"cost_center",
|
||||
"finance_book",
|
||||
"voucher_type",
|
||||
@@ -142,12 +143,17 @@
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"label": "Remarks"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Detail No"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-22 15:32:56.629430",
|
||||
"modified": "2023-06-29 12:24:20.500632",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
|
||||
@@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
cr_note1.return_against = si3.name
|
||||
cr_note1 = cr_note1.save().submit()
|
||||
|
||||
pl_entries = (
|
||||
pl_entries_si3 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
@@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
pl_entries_cr_note1 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||
)
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values_for_si3 = [
|
||||
{
|
||||
"voucher_type": si3.doctype,
|
||||
"voucher_no": si3.name,
|
||||
@@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
"against_voucher_no": si3.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
}
|
||||
]
|
||||
# credit/debit notes post ledger entries against itself
|
||||
expected_values_for_cr_note1 = [
|
||||
{
|
||||
"voucher_type": cr_note1.doctype,
|
||||
"voucher_no": cr_note1.name,
|
||||
"against_voucher_type": si3.doctype,
|
||||
"against_voucher_no": si3.name,
|
||||
"against_voucher_type": cr_note1.doctype,
|
||||
"against_voucher_no": cr_note1.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
self.assertEqual(pl_entries_si3, expected_values_for_si3)
|
||||
self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
|
||||
|
||||
def test_je_against_inv_and_note(self):
|
||||
ple = self.ple
|
||||
|
||||
@@ -124,7 +124,7 @@ frappe.ui.form.on('Payment Order', {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_order.payment_order.make_payment_records",
|
||||
args: {
|
||||
"name": me.frm.doc.name,
|
||||
"name": frm.doc.name,
|
||||
"supplier": args.supplier,
|
||||
"mode_of_payment": args.mode_of_payment
|
||||
},
|
||||
|
||||
@@ -24,7 +24,19 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
filters: {
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
|
||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type],
|
||||
"root_type": this.frm.doc.party_type == 'Customer' ? "Asset" : "Liability"
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('default_advance_account', () => {
|
||||
return {
|
||||
filters: {
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": this.frm.doc.party_type == 'Customer' ? "Receivable": "Payable",
|
||||
"root_type": this.frm.doc.party_type == 'Customer' ? "Liability": "Asset"
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -85,25 +97,29 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
|
||||
if(enabled) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'is_auto_process_enabled',
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -124,19 +140,20 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.trigger("clear_child_tables");
|
||||
|
||||
if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) {
|
||||
return frappe.call({
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party
|
||||
party: this.frm.doc.party,
|
||||
include_advance: 1
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
this.frm.refresh();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,6 +164,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.refresh();
|
||||
}
|
||||
|
||||
invoice_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
payment_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
|
||||
clear_child_tables() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"column_break_4",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"col_break1",
|
||||
"from_invoice_date",
|
||||
"from_payment_date",
|
||||
@@ -26,8 +27,10 @@
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
"payment_name",
|
||||
"payments",
|
||||
"sec_break2",
|
||||
"allocation"
|
||||
@@ -136,6 +139,7 @@
|
||||
"label": "Minimum Invoice Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "invoice_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -166,6 +170,7 @@
|
||||
"label": "Maximum Payment Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "payment_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -185,13 +190,31 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"fieldname": "default_advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Payment"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-29 15:37:10.246831",
|
||||
"modified": "2023-08-15 05:35:50.109290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
@@ -14,6 +15,7 @@ from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_rec
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
create_gain_loss_journal,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
@@ -55,12 +57,31 @@ class PaymentReconciliation(Document):
|
||||
self.add_payment_entries(non_reconciled_payments)
|
||||
|
||||
def get_payment_entries(self):
|
||||
if self.default_advance_account:
|
||||
party_account = [self.receivable_payable_account, self.default_advance_account]
|
||||
else:
|
||||
party_account = [self.receivable_payable_account]
|
||||
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
condition = frappe._dict(
|
||||
{
|
||||
"company": self.get("company"),
|
||||
"get_payments": True,
|
||||
"cost_center": self.get("cost_center"),
|
||||
"from_payment_date": self.get("from_payment_date"),
|
||||
"to_payment_date": self.get("to_payment_date"),
|
||||
"maximum_payment_amount": self.get("maximum_payment_amount"),
|
||||
"minimum_payment_amount": self.get("minimum_payment_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
if self.payment_name:
|
||||
condition.update({"name": self.payment_name})
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
party_account,
|
||||
order_doctype,
|
||||
against_all_orders=True,
|
||||
limit=self.payment_limit,
|
||||
@@ -72,6 +93,9 @@ class PaymentReconciliation(Document):
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
|
||||
if self.payment_name:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
|
||||
@@ -92,7 +116,7 @@ class PaymentReconciliation(Document):
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||
t2.account_currency as currency
|
||||
t2.account_currency as currency, t2.cost_center as cost_center
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
@@ -129,6 +153,15 @@ class PaymentReconciliation(Document):
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
|
||||
conditions = []
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
@@ -136,11 +169,7 @@ class PaymentReconciliation(Document):
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -157,15 +186,12 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
if self.return_invoices:
|
||||
ple_query = QueryPaymentLedger()
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
vouchers=self.return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
@@ -183,6 +209,7 @@ class PaymentReconciliation(Document):
|
||||
"amount": -(inv.outstanding_in_account_currency),
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
"cost_center": inv.cost_center,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -209,6 +236,8 @@ class PaymentReconciliation(Document):
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
limit=self.invoice_limit,
|
||||
voucher_no=self.invoice_name,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
@@ -252,10 +281,19 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_auto_process_enabled(self):
|
||||
return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments")
|
||||
|
||||
@frappe.whitelist()
|
||||
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
|
||||
if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_entry[0]["exchange_rate"] = invoice_exchange_map.get(
|
||||
payment_entry[0].get("reference_name")
|
||||
)
|
||||
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
@@ -320,6 +358,7 @@ class PaymentReconciliation(Document):
|
||||
"allocated_amount": allocated_amount,
|
||||
"difference_amount": pay.get("difference_amount"),
|
||||
"currency": inv.get("currency"),
|
||||
"cost_center": pay.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -343,9 +382,6 @@ class PaymentReconciliation(Document):
|
||||
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if payment_details.difference_amount:
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
|
||||
@@ -378,57 +414,6 @@ class PaymentReconciliation(Document):
|
||||
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def make_difference_entry(self, row):
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = nowdate()
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
party_account_currency = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
)
|
||||
difference_account_currency = frappe.get_cached_value(
|
||||
"Account", row.difference_account, "account_currency"
|
||||
)
|
||||
|
||||
# Account Currency has balance
|
||||
dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": self.receivable_payable_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": row.against_voucher_type,
|
||||
"reference_name": row.against_voucher,
|
||||
dr_or_cr: flt(row.difference_amount),
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": row.difference_account,
|
||||
"account_currency": difference_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
|
||||
reverse_dr_or_cr: flt(row.difference_amount),
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
journal_entry.submit()
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -448,6 +433,7 @@ class PaymentReconciliation(Document):
|
||||
"allocated_amount": flt(row.get("allocated_amount")),
|
||||
"difference_amount": flt(row.get("difference_amount")),
|
||||
"difference_account": row.get("difference_account"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -620,7 +606,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
inv.dr_or_cr: abs(inv.allocated_amount),
|
||||
"reference_type": inv.against_voucher_type,
|
||||
"reference_name": inv.against_voucher,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
|
||||
},
|
||||
{
|
||||
"account": inv.account,
|
||||
@@ -633,10 +621,45 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
),
|
||||
"reference_type": inv.voucher_type,
|
||||
"reference_name": inv.voucher_no,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
jv.remark = None
|
||||
jv.flags.skip_remarks_creation = True
|
||||
jv.is_system_generated = True
|
||||
jv.submit()
|
||||
|
||||
if inv.difference_amount != 0:
|
||||
# make gain/loss journal
|
||||
if inv.party_type == "Customer":
|
||||
dr_or_cr = "credit" if inv.difference_amount < 0 else "debit"
|
||||
else:
|
||||
dr_or_cr = "debit" if inv.difference_amount < 0 else "credit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
inv.difference_account,
|
||||
inv.difference_amount,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
inv.voucher_type,
|
||||
inv.voucher_no,
|
||||
None,
|
||||
inv.against_voucher_type,
|
||||
inv.against_voucher,
|
||||
None,
|
||||
inv.cost_center,
|
||||
)
|
||||
|
||||
@@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_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.accounts.party import get_party_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestPaymentReconciliation(FrappeTestCase):
|
||||
def setUp(self):
|
||||
@@ -163,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
def create_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party_type = (
|
||||
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
|
||||
)
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
@@ -681,14 +686,24 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(debit) as amount",
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
self.assertEqual(flt(total_debit_amount, 2), -500)
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
@@ -890,6 +905,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||
|
||||
def test_reconciliation_purchase_invoice_against_return(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
|
||||
).submit()
|
||||
|
||||
pi_return = frappe.get_doc(pi.as_dict())
|
||||
pi_return.name = None
|
||||
pi_return.docstatus = 0
|
||||
pi_return.is_return = 1
|
||||
pi_return.conversion_rate = 80
|
||||
pi_return.items[0].qty = -pi_return.items[0].qty
|
||||
pi_return.submit()
|
||||
|
||||
self.company = "_Test Company"
|
||||
self.party_type = "Supplier"
|
||||
self.customer = "_Test Supplier USD"
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = []
|
||||
payments = []
|
||||
for invoice in pr.invoices:
|
||||
if invoice.invoice_number == pi.name:
|
||||
invoices.append(invoice.as_dict())
|
||||
break
|
||||
for payment in pr.payments:
|
||||
if payment.reference_name == pi_return.name:
|
||||
payments.append(payment.as_dict())
|
||||
break
|
||||
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user