mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 04:29:18 +00:00
Compare commits
629 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b38b900c | ||
|
|
235f1664e0 | ||
|
|
21ad1dba19 | ||
|
|
9c356a0ac2 | ||
|
|
930aee1d70 | ||
|
|
a8b26ebc6b | ||
|
|
b2b8b72952 | ||
|
|
555ebc190c | ||
|
|
58eaaaa8ad | ||
|
|
df00bde748 | ||
|
|
00bbf0fbc5 | ||
|
|
99338b6d01 | ||
|
|
f6418c14dd | ||
|
|
42ccc092fe | ||
|
|
222bce62bf | ||
|
|
487aa35ee3 | ||
|
|
0f848fd968 | ||
|
|
c117ab851a | ||
|
|
b7b62a8966 | ||
|
|
86cf156968 | ||
|
|
25718d9f1b | ||
|
|
9b66a06c86 | ||
|
|
7c2bd24d0b | ||
|
|
af57cc6c5f | ||
|
|
25448fcdbc | ||
|
|
69e83ff6ab | ||
|
|
87a8c8d08e | ||
|
|
4c01128827 | ||
|
|
712ddb75be | ||
|
|
78a9edf6c9 | ||
|
|
20ca948e6b | ||
|
|
f1407bcfd2 | ||
|
|
b605b08ec1 | ||
|
|
fabcfc1fce | ||
|
|
cfb9d8c6be | ||
|
|
6a6a5b0a75 | ||
|
|
3f296eea3a | ||
|
|
8e4e4a9fb4 | ||
|
|
254dd3d9bf | ||
|
|
83b3785202 | ||
|
|
b017f4a817 | ||
|
|
52cfe3f612 | ||
|
|
fd21dcd3b5 | ||
|
|
fea27d5e2e | ||
|
|
848928e7d1 | ||
|
|
0f4e50185e | ||
|
|
a0893ddf96 | ||
|
|
556095daaa | ||
|
|
375be8cd93 | ||
|
|
38665760cd | ||
|
|
f5120a6dd0 | ||
|
|
519bf3d377 | ||
|
|
cef2231d6a | ||
|
|
e457d39b5d | ||
|
|
7558b622a4 | ||
|
|
c58fefb359 | ||
|
|
e96d5b314a | ||
|
|
a615535ca5 | ||
|
|
fa490ef2f0 | ||
|
|
e01e9ebc37 | ||
|
|
89f07dcfac | ||
|
|
d02ebd6215 | ||
|
|
f5e9913746 | ||
|
|
a7b75a4cc1 | ||
|
|
6b951bd17a | ||
|
|
912c315286 | ||
|
|
f35a0c227d | ||
|
|
2317f6a000 | ||
|
|
91130854d8 | ||
|
|
677d728e67 | ||
|
|
fa5780ca81 | ||
|
|
80774e2da1 | ||
|
|
72d32a4901 | ||
|
|
638c271d70 | ||
|
|
356b1bdb38 | ||
|
|
0b082f0edc | ||
|
|
be90be37c7 | ||
|
|
922ace4076 | ||
|
|
920de792ce | ||
|
|
d576fc7490 | ||
|
|
7b9daeff66 | ||
|
|
c921d7d409 | ||
|
|
c8922ad566 | ||
|
|
d71b885fb8 | ||
|
|
f413530493 | ||
|
|
ae788e8358 | ||
|
|
515bed8c80 | ||
|
|
063d658b04 | ||
|
|
fa7fa85d92 | ||
|
|
ca13816ca1 | ||
|
|
78b7c26420 | ||
|
|
8e31379ecc | ||
|
|
c05e0a4ffc | ||
|
|
ec208b8df5 | ||
|
|
1cb9f4cf8b | ||
|
|
50daf701dd | ||
|
|
8660faaa54 | ||
|
|
6931db98f1 | ||
|
|
fdb2e94b5c | ||
|
|
e1504efd40 | ||
|
|
95abd7908f | ||
|
|
022f85dd08 | ||
|
|
837a6b5f50 | ||
|
|
f6a550faae | ||
|
|
54f672e144 | ||
|
|
e8d082560a | ||
|
|
ac7d6d6d59 | ||
|
|
e2e89492e0 | ||
|
|
760eab961d | ||
|
|
3499089323 | ||
|
|
7db6988364 | ||
|
|
bfa93cd3f6 | ||
|
|
e23710bf00 | ||
|
|
5025850258 | ||
|
|
473610506c | ||
|
|
71cb7d37ee | ||
|
|
5b1016c17d | ||
|
|
d598dad50e | ||
|
|
76ef61c24f | ||
|
|
001c230688 | ||
|
|
44f7de0f31 | ||
|
|
c32258e4b6 | ||
|
|
1b94510f08 | ||
|
|
65b7fb1293 | ||
|
|
d1f6d62d72 | ||
|
|
77e7a6cde7 | ||
|
|
78e22af3ca | ||
|
|
7f903532f3 | ||
|
|
8d1eac89e3 | ||
|
|
d78316869b | ||
|
|
33becb7b32 | ||
|
|
b97fdbe6fc | ||
|
|
5699a8daa2 | ||
|
|
91a5bd8615 | ||
|
|
485cb7dd28 | ||
|
|
9d6b434d1f | ||
|
|
405d1528c3 | ||
|
|
f1814a1a2a | ||
|
|
9406ddbff0 | ||
|
|
bae6c5bf5f | ||
|
|
cf9acd1ff6 | ||
|
|
6f143d35aa | ||
|
|
d266423011 | ||
|
|
d2b22db500 | ||
|
|
c43d0b81e3 | ||
|
|
cf0ab51348 | ||
|
|
0590f21814 | ||
|
|
bbdf26c82e | ||
|
|
020d2e2ca6 | ||
|
|
f434548204 | ||
|
|
f9b2355066 | ||
|
|
2815952a03 | ||
|
|
33f4fae8cd | ||
|
|
d37a1811db | ||
|
|
8dd26949b7 | ||
|
|
26ad688584 | ||
|
|
48ceead9d0 | ||
|
|
9dd33739f9 | ||
|
|
e4ed1d684d | ||
|
|
573b159541 | ||
|
|
fb0b426fe4 | ||
|
|
63ff1f1eaa | ||
|
|
4d79a4e7b3 | ||
|
|
97f9656460 | ||
|
|
14ddcd6c24 | ||
|
|
9689c1fcec | ||
|
|
ee1255a716 | ||
|
|
b0ac097327 | ||
|
|
5d97a69e32 | ||
|
|
0aad942312 | ||
|
|
debfbc4761 | ||
|
|
3dc68e3b00 | ||
|
|
bec3e8ed96 | ||
|
|
4123e7b244 | ||
|
|
c9bcf79e83 | ||
|
|
677525b2cf | ||
|
|
8c83bbc096 | ||
|
|
a512d27dbb | ||
|
|
6c8a65e03b | ||
|
|
2d13dda49c | ||
|
|
cde848dc7f | ||
|
|
79e414cb97 | ||
|
|
f5245f6b3f | ||
|
|
dc804f216d | ||
|
|
bc0db696c9 | ||
|
|
06b04770fc | ||
|
|
b22ac137f5 | ||
|
|
eed58634ba | ||
|
|
24852e46c1 | ||
|
|
50521d4efe | ||
|
|
8fc705ea6a | ||
|
|
63274527d2 | ||
|
|
aba51ee352 | ||
|
|
4bd83b5058 | ||
|
|
75d3093aea | ||
|
|
669d692844 | ||
|
|
606c99e57c | ||
|
|
cf308912a1 | ||
|
|
f5718390b7 | ||
|
|
8954bd7759 | ||
|
|
3cbaea389b | ||
|
|
335cb5fd28 | ||
|
|
cd2d335256 | ||
|
|
1a69db0f80 | ||
|
|
84e4a2509c | ||
|
|
f4e1959cc7 | ||
|
|
7651ecbc2b | ||
|
|
e464f5e419 | ||
|
|
1e93d0bcc4 | ||
|
|
b886589657 | ||
|
|
3a670264b2 | ||
|
|
2fd500ce26 | ||
|
|
9422422dcc | ||
|
|
37fc82cd11 | ||
|
|
cb35218eec | ||
|
|
9531a45b94 | ||
|
|
fb41f5f88c | ||
|
|
b9647ac0a4 | ||
|
|
77fa0f68df | ||
|
|
ae8355c953 | ||
|
|
c42ef922d2 | ||
|
|
e58b3b11e9 | ||
|
|
8f0e10bfbe | ||
|
|
77d719af6e | ||
|
|
3f59518d01 | ||
|
|
24b1100c8f | ||
|
|
c29eab12df | ||
|
|
15d2024b8e | ||
|
|
1480acabb0 | ||
|
|
4c337a6f44 | ||
|
|
63f45739e0 | ||
|
|
55a9a8fd51 | ||
|
|
c1d40a6bfa | ||
|
|
bbc13c719e | ||
|
|
3b49079ad7 | ||
|
|
6fad2bad11 | ||
|
|
1ca0516fe5 | ||
|
|
04b8527ba8 | ||
|
|
98a9007e9f | ||
|
|
64a50cfabf | ||
|
|
587a965bdf | ||
|
|
52c0900576 | ||
|
|
67a43c353c | ||
|
|
cf9fc552f0 | ||
|
|
6947686141 | ||
|
|
2b38b780ba | ||
|
|
0ecd7d2bf5 | ||
|
|
9f1b9320e9 | ||
|
|
12a7cb21b5 | ||
|
|
643bb0511c | ||
|
|
e975a10a75 | ||
|
|
c3aeb2dec5 | ||
|
|
fe32787e6e | ||
|
|
f3b872a8e2 | ||
|
|
020aedb8b0 | ||
|
|
82e8606b3c | ||
|
|
6daea6ccb2 | ||
|
|
f7de825e89 | ||
|
|
04f0dfb691 | ||
|
|
95e0bf5e0e | ||
|
|
7f1483ad70 | ||
|
|
7fd4d3c882 | ||
|
|
cfb3a9eabf | ||
|
|
21c1c7194b | ||
|
|
81c4279a30 | ||
|
|
01b54134ae | ||
|
|
28756bf7b6 | ||
|
|
4962b67358 | ||
|
|
403ff697e9 | ||
|
|
6d7aa2ae94 | ||
|
|
2b30727fdc | ||
|
|
49f0f1ca09 | ||
|
|
8fe4a4d3aa | ||
|
|
1b86e7e2f5 | ||
|
|
53817e463f | ||
|
|
6a8146ba8a | ||
|
|
b18678df4d | ||
|
|
8d813e3256 | ||
|
|
c6966f4e2d | ||
|
|
abb6044291 | ||
|
|
e752027709 | ||
|
|
e483b4a78a | ||
|
|
92eabe3cf5 | ||
|
|
1dc58b3660 | ||
|
|
0a0d5b3e66 | ||
|
|
78ab2013e5 | ||
|
|
70e2093941 | ||
|
|
279f21d1e5 | ||
|
|
ba45299e0d | ||
|
|
c9ba777e3c | ||
|
|
ab8a4f7f7b | ||
|
|
25f800d3f5 | ||
|
|
2b8118c5e6 | ||
|
|
4250a7c4d1 | ||
|
|
81d52b16d9 | ||
|
|
e0da8d261f | ||
|
|
57c82c1800 | ||
|
|
b4bc44db4a | ||
|
|
d3c9a6d975 | ||
|
|
9d5fce9091 | ||
|
|
139a7ce911 | ||
|
|
591c720e51 | ||
|
|
1181dcf521 | ||
|
|
02fc67c83c | ||
|
|
43f3779f10 | ||
|
|
f2bcfb5f97 | ||
|
|
03e52d3859 | ||
|
|
56657b6122 | ||
|
|
2713925628 | ||
|
|
428870a65d | ||
|
|
c5df164e1c | ||
|
|
8c14dbd7ac | ||
|
|
8236814270 | ||
|
|
1cb8c64c94 | ||
|
|
699ad80802 | ||
|
|
480a0ca7a8 | ||
|
|
29ff0ce286 | ||
|
|
af05864e6d | ||
|
|
dfe5f63f59 | ||
|
|
366325ca3c | ||
|
|
e62b783f34 | ||
|
|
8ef548f999 | ||
|
|
4b700b726f | ||
|
|
c41cb3930c | ||
|
|
46f94cf387 | ||
|
|
79321f56ca | ||
|
|
18702841af | ||
|
|
8b2328c6d3 | ||
|
|
13aaff30a5 | ||
|
|
a563fed6dc | ||
|
|
19a227a970 | ||
|
|
727dcc5034 | ||
|
|
89b570ecf5 | ||
|
|
b33db6c79a | ||
|
|
e5177a6e46 | ||
|
|
fffa13f22b | ||
|
|
f2395a9297 | ||
|
|
3ecdf028f2 | ||
|
|
8772e40bae | ||
|
|
b56c9b91f1 | ||
|
|
c2a0c1e989 | ||
|
|
461b607a6a | ||
|
|
9bc44a3b40 | ||
|
|
50cfc68d2c | ||
|
|
aa0a756111 | ||
|
|
413b40f5a7 | ||
|
|
7af8aec879 | ||
|
|
69454fc804 | ||
|
|
d278b11603 | ||
|
|
66027877d3 | ||
|
|
1492ce507c | ||
|
|
21be889a77 | ||
|
|
a35abf8403 | ||
|
|
619644af04 | ||
|
|
5c0a232e0f | ||
|
|
7de3d08ce3 | ||
|
|
fb7ca8fca2 | ||
|
|
d9f15e96b5 | ||
|
|
acd9c69201 | ||
|
|
284181d766 | ||
|
|
f9f1ac3601 | ||
|
|
53270dd933 | ||
|
|
657ca7ff22 | ||
|
|
2077b2cde4 | ||
|
|
7ea53cc316 | ||
|
|
7acf732a9c | ||
|
|
43d9a10093 | ||
|
|
2ae4463b76 | ||
|
|
62569b1b41 | ||
|
|
24a4815250 | ||
|
|
1894371b68 | ||
|
|
e210b28f0d | ||
|
|
f251d6cb69 | ||
|
|
4fede56d98 | ||
|
|
a4b80d1ec4 | ||
|
|
260567e99e | ||
|
|
fe69d5364d | ||
|
|
58163d5aa8 | ||
|
|
dbeb132688 | ||
|
|
e3d64fc553 | ||
|
|
c125dea0f1 | ||
|
|
119273639c | ||
|
|
fc79af5926 | ||
|
|
035eaa5e40 | ||
|
|
09e2f24329 | ||
|
|
a0c65342a1 | ||
|
|
01eae2b758 | ||
|
|
b43c6ff1ae | ||
|
|
18f8f7f09c | ||
|
|
2d2bcd37cb | ||
|
|
068f1b5a6b | ||
|
|
fca154fd60 | ||
|
|
451cc7bc12 | ||
|
|
11e67c7dc0 | ||
|
|
5c8bee0a95 | ||
|
|
553ff11de6 | ||
|
|
5523bc5081 | ||
|
|
c8d81cc52d | ||
|
|
d24c8b1bbc | ||
|
|
7fd96d0116 | ||
|
|
475750302b | ||
|
|
9168b3b0e8 | ||
|
|
0ff1546b9b | ||
|
|
345c6084e5 | ||
|
|
3820953022 | ||
|
|
78051e7f0f | ||
|
|
61752ac2b4 | ||
|
|
5a226a8395 | ||
|
|
543e4c87ea | ||
|
|
0a0b94e1cb | ||
|
|
98c26403c1 | ||
|
|
d19716142e | ||
|
|
83e3402fea | ||
|
|
9bc2b419e3 | ||
|
|
b9a5d54e03 | ||
|
|
a8b58800bb | ||
|
|
0a632660e0 | ||
|
|
09d9263082 | ||
|
|
38d27a13c3 | ||
|
|
46bea0e56f | ||
|
|
132957f59e | ||
|
|
420536ca52 | ||
|
|
b95ab3425a | ||
|
|
16edf3ac36 | ||
|
|
22247cfa17 | ||
|
|
d2091cc22c | ||
|
|
9789b7bdef | ||
|
|
e8d7d30682 | ||
|
|
05f657e690 | ||
|
|
0350c69856 | ||
|
|
adc87f16a3 | ||
|
|
bd41cb221b | ||
|
|
7a5b454e97 | ||
|
|
256c3c81a4 | ||
|
|
9b2a84f259 | ||
|
|
c07548a612 | ||
|
|
0f98cc85e9 | ||
|
|
aad91c02f3 | ||
|
|
587283e787 | ||
|
|
78b0a52d41 | ||
|
|
c4d338a59b | ||
|
|
bc6bd81f87 | ||
|
|
fd4159423d | ||
|
|
95a6e1d855 | ||
|
|
e8dc63c89c | ||
|
|
6edfcf4de8 | ||
|
|
4fa07777e9 | ||
|
|
b1ebd81f77 | ||
|
|
20c45c7975 | ||
|
|
4a4cba0715 | ||
|
|
ab9da5281e | ||
|
|
c5080abd46 | ||
|
|
a8c53eeb93 | ||
|
|
d727a13562 | ||
|
|
4556c36736 | ||
|
|
d1b2ce35b6 | ||
|
|
a0a3519cd3 | ||
|
|
f16386bd07 | ||
|
|
94612b90ef | ||
|
|
6a9935c00e | ||
|
|
873ee384a1 | ||
|
|
13b1263723 | ||
|
|
6761874e28 | ||
|
|
19eb6af65f | ||
|
|
0dc0e287a3 | ||
|
|
a0575ed2b0 | ||
|
|
cb9aad3e87 | ||
|
|
37cee42561 | ||
|
|
928e475824 | ||
|
|
296a4d7a12 | ||
|
|
fe78076cde | ||
|
|
d082e68e83 | ||
|
|
4606079568 | ||
|
|
3634e80341 | ||
|
|
e1bd9a7e8d | ||
|
|
72d9dc6c85 | ||
|
|
9ec11d9502 | ||
|
|
2f92981afe | ||
|
|
c74a414313 | ||
|
|
e8f1c82089 | ||
|
|
6366f5aadb | ||
|
|
933d4bfceb | ||
|
|
de3b67c327 | ||
|
|
d5a596dff1 | ||
|
|
620b21fec5 | ||
|
|
5bd2a0923f | ||
|
|
e62ffa990d | ||
|
|
01ae513f70 | ||
|
|
37ef6e959b | ||
|
|
2a467a9fbf | ||
|
|
7ac35b496a | ||
|
|
cdb66bf198 | ||
|
|
8530a28c62 | ||
|
|
2c8c3a022c | ||
|
|
1269f2d301 | ||
|
|
11ba553dbd | ||
|
|
248d4082c0 | ||
|
|
f578c3219d | ||
|
|
c1f1a21714 | ||
|
|
3198f2669d | ||
|
|
a623469778 | ||
|
|
ee5a1d91ff | ||
|
|
0a4947a8f6 | ||
|
|
2aa3b30b56 | ||
|
|
641fe7738c | ||
|
|
9668615f7e | ||
|
|
506ac10119 | ||
|
|
36147ec02c | ||
|
|
3dd642441b | ||
|
|
f186015f58 | ||
|
|
1f76c6972b | ||
|
|
c308bd5309 | ||
|
|
3f33d4cf76 | ||
|
|
47345e81a1 | ||
|
|
3e23e1fb91 | ||
|
|
b6134749c3 | ||
|
|
f8d6fe6be0 | ||
|
|
b773465b41 | ||
|
|
787118d00b | ||
|
|
67c8350f70 | ||
|
|
cba1c63beb | ||
|
|
2f0e7fcb4a | ||
|
|
44c1b91ca7 | ||
|
|
1deebe8757 | ||
|
|
99777d3fa4 | ||
|
|
33d5250cec | ||
|
|
e55c160438 | ||
|
|
3a82eb4ccf | ||
|
|
ca34b63470 | ||
|
|
83cbc1bef6 | ||
|
|
716d5c0b98 | ||
|
|
90b390c2c5 | ||
|
|
b131f70ed6 | ||
|
|
18cf93d1c8 | ||
|
|
2e6bfa36de | ||
|
|
b3f4c14a26 | ||
|
|
946aadb0c0 | ||
|
|
f92453ae45 | ||
|
|
7469018d3e | ||
|
|
61afffc908 | ||
|
|
ed0881dacb | ||
|
|
efb293398a | ||
|
|
8d32a1f4b3 | ||
|
|
4c527d6bba | ||
|
|
2a61d854d3 | ||
|
|
052abcb075 | ||
|
|
3542df70f6 | ||
|
|
c5c440b7bc | ||
|
|
349601b4b9 | ||
|
|
09e9b16b93 | ||
|
|
39c439dc4b | ||
|
|
72a507f888 | ||
|
|
22dbe52586 | ||
|
|
1999132c28 | ||
|
|
57af6d9c21 | ||
|
|
a9faa92796 | ||
|
|
d9219dc7d2 | ||
|
|
395f87a0f8 | ||
|
|
4094fbc3e5 | ||
|
|
513721c338 | ||
|
|
077d98e0fa | ||
|
|
8be312b73e | ||
|
|
197e5881aa | ||
|
|
7c3fc7eb3b | ||
|
|
01953bc0e3 | ||
|
|
075a7dfe2e | ||
|
|
86aead3d45 | ||
|
|
e20b213737 | ||
|
|
4025442491 | ||
|
|
f6bb6b78db | ||
|
|
00a26ea6e6 | ||
|
|
e2c35f8c85 | ||
|
|
72e88d22ed | ||
|
|
220bf24555 | ||
|
|
1f76dde025 | ||
|
|
59b7e96255 | ||
|
|
d030f4a100 | ||
|
|
6c6acef78e | ||
|
|
a9da619903 | ||
|
|
72005bdb39 | ||
|
|
287af687cf | ||
|
|
db46987d4b | ||
|
|
44110860b4 | ||
|
|
c06a6bfc09 | ||
|
|
ac0fff7e94 | ||
|
|
3044f46c52 | ||
|
|
0a832bc979 | ||
|
|
e5bc888798 | ||
|
|
9172985a9b | ||
|
|
a3032910a7 | ||
|
|
ce08f038d2 | ||
|
|
df632d7ecb | ||
|
|
1693905703 | ||
|
|
21d3fb0625 | ||
|
|
c83f10f638 | ||
|
|
b901cfdbe2 | ||
|
|
cbcdf30840 | ||
|
|
8b13185c25 | ||
|
|
1377cf4cf1 | ||
|
|
4ca1f3b9cf | ||
|
|
347c67a0f1 | ||
|
|
2912648151 | ||
|
|
ddcf555486 | ||
|
|
e4e1f03bac | ||
|
|
63c061e5e8 | ||
|
|
96663d7e28 | ||
|
|
ee04c6d5c5 | ||
|
|
a4d6e0092c | ||
|
|
9821f90350 | ||
|
|
5488785fd8 | ||
|
|
25ac0ce1cc | ||
|
|
727a379581 | ||
|
|
29181274c8 | ||
|
|
35450d7bd9 | ||
|
|
bb112eca05 | ||
|
|
2800ad39d2 | ||
|
|
b49309c160 | ||
|
|
a04471024d | ||
|
|
9b0d30c56b | ||
|
|
00c7dbceaa | ||
|
|
5443592d84 | ||
|
|
a864e07d4f | ||
|
|
19cfcea78e | ||
|
|
8083c0b59e | ||
|
|
8abc0adb18 | ||
|
|
fe41be953d | ||
|
|
c2b5417cb8 | ||
|
|
d273948d7a | ||
|
|
eb2f68ec98 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.34.1"
|
||||
__version__ = "14.46.6"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -117,9 +117,6 @@ frappe.ui.form.on('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) {
|
||||
|
||||
@@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAccountMergeError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Account(NestedSet):
|
||||
nsm_parent_field = "parent_account"
|
||||
|
||||
@@ -444,24 +448,35 @@ 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
|
||||
if not frappe.db.exists("Account", new):
|
||||
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))
|
||||
|
||||
val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
|
||||
|
||||
if val != [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 frappe.db.get_value("Account", new, "parent_account") == old:
|
||||
frappe.db.set_value(
|
||||
"Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = {
|
||||
accounts = nodes;
|
||||
}
|
||||
|
||||
const get_balances = frappe.call({
|
||||
method: 'erpnext.accounts.utils.get_account_balances',
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company
|
||||
},
|
||||
});
|
||||
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
|
||||
if(value) {
|
||||
|
||||
get_balances.then(r => {
|
||||
if (!r.message || r.message.length == 0) return;
|
||||
const get_balances = frappe.call({
|
||||
method: 'erpnext.accounts.utils.get_account_balances',
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company
|
||||
},
|
||||
});
|
||||
|
||||
for (let account of r.message) {
|
||||
get_balances.then(r => {
|
||||
if (!r.message || r.message.length == 0) return;
|
||||
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
for (let account of r.message) {
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
if (account.balance!==undefined) {
|
||||
node.parent && node.parent.find('.balance-area').remove();
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (account.balance_in_account_currency ?
|
||||
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
|
||||
+ format(account.balance, account.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
if (account.balance!==undefined) {
|
||||
node.parent && node.parent.find('.balance-area').remove();
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (account.balance_in_account_currency ?
|
||||
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
|
||||
+ format(account.balance, account.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
}
|
||||
)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -291,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
|
||||
|
||||
return dimension_filters, default_dimensions_map
|
||||
|
||||
|
||||
def create_accounting_dimensions_for_doctype(doctype):
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
for d in accounting_dimensions:
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": "accounting_dimensions_section",
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
"report_settings_sb",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching"
|
||||
"enable_fuzzy_matching",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -416,6 +418,17 @@
|
||||
"fieldname": "ignore_account_closing_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Account Closing Balance"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_break_dpet",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_balance_in_coa",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Balances in Chart Of Accounts"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
|
||||
@@ -17,6 +17,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
|
||||
get_entries,
|
||||
)
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class BankReconciliationTool(Document):
|
||||
@@ -129,7 +130,7 @@ def create_journal_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
@@ -143,29 +144,94 @@ def create_journal_entry_bts(
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
|
||||
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
|
||||
|
||||
# determine if multi-currency Journal or not
|
||||
is_multi_currency = (
|
||||
True
|
||||
if company_default_currency != company_account_currency
|
||||
or company_default_currency != second_account_currency
|
||||
or company_default_currency != bank_transaction.currency
|
||||
else False
|
||||
)
|
||||
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append(
|
||||
{
|
||||
"account": second_account,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
second_account_dict = {
|
||||
"account": second_account,
|
||||
"account_currency": second_account_currency,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": company_account,
|
||||
"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_account_dict = {
|
||||
"account": company_account,
|
||||
"account_currency": company_account_currency,
|
||||
"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),
|
||||
}
|
||||
|
||||
# convert transaction amount to company currency
|
||||
if is_multi_currency:
|
||||
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
|
||||
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
|
||||
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
|
||||
else:
|
||||
withdrawal_in_company_currency = bank_transaction.withdrawal
|
||||
deposit_in_company_currency = bank_transaction.deposit
|
||||
|
||||
# if second account is of foreign currency, convert and set debit and credit fields.
|
||||
if second_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
|
||||
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": deposit_in_company_currency,
|
||||
"debit_in_account_currency": withdrawal_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
# if company account is of foreign currency, convert and set debit and credit fields.
|
||||
if company_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
else:
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
"credit_in_account_currency": withdrawal_in_company_currency,
|
||||
"debit_in_account_currency": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(second_account_dict)
|
||||
accounts.append(company_account_dict)
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
@@ -175,6 +241,9 @@ def create_journal_entry_bts(
|
||||
"cheque_no": reference_number,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
}
|
||||
if is_multi_currency:
|
||||
journal_entry_dict.update({"multi_currency": True})
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.update(journal_entry_dict)
|
||||
journal_entry.set("accounts", accounts)
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bank Statement Import", {
|
||||
onload(frm) {
|
||||
frm.set_query("bank_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setup(frm) {
|
||||
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
|
||||
frm.import_in_progress = false;
|
||||
@@ -352,10 +362,11 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
export_errored_rows(frm) {
|
||||
open_url_post(
|
||||
"/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
|
||||
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template",
|
||||
{
|
||||
data_import_name: frm.doc.name,
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"api_details_section",
|
||||
"disabled",
|
||||
"service_provider",
|
||||
"api_endpoint",
|
||||
"access_key",
|
||||
"url",
|
||||
"column_break_3",
|
||||
"help",
|
||||
@@ -77,12 +79,24 @@
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
|
||||
"fieldname": "access_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "Access Key"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-10 15:51:14.521174",
|
||||
"modified": "2023-10-04 15:30:25.333860",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -18,11 +18,21 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == "exchangerate.host":
|
||||
|
||||
if not self.access_key:
|
||||
frappe.throw(
|
||||
_("Access Key is required for Service Provider: {0}").format(
|
||||
frappe.bold(self.service_provider)
|
||||
)
|
||||
)
|
||||
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||
self.append("result_key", {"key": "result"})
|
||||
self.append("req_params", {"key": "access_key", "value": self.access_key})
|
||||
self.append("req_params", {"key": "amount", "value": "1"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -58,7 +58,14 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if frappe.db.get_value("Account", self.account, "account_type") not in [
|
||||
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",
|
||||
]:
|
||||
|
||||
@@ -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', 'Asset Movement'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@@ -50,8 +50,18 @@ frappe.ui.form.on("Journal Entry", {
|
||||
frm.trigger("make_inter_company_journal_entry");
|
||||
}, __('Make'));
|
||||
}
|
||||
},
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
before_save: function(frm) {
|
||||
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
|
||||
let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
|
||||
if (payment_entry_references.length > 0) {
|
||||
let rows = payment_entry_references.map(x => "#"+x.idx);
|
||||
frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
|
||||
}
|
||||
}
|
||||
},
|
||||
make_inter_company_journal_entry: function(frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Select Company"),
|
||||
|
||||
@@ -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()
|
||||
@@ -487,11 +489,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
|
||||
@@ -574,7 +577,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,
|
||||
@@ -756,18 +761,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))
|
||||
@@ -775,6 +785,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))
|
||||
|
||||
@@ -923,6 +936,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])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -49,9 +49,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()
|
||||
|
||||
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "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);
|
||||
@@ -152,6 +152,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
|
||||
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
|
||||
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
|
||||
}, __('Actions'));
|
||||
|
||||
}
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -526,15 +533,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
|
||||
@@ -543,6 +556,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",
|
||||
@@ -552,9 +566,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;
|
||||
|
||||
@@ -870,12 +889,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"));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +24,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
get_supplier_block_status,
|
||||
@@ -61,7 +66,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate(self):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_missing_ref_details()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
@@ -100,7 +105,12 @@ class PaymentEntry(AccountsController):
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
@@ -179,84 +189,89 @@ class PaymentEntry(AccountsController):
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
if self.references:
|
||||
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
|
||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||
and d.payment_term == ""
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -361,7 +376,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
if ref_doc:
|
||||
if self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate")
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
|
||||
if not self.source_exchange_rate:
|
||||
self.source_exchange_rate = get_exchange_rate(
|
||||
@@ -374,7 +389,7 @@ class PaymentEntry(AccountsController):
|
||||
elif self.paid_to and not self.target_exchange_rate:
|
||||
if ref_doc:
|
||||
if self.paid_to_account_currency == ref_doc.currency:
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate")
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
|
||||
if not self.target_exchange_rate:
|
||||
self.target_exchange_rate = get_exchange_rate(
|
||||
@@ -783,10 +798,30 @@ class PaymentEntry(AccountsController):
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
|
||||
# Use source/target exchange rate, so no difference amount is calculated.
|
||||
# then update exchange gain/loss amount in reference table
|
||||
# if there is an exchange gain/loss amount in reference table, submit a JE for that
|
||||
|
||||
exchange_rate = 1
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.source_exchange_rate
|
||||
elif self.payment_type == "Pay":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
|
||||
# on rare case, when `exchange_rate` is unset, gain/loss amount is incorrectly calculated
|
||||
# for base currency transactions
|
||||
if d.exchange_rate is None:
|
||||
d.exchange_rate = 1
|
||||
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -977,6 +1012,10 @@ class PaymentEntry(AccountsController):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
else:
|
||||
self.make_exchange_gain_loss_journal()
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
@@ -1420,6 +1459,14 @@ def get_outstanding_reference_documents(args):
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
@@ -1438,6 +1485,7 @@ def get_outstanding_reference_documents(args):
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
vouchers=args.get("vouchers") or None,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(
|
||||
@@ -1556,11 +1604,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": flt(d.outstanding_amount),
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"allocated_amount": payment_term_outstanding
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
@@ -1836,10 +1883,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
if not total_amount:
|
||||
if party_account_currency == company_currency:
|
||||
# for handling cases that don't have multi-currency (base field)
|
||||
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
|
||||
total_amount = (
|
||||
ref_doc.get("base_rounded_total")
|
||||
or ref_doc.get("rounded_total")
|
||||
or ref_doc.get("base_grand_total")
|
||||
or ref_doc.get("grand_total")
|
||||
)
|
||||
exchange_rate = 1
|
||||
else:
|
||||
total_amount = ref_doc.get("grand_total")
|
||||
total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
|
||||
if not exchange_rate:
|
||||
# Get the exchange rate from the original ref doc
|
||||
# or get it based on the posting date of the ref doc.
|
||||
@@ -1878,7 +1930,6 @@ def get_payment_entry(
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
@@ -2019,7 +2070,7 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
@@ -2132,7 +2183,7 @@ def set_paid_amount_and_received_amount(
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
if bank and company_currency != bank.account_currency:
|
||||
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
@@ -2141,7 +2192,7 @@ def set_paid_amount_and_received_amount(
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
if bank and company_currency != bank.account_currency:
|
||||
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
|
||||
@@ -31,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")
|
||||
@@ -591,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,
|
||||
@@ -792,33 +796,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)
|
||||
|
||||
@@ -1202,6 +1201,24 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
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")
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
"label": "Posting Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
@@ -147,7 +148,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-22 15:32:56.629430",
|
||||
"modified": "2023-10-30 16:15:00.470283",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
|
||||
@@ -151,6 +151,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");
|
||||
@@ -207,6 +216,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.data = [];
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Difference Account"),
|
||||
size: 'extra-large',
|
||||
fields: [
|
||||
{
|
||||
fieldname: "allocation",
|
||||
@@ -230,6 +240,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
in_list_view: 1,
|
||||
read_only: 1
|
||||
}, {
|
||||
fieldtype:'Date',
|
||||
fieldname:"gain_loss_posting_date",
|
||||
label: __("Posting Date"),
|
||||
in_list_view: 1,
|
||||
reqd: 1,
|
||||
}, {
|
||||
|
||||
fieldtype:'Link',
|
||||
options: 'Account',
|
||||
in_list_view: 1,
|
||||
@@ -263,6 +280,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
args.forEach(d => {
|
||||
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
|
||||
"difference_account", d.difference_account);
|
||||
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
|
||||
"gain_loss_posting_date", d.gain_loss_posting_date);
|
||||
|
||||
});
|
||||
|
||||
this.reconcile_payment_entries();
|
||||
@@ -278,6 +298,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
'reference_name': d.reference_name,
|
||||
'difference_amount': d.difference_amount,
|
||||
'difference_account': d.difference_account,
|
||||
'gain_loss_posting_date': d.gain_loss_posting_date
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
"payment_name",
|
||||
"payments",
|
||||
"sec_break2",
|
||||
"allocation"
|
||||
@@ -136,6 +138,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 +169,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 +189,23 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -218,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +15,11 @@ 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,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
|
||||
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
@@ -57,7 +59,10 @@ class PaymentReconciliation(Document):
|
||||
def get_payment_entries(self):
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
payment_entries = get_advance_payment_entries(
|
||||
if self.payment_name:
|
||||
condition += "name like '%%{0}%%'".format(self.payment_name)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
@@ -72,6 +77,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 +100,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 +137,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 +153,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)
|
||||
)
|
||||
|
||||
@@ -183,6 +196,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 +223,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 = (
|
||||
@@ -260,6 +276,11 @@ class PaymentReconciliation(Document):
|
||||
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
|
||||
)
|
||||
@@ -294,6 +315,7 @@ class PaymentReconciliation(Document):
|
||||
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
|
||||
res.difference_account = default_exchange_gain_loss_account
|
||||
res.exchange_rate = inv.get("exchange_rate")
|
||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
@@ -324,10 +346,12 @@ class PaymentReconciliation(Document):
|
||||
"allocated_amount": allocated_amount,
|
||||
"difference_amount": pay.get("difference_amount"),
|
||||
"currency": inv.get("currency"),
|
||||
"cost_center": pay.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
adjust_allocations_for_taxes(self)
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -347,12 +371,6 @@ class PaymentReconciliation(Document):
|
||||
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if payment_details.difference_amount and row.reference_type not in [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
]:
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
|
||||
@@ -385,59 +403,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()
|
||||
|
||||
return journal_entry
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -457,6 +422,8 @@ class PaymentReconciliation(Document):
|
||||
"allocated_amount": flt(row.get("allocated_amount")),
|
||||
"difference_amount": flt(row.get("difference_amount")),
|
||||
"difference_account": row.get("difference_account"),
|
||||
"difference_posting_date": row.get("gain_loss_posting_date"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -603,16 +570,6 @@ class PaymentReconciliation(Document):
|
||||
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
def get_difference_row(inv):
|
||||
if inv.difference_amount != 0 and inv.difference_account:
|
||||
difference_row = {
|
||||
"account": inv.difference_account,
|
||||
inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
|
||||
reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(company),
|
||||
}
|
||||
return difference_row
|
||||
|
||||
for inv in dr_cr_notes:
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
@@ -639,7 +596,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),
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": inv.account,
|
||||
@@ -652,14 +611,50 @@ 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),
|
||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
|
||||
"exchange_rate": inv.exchange_rate,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if difference_entry := get_difference_row(inv):
|
||||
jv.append("accounts", difference_entry)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.flags.skip_remarks_creation = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
jv.is_system_generated = True
|
||||
jv.remark = None
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def adjust_allocations_for_taxes(doc):
|
||||
pass
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
@@ -85,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
||||
|
||||
def create_account(self):
|
||||
account_name = "Debtors EUR"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Receivable - _PR"
|
||||
acc.company = self.company
|
||||
acc.account_currency = "EUR"
|
||||
acc.account_type = "Receivable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.debtors_eur = acc.name
|
||||
accounts = [
|
||||
{
|
||||
"attribute": "debtors_eur",
|
||||
"account_name": "Debtors EUR",
|
||||
"parent_account": "Accounts Receivable - _PR",
|
||||
"account_currency": "EUR",
|
||||
"account_type": "Receivable",
|
||||
},
|
||||
{
|
||||
"attribute": "creditors_usd",
|
||||
"account_name": "Payable USD",
|
||||
"parent_account": "Accounts Payable - _PR",
|
||||
"account_currency": "USD",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
]
|
||||
|
||||
for x in accounts:
|
||||
x = frappe._dict(x)
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": x.account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = x.account_name
|
||||
acc.parent_account = x.parent_account
|
||||
acc.company = self.company
|
||||
acc.account_currency = x.account_currency
|
||||
acc.account_type = x.account_type
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": x.account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
setattr(self, x.attribute, acc.name)
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
@@ -151,6 +170,64 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def create_purchase_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
pinv = make_purchase_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.supplier,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return pinv
|
||||
|
||||
def create_purchase_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
pord = create_purchase_order(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.supplier,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return pord
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
@@ -163,13 +240,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_payment_reconciliation(self):
|
||||
def create_payment_reconciliation(self, party_is_customer=True):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = (
|
||||
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
|
||||
)
|
||||
pr.party = self.customer
|
||||
pr.party_type = "Customer" if party_is_customer else "Supplier"
|
||||
pr.party = self.customer if party_is_customer else self.supplier
|
||||
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()
|
||||
return pr
|
||||
@@ -686,14 +761,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
|
||||
@@ -896,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
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()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
|
||||
pi.supplier = self.supplier
|
||||
pi.currency = "USD"
|
||||
pi.conversion_rate = 50
|
||||
pi.credit_to = self.creditors_usd
|
||||
pi.save().submit()
|
||||
|
||||
pi_return = frappe.get_doc(pi.as_dict())
|
||||
pi_return.name = None
|
||||
@@ -908,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
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 = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = []
|
||||
@@ -921,6 +1011,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
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())
|
||||
@@ -931,6 +1022,121 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
||||
pr.reconcile()
|
||||
|
||||
def test_reconciliation_from_purchase_order_to_multiple_invoices(self):
|
||||
"""
|
||||
Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation
|
||||
"""
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||
pi2 = self.create_purchase_invoice(qty=10, rate=100)
|
||||
po = self.create_purchase_order(qty=20, rate=100)
|
||||
pay = get_payment_entry(po.doctype, po.name)
|
||||
# Overpay Puchase Order
|
||||
pay.paid_amount = 3000
|
||||
pay.save().submit()
|
||||
# assert total allocated and unallocated before reconciliation
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[0].reference_doctype,
|
||||
pay.references[0].reference_name,
|
||||
pay.references[0].allocated_amount,
|
||||
),
|
||||
(po.doctype, po.name, 2000),
|
||||
)
|
||||
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||
self.assertEqual(pay.unallocated_amount, 1000)
|
||||
self.assertEqual(pay.difference_amount, 0)
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 2)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
for x in pr.payments:
|
||||
self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name))
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
# partial allocation on pi1 and full allocate on pi2
|
||||
pr.allocation[0].allocated_amount = 100
|
||||
pr.reconcile()
|
||||
|
||||
# assert references and total allocated and unallocated amount
|
||||
pay.reload()
|
||||
self.assertEqual(len(pay.references), 3)
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[0].reference_doctype,
|
||||
pay.references[0].reference_name,
|
||||
pay.references[0].allocated_amount,
|
||||
),
|
||||
(po.doctype, po.name, 900),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[1].reference_doctype,
|
||||
pay.references[1].reference_name,
|
||||
pay.references[1].allocated_amount,
|
||||
),
|
||||
(pi1.doctype, pi1.name, 100),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[2].reference_doctype,
|
||||
pay.references[2].reference_name,
|
||||
pay.references[2].allocated_amount,
|
||||
),
|
||||
(pi2.doctype, pi2.name, 1000),
|
||||
)
|
||||
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||
self.assertEqual(pay.unallocated_amount, 1000)
|
||||
self.assertEqual(pay.difference_amount, 0)
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# assert references and total allocated and unallocated amount
|
||||
pay.reload()
|
||||
self.assertEqual(len(pay.references), 3)
|
||||
# PO references should be removed now
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[0].reference_doctype,
|
||||
pay.references[0].reference_name,
|
||||
pay.references[0].allocated_amount,
|
||||
),
|
||||
(pi1.doctype, pi1.name, 100),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[1].reference_doctype,
|
||||
pay.references[1].reference_name,
|
||||
pay.references[1].allocated_amount,
|
||||
),
|
||||
(pi2.doctype, pi2.name, 1000),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
pay.references[2].reference_doctype,
|
||||
pay.references[2].reference_name,
|
||||
pay.references[2].allocated_amount,
|
||||
),
|
||||
(pi1.doctype, pi1.name, 900),
|
||||
)
|
||||
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||
self.assertEqual(pay.unallocated_amount, 1000)
|
||||
self.assertEqual(pay.difference_amount, 0)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency"
|
||||
"currency",
|
||||
"cost_center"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -144,11 +146,22 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-24 21:01:14.882747",
|
||||
"modified": "2023-10-23 10:44:56.066303",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"sec_break1",
|
||||
"remark",
|
||||
"currency",
|
||||
"exchange_rate"
|
||||
"exchange_rate",
|
||||
"cost_center"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -98,11 +99,17 @@
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-08 18:18:36.268760",
|
||||
"modified": "2023-09-03 07:43:29.965353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
|
||||
@@ -249,7 +249,7 @@ class PaymentRequest(Document):
|
||||
if (
|
||||
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
||||
):
|
||||
party_amount = ref_doc.base_grand_total
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
|
||||
|
||||
@@ -144,8 +144,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
|
||||
[pr.payment_account, 6290.0, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
|
||||
[pr.payment_account, 5000.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"transaction_date",
|
||||
"posting_date",
|
||||
"fiscal_year",
|
||||
"year_start_date",
|
||||
"amended_from",
|
||||
"company",
|
||||
"column_break1",
|
||||
@@ -100,16 +101,22 @@
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Message",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "year_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Year Start Date"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-20 14:51:04.714154",
|
||||
"modified": "2023-09-11 20:19:11.810533",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -144,5 +151,6 @@
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "closing_account_head"
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def on_cancel(self):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
@@ -95,15 +95,23 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
pce = frappe.db.sql(
|
||||
"""select name from `tabPeriod Closing Voucher`
|
||||
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
||||
(self.posting_date, self.fiscal_year, self.company),
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
existing_entry = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(pcv.name)
|
||||
.where(
|
||||
(pcv.posting_date >= self.posting_date)
|
||||
& (pcv.fiscal_year == self.fiscal_year)
|
||||
& (pcv.docstatus == 1)
|
||||
& (pcv.company == self.company)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
if pce and pce[0][0]:
|
||||
|
||||
if existing_entry and existing_entry[0][0]:
|
||||
frappe.throw(
|
||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
||||
pce[0][0], self.posting_date
|
||||
existing_entry[0][0], self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
@@ -126,22 +134,31 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def make_gl_entries(self, get_opening_entries=False):
|
||||
gl_entries = self.get_gl_entries()
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if len(gl_entries) > 5000:
|
||||
if len(gl_entries + closing_entries) > 3000:
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
voucher_name=self.name,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.enqueue(
|
||||
process_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
company=self.company,
|
||||
closing_date=self.posting_date,
|
||||
queue="long",
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
process_gl_entries(gl_entries, self.name)
|
||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
@@ -322,17 +339,12 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
def process_gl_entries(gl_entries, voucher_name):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -340,6 +352,19 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
try:
|
||||
if gl_entries + closing_entries:
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.utils import add_months, today
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.utils import get_fiscal_year, now
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
@@ -153,7 +153,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
frappe.ui.form.on('POS Closing Entry Detail', {
|
||||
closing_amount: (frm, cdt, cdn) => {
|
||||
const row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount));
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
args: { "pos_profile": frm.pos_profile },
|
||||
callback: ({ message: profile }) => {
|
||||
this.update_customer_groups_settings(profile?.customer_groups);
|
||||
this.frm.set_value("company", profile?.company);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import collections
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
@@ -54,6 +56,8 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_pos()
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_duplicate_serial_no()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -154,6 +158,18 @@ class POSInvoice(SalesInvoice):
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_duplicate_serial_no(self):
|
||||
serial_nos = []
|
||||
|
||||
for row in self.get("items"):
|
||||
if row.serial_no:
|
||||
serial_nos = row.serial_no.split("\n")
|
||||
|
||||
if serial_nos:
|
||||
for key, value in collections.Counter(serial_nos).items():
|
||||
if value > 1:
|
||||
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
|
||||
|
||||
def validate_pos_reserved_batch_qty(self, item):
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
|
||||
|
||||
@@ -370,6 +386,14 @@ class POSInvoice(SalesInvoice):
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_company_with_pos_company(self):
|
||||
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
||||
frappe.throw(
|
||||
_("Company {} does not match with POS Profile Company {}").format(
|
||||
self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
|
||||
)
|
||||
)
|
||||
|
||||
def validate_loyalty_transaction(self):
|
||||
if self.redeem_loyalty_points and (
|
||||
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
|
||||
@@ -448,6 +472,7 @@ class POSInvoice(SalesInvoice):
|
||||
profile = {}
|
||||
if self.pos_profile:
|
||||
profile = frappe.get_doc("POS Profile", self.pos_profile)
|
||||
self.company = profile.get("company")
|
||||
|
||||
if not self.get("payments") and not for_validate:
|
||||
update_multi_mode_option(self, profile)
|
||||
@@ -493,7 +518,7 @@ class POSInvoice(SalesInvoice):
|
||||
selling_price_list = (
|
||||
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
|
||||
)
|
||||
if customer_currency != profile.get("currency"):
|
||||
if customer_currency and customer_currency != profile.get("currency"):
|
||||
self.set("currency", customer_currency)
|
||||
|
||||
else:
|
||||
@@ -651,7 +676,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.stock_qty
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
|
||||
@@ -464,6 +464,37 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos2.insert()
|
||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||
|
||||
def test_pos_invoice_with_duplicate_serial_no(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
se = make_serialized_item(
|
||||
company="_Test Company",
|
||||
target_warehouse="Stores - _TC",
|
||||
cost_center="Main - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
)
|
||||
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
pos = create_pos_invoice(
|
||||
company="_Test Company",
|
||||
debit_to="Debtors - _TC",
|
||||
account_for_change_amount="Cash - _TC",
|
||||
warehouse="Stores - _TC",
|
||||
income_account="Sales - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
item=se.get("items")[0].item_code,
|
||||
rate=1000,
|
||||
qty=2,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos.get("items")[0].has_serial_no = 1
|
||||
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[0]
|
||||
self.assertRaises(frappe.ValidationError, pos.submit)
|
||||
|
||||
def test_invalid_serial_no_validation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:19:30.912953",
|
||||
"modified": "2023-08-11 10:56:51.699137",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -154,15 +154,25 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"column_break_21",
|
||||
"start_date",
|
||||
"section_break_33",
|
||||
"pdf_name",
|
||||
"subject",
|
||||
"column_break_28",
|
||||
"cc_to",
|
||||
@@ -273,7 +274,7 @@
|
||||
"fieldname": "help_text",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Help Text",
|
||||
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
|
||||
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.customer_name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
@@ -368,10 +369,15 @@
|
||||
"fieldname": "based_on_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Based On Payment Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "pdf_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "PDF Name"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-06-23 10:13:15.051950",
|
||||
"modified": "2023-08-28 12:59:53.071334",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -26,7 +26,13 @@ class ProcessStatementOfAccounts(Document):
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||
if self.report == "General Ledger":
|
||||
body_str = " from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||
else:
|
||||
body_str = " until {{ doc.posting_date }}."
|
||||
self.body = "Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts" + body_str
|
||||
if not self.pdf_name:
|
||||
self.pdf_name = "{{ customer.customer_name }}"
|
||||
|
||||
validate_template(self.subject)
|
||||
validate_template(self.body)
|
||||
@@ -41,6 +47,20 @@ class ProcessStatementOfAccounts(Document):
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
|
||||
result = delimiter.join(list(statement_dict.values()))
|
||||
return get_pdf(result, {"orientation": doc.orientation})
|
||||
else:
|
||||
for customer, statement_html in statement_dict.items():
|
||||
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
||||
return statement_dict
|
||||
|
||||
|
||||
def get_statement_dict(doc, get_statement_dict=False):
|
||||
statement_dict = {}
|
||||
ageing = ""
|
||||
|
||||
@@ -59,30 +79,23 @@ def get_report_pdf(doc, consolidated=True):
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
else:
|
||||
filters.update(get_ar_filters(doc, entry))
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
col, res = get_soa(filters)
|
||||
for x in [0, -2, -1]:
|
||||
res[x]["account"] = res[x]["account"].replace("'", "")
|
||||
if len(res) == 3:
|
||||
continue
|
||||
else:
|
||||
filters.update(get_ar_filters(doc, entry))
|
||||
ar_res = get_ar_soa(filters)
|
||||
col, res = ar_res[0], ar_res[1]
|
||||
if not res:
|
||||
continue
|
||||
|
||||
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
|
||||
statement_dict[entry.customer] = (
|
||||
[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
|
||||
)
|
||||
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
result = "".join(list(statement_dict.values()))
|
||||
return get_pdf(result, {"orientation": doc.orientation})
|
||||
else:
|
||||
for customer, statement_html in statement_dict.items():
|
||||
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
||||
return statement_dict
|
||||
return statement_dict
|
||||
|
||||
|
||||
def set_ageing(doc, entry):
|
||||
@@ -95,7 +108,8 @@ def set_ageing(doc, entry):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer": entry.customer,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
}
|
||||
)
|
||||
col1, ageing = get_ageing(ageing_filters)
|
||||
@@ -138,7 +152,9 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
def get_ar_filters(doc, entry):
|
||||
return {
|
||||
"report_date": doc.posting_date if doc.posting_date else None,
|
||||
"customer": entry.customer,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"customer_name": entry.customer_name if entry.customer_name else None,
|
||||
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
|
||||
"sales_partner": doc.sales_partner if doc.sales_partner else None,
|
||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||
@@ -362,16 +378,20 @@ def download_statements(document_name):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_emails(document_name, from_scheduler=False):
|
||||
def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
||||
report = get_report_pdf(doc, consolidated=False)
|
||||
|
||||
if report:
|
||||
for customer, report_pdf in report.items():
|
||||
attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
|
||||
context = get_context(customer, doc)
|
||||
filename = frappe.render_template(doc.pdf_name, context)
|
||||
attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}]
|
||||
|
||||
recipients, cc = get_recipients_and_cc(customer, doc)
|
||||
context = get_context(customer, doc)
|
||||
if not recipients:
|
||||
continue
|
||||
|
||||
subject = frappe.render_template(doc.subject, context)
|
||||
message = frappe.render_template(doc.body, context)
|
||||
|
||||
@@ -390,7 +410,7 @@ def send_emails(document_name, from_scheduler=False):
|
||||
)
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
new_to_date = getdate(today())
|
||||
new_to_date = getdate(posting_date or today())
|
||||
if doc.frequency == "Weekly":
|
||||
new_to_date = add_days(new_to_date, 7)
|
||||
else:
|
||||
@@ -399,8 +419,11 @@ def send_emails(document_name, from_scheduler=False):
|
||||
doc.add_comment(
|
||||
"Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())
|
||||
)
|
||||
doc.db_set("to_date", new_to_date, commit=True)
|
||||
doc.db_set("from_date", new_from_date, commit=True)
|
||||
if doc.report == "General Ledger":
|
||||
doc.db_set("to_date", new_to_date, commit=True)
|
||||
doc.db_set("from_date", new_from_date, commit=True)
|
||||
else:
|
||||
doc.db_set("posting_date", new_to_date, commit=True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -410,7 +433,8 @@ def send_emails(document_name, from_scheduler=False):
|
||||
def send_auto_email():
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"to_date": format_date(today()), "enable_auto_email": 1},
|
||||
filters={"enable_auto_email": 1},
|
||||
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
|
||||
)
|
||||
for entry in selected:
|
||||
send_emails(entry.name, from_scheduler=True)
|
||||
|
||||
@@ -8,9 +8,24 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="header-html" class="hidden-pdf">
|
||||
{% if letter_head.content %}
|
||||
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||
{{ letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h4 class="text-center">
|
||||
{{ filters.customer }}
|
||||
{{ filters.customer_name }}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) %}
|
||||
@@ -341,4 +356,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if terms_and_conditions %}
|
||||
<div>
|
||||
{{ terms_and_conditions }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
|
||||
|
||||
@@ -1,9 +1,110 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
class TestProcessStatementOfAccounts(unittest.TestCase):
|
||||
pass
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
|
||||
get_statement_dict,
|
||||
send_emails,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_customer(customer_name="Other Customer")
|
||||
self.clear_old_entries()
|
||||
self.si = create_sales_invoice()
|
||||
create_sales_invoice(customer="Other Customer")
|
||||
|
||||
def test_process_soa_for_gl(self):
|
||||
"""Tests the utils for Statement of Accounts(General Ledger)"""
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA for GL",
|
||||
customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
|
||||
)
|
||||
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||
|
||||
# Checks if the statements are filtered based on the Customer
|
||||
self.assertIn("Other Customer", statement_dict)
|
||||
self.assertIn("_Test Customer", statement_dict)
|
||||
|
||||
# Checks if the correct number of receivable entries exist
|
||||
# 3 rows for opening and closing and 1 row for SI
|
||||
receivable_entries = statement_dict["_Test Customer"][0]
|
||||
self.assertEqual(len(receivable_entries), 4)
|
||||
|
||||
# Checks the amount for the receivable entry
|
||||
self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
|
||||
self.assertEqual(receivable_entries[1].balance, 100)
|
||||
|
||||
def test_process_soa_for_ar(self):
|
||||
"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
|
||||
process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
|
||||
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||
|
||||
# Checks if the statements are filtered based on the Customer
|
||||
self.assertNotIn("Other Customer", statement_dict)
|
||||
self.assertIn("_Test Customer", statement_dict)
|
||||
|
||||
# Checks if the correct number of receivable entries exist
|
||||
receivable_entries = statement_dict["_Test Customer"][0]
|
||||
self.assertEqual(len(receivable_entries), 1)
|
||||
|
||||
# Checks the amount for the receivable entry
|
||||
self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
|
||||
self.assertEqual(receivable_entries[0].total_due, 100)
|
||||
|
||||
# Checks the ageing summary for AR
|
||||
ageing_summary = statement_dict["_Test Customer"][1][0]
|
||||
expected_summary = frappe._dict(
|
||||
range1=100,
|
||||
range2=0,
|
||||
range3=0,
|
||||
range4=0,
|
||||
range5=0,
|
||||
)
|
||||
self.check_ageing_summary(ageing_summary, expected_summary)
|
||||
|
||||
def test_auto_email_for_process_soa_ar(self):
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||
)
|
||||
send_emails(process_soa.name, from_scheduler=True)
|
||||
process_soa.load_from_db()
|
||||
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
|
||||
def check_ageing_summary(self, ageing, expected_ageing):
|
||||
for age_range in expected_ageing:
|
||||
self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_process_soa(**args):
|
||||
args = frappe._dict(args)
|
||||
frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
|
||||
process_soa = frappe.new_doc("Process Statement Of Accounts")
|
||||
soa_dict = frappe._dict(
|
||||
name=args.name,
|
||||
company=args.company or "_Test Company",
|
||||
customers=args.customers or [{"customer": "_Test Customer"}],
|
||||
enable_auto_email=1 if args.enable_auto_email else 0,
|
||||
frequency=args.frequency or "Weekly",
|
||||
report=args.report or "General Ledger",
|
||||
from_date=args.from_date or getdate(today()),
|
||||
to_date=args.to_date or getdate(today()),
|
||||
posting_date=args.posting_date or getdate(today()),
|
||||
include_ageing=1,
|
||||
)
|
||||
process_soa.update(soa_dict)
|
||||
process_soa.save()
|
||||
return process_soa
|
||||
|
||||
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -59,6 +59,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||
if(doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -162,6 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
unblock_invoice() {
|
||||
@@ -460,6 +480,12 @@ cur_frm.set_query("expense_account", "items", function(doc) {
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.set_query("wip_composite_asset", "items", function() {
|
||||
return {
|
||||
filters: {'is_composite_asset': 1, 'docstatus': 0 }
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
|
||||
var d = locals[cdt][cdn];
|
||||
if(d.idx == 1 && d.expense_account){
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"column_break2",
|
||||
"buying_price_list",
|
||||
"price_list_currency",
|
||||
@@ -166,6 +167,7 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
@@ -190,8 +192,7 @@
|
||||
"inter_company_invoice_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"remarks",
|
||||
"connections_tab",
|
||||
"column_break_38"
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -987,6 +988,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cash_bank_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cash/Bank Account",
|
||||
@@ -1050,6 +1052,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:flt(doc.write_off_amount)!=0",
|
||||
"fieldname": "write_off_account",
|
||||
"fieldtype": "Link",
|
||||
@@ -1213,6 +1216,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
@@ -1345,6 +1349,7 @@
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:doc.is_internal_supplier",
|
||||
"description": "Unrealized Profit/Loss account for intra-company transfers",
|
||||
"fieldname": "unrealized_profit_loss_account",
|
||||
@@ -1377,6 +1382,7 @@
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Supplier Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
@@ -1494,10 +1500,6 @@
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -1568,13 +1570,29 @@
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Transaction Date Exchange Rate",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 17:23:59.145031",
|
||||
"modified": "2023-10-16 16:24:51.886231",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -11,6 +11,9 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
get_total_in_party_account_currency,
|
||||
@@ -30,7 +33,7 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
@@ -269,9 +272,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
|
||||
if len(asset_items) > 0:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -283,9 +284,6 @@ class PurchaseInvoice(BuyingController):
|
||||
# in case of auto inventory accounting,
|
||||
# expense account is always "Stock Received But Not Billed" for a stock item
|
||||
# except opening entry, drop-ship entry and fixed asset items
|
||||
if item.item_code:
|
||||
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
if (
|
||||
auto_accounting_for_stock
|
||||
and item.item_code in stock_items
|
||||
@@ -352,20 +350,26 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
|
||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=item.item_code, company=self.company
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", asset_category)
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -487,6 +491,11 @@ class PurchaseInvoice(BuyingController):
|
||||
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
|
||||
)
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def on_submit(self):
|
||||
super(PurchaseInvoice, self).on_submit()
|
||||
|
||||
@@ -529,6 +538,19 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
@@ -543,6 +565,7 @@ class PurchaseInvoice(BuyingController):
|
||||
merge_entries=False,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
@@ -569,12 +592,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||
|
||||
if self.auto_accounting_for_stock:
|
||||
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
||||
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||
else:
|
||||
self.stock_received_but_not_billed = None
|
||||
self.expenses_included_in_valuation = None
|
||||
|
||||
self.negative_expense_to_be_booked = 0.0
|
||||
gl_entries = []
|
||||
@@ -583,11 +605,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
if self.check_asset_cwip_enabled():
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
@@ -690,7 +708,11 @@ class PurchaseInvoice(BuyingController):
|
||||
if item.item_code:
|
||||
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items:
|
||||
if (
|
||||
self.update_stock
|
||||
and self.auto_accounting_for_stock
|
||||
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||
):
|
||||
# warehouse account
|
||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
@@ -768,21 +790,22 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(amount["amount"]),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
@@ -804,9 +827,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
elif not item.is_fixed_asset or (
|
||||
item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)
|
||||
):
|
||||
else:
|
||||
expense_account = (
|
||||
item.expense_account
|
||||
if (not item.enable_deferred_expense or self.is_return)
|
||||
@@ -899,40 +920,6 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
# If asset is bought through this document and not linked to PR
|
||||
if self.update_stock and item.landed_cost_voucher_amount:
|
||||
expenses_included_in_asset_valuation = self.get_company_default(
|
||||
"expenses_included_in_asset_valuation"
|
||||
)
|
||||
# Amount added through landed-cost-voucher
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expenses_included_in_asset_valuation,
|
||||
"against": expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": expenses_included_in_asset_valuation,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# update gross amount of asset bought through this document
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
@@ -957,11 +944,17 @@ class PurchaseInvoice(BuyingController):
|
||||
(item.purchase_receipt, valuation_tax_accounts),
|
||||
)
|
||||
|
||||
stock_rbnb = (
|
||||
self.get_company_default("asset_received_but_not_billed")
|
||||
if item.is_fixed_asset
|
||||
else self.stock_received_but_not_billed
|
||||
)
|
||||
|
||||
if not negative_expense_booked_in_pr:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.stock_received_but_not_billed,
|
||||
"account": stock_rbnb,
|
||||
"against": self.supplier,
|
||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||
@@ -976,148 +969,12 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
def get_asset_gl_entry(self, gl_entries):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.is_fixed_asset:
|
||||
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
|
||||
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
|
||||
|
||||
item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type")
|
||||
if not item.expense_account or item_exp_acc_type not in [
|
||||
"Asset Received But Not Billed",
|
||||
"Fixed Asset",
|
||||
]:
|
||||
item.expense_account = arbnb_account
|
||||
|
||||
if not self.update_stock:
|
||||
arbnb_currency = get_account_currency(item.expense_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": self.supplier,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": base_asset_amount,
|
||||
"debit_in_account_currency": (
|
||||
base_asset_amount if arbnb_currency == self.company_currency else asset_amount
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if item.item_tax_amount:
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": eiiav_account,
|
||||
"against": self.supplier,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"credit": item.item_tax_amount,
|
||||
"credit_in_account_currency": (
|
||||
item.item_tax_amount
|
||||
if asset_eiiav_currency == self.company_currency
|
||||
else item.item_tax_amount / self.conversion_rate
|
||||
),
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
else:
|
||||
cwip_account = get_asset_account(
|
||||
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
|
||||
)
|
||||
|
||||
cwip_account_currency = get_account_currency(cwip_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cwip_account,
|
||||
"against": self.supplier,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": base_asset_amount,
|
||||
"debit_in_account_currency": (
|
||||
base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
|
||||
),
|
||||
"cost_center": self.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": eiiav_account,
|
||||
"against": self.supplier,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"cost_center": item.cost_center,
|
||||
"credit": item.item_tax_amount,
|
||||
"project": item.project or self.project,
|
||||
"credit_in_account_currency": (
|
||||
item.item_tax_amount
|
||||
if asset_eiiav_currency == self.company_currency
|
||||
else item.item_tax_amount / self.conversion_rate
|
||||
),
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# When update stock is checked
|
||||
# Assets are bought through this document then it will be linked to this document
|
||||
if self.update_stock:
|
||||
if flt(item.landed_cost_voucher_amount):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": eiiav_account,
|
||||
"against": cwip_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cwip_account,
|
||||
"against": eiiav_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# update gross amount of assets bought through this document
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
|
||||
|
||||
def make_stock_adjustment_entry(
|
||||
self, gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
@@ -1422,6 +1279,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
@@ -1817,6 +1676,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"po_detail": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -33,7 +33,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
|
||||
test_ignore = ["Serial No"]
|
||||
|
||||
|
||||
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
@@ -43,6 +43,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
def tearDownClass(self):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_purchase_invoice_received_qty(self):
|
||||
"""
|
||||
1. Test if received qty is validated against accepted + rejected
|
||||
@@ -417,6 +420,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||
self.assertEqual(tax.total, expected_values[i][2])
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_purchase_invoice_with_advance(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -471,6 +475,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
)
|
||||
)
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_invoice_with_advance_and_multi_payment_terms(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -1153,7 +1158,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = deferred_account
|
||||
item.item_defaults[0].deferred_expense_account = deferred_account
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
||||
@@ -1209,6 +1214,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
@@ -1264,10 +1270,11 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
creditors_account = pi.credit_to
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 37500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -2500.0],
|
||||
["_Test Payable USD - _TC", -37500.0],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -1284,6 +1291,31 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
pi.reload()
|
||||
self.assertEqual(pi.outstanding_amount, 0)
|
||||
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
self.assertEqual(flt(total_debit_amount, 2), 2500)
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": creditors_account,
|
||||
"docstatus": 1,
|
||||
"reference_name": pi.name,
|
||||
"debit": 2500,
|
||||
"debit_in_account_currency": 0,
|
||||
},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
pi_2 = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
@@ -1308,10 +1340,12 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi_2.save()
|
||||
pi_2.submit()
|
||||
|
||||
pi_2.reload()
|
||||
self.assertEqual(pi_2.outstanding_amount, 0)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 36500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -1500.0],
|
||||
["_Test Payable USD - _TC", -36500.0],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -1342,12 +1376,39 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
self.assertEqual(flt(total_debit_amount, 2), 1500)
|
||||
jea_parent_2 = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"account": creditors_account,
|
||||
"docstatus": 1,
|
||||
"reference_name": pi_2.name,
|
||||
"debit": 1500,
|
||||
"debit_in_account_currency": 0,
|
||||
},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"),
|
||||
"Exchange Gain Or Loss",
|
||||
)
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2)
|
||||
|
||||
pi_2.reload()
|
||||
pi_2.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2)
|
||||
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
@@ -1356,6 +1417,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
@@ -1656,9 +1718,14 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
def test_gl_entries_for_standalone_debit_note(self):
|
||||
make_purchase_invoice(qty=5, rate=500, update_stock=True)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
|
||||
item_code = make_item(properties={"is_stock_item": 1})
|
||||
make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
|
||||
|
||||
returned_inv = make_purchase_invoice(
|
||||
item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
|
||||
)
|
||||
|
||||
# override the rate with valuation rate
|
||||
sle = frappe.get_all(
|
||||
@@ -1668,7 +1735,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
)[0]
|
||||
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
@@ -1716,23 +1783,150 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
0,
|
||||
)
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import (
|
||||
clear_dimension_defaults,
|
||||
create_accounting_dimension,
|
||||
disable_dimension,
|
||||
)
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
|
||||
order by posting_date asc, account asc""",
|
||||
(voucher_no, posting_date),
|
||||
as_dict=1,
|
||||
create_account(
|
||||
account_name="Offsetting",
|
||||
company="_Test Company",
|
||||
parent_account="Temporary Accounts - _TC",
|
||||
)
|
||||
|
||||
create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
|
||||
|
||||
branch1 = frappe.new_doc("Branch")
|
||||
branch1.branch = "Location 1"
|
||||
branch1.insert(ignore_if_duplicate=True)
|
||||
branch2 = frappe.new_doc("Branch")
|
||||
branch2.branch = "Location 2"
|
||||
branch2.insert(ignore_if_duplicate=True)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company",
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
rate=1000,
|
||||
price_list_rate=1000,
|
||||
qty=1,
|
||||
)
|
||||
pi.branch = branch1.branch
|
||||
pi.items[0].branch = branch2.branch
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch],
|
||||
["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch],
|
||||
["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch],
|
||||
["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self,
|
||||
pi.name,
|
||||
expected_gle,
|
||||
nowdate(),
|
||||
voucher_type="Purchase Invoice",
|
||||
additional_columns=["branch"],
|
||||
)
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
def test_repost_accounting_entries(self):
|
||||
pi = make_purchase_invoice(
|
||||
rate=1000,
|
||||
price_list_rate=1000,
|
||||
qty=1,
|
||||
)
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
def test_default_cost_center_for_purchase(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]:
|
||||
create_cost_center(cost_center_name=c_center)
|
||||
|
||||
item = create_item(
|
||||
"_Test Cost Center Item For Purchase",
|
||||
is_stock_item=1,
|
||||
buying_cost_center="_Test Cost Center Buying - _TC",
|
||||
selling_cost_center="_Test Cost Center Selling - _TC",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center=""
|
||||
)
|
||||
|
||||
pi.items[0].cost_center = ""
|
||||
pi.set_missing_values()
|
||||
pi.calculate_taxes_and_totals()
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
voucher_no,
|
||||
expected_gle,
|
||||
posting_date,
|
||||
voucher_type="Purchase Invoice",
|
||||
additional_columns=None,
|
||||
):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(gl.account, gl.debit, gl.credit, gl.posting_date)
|
||||
.where(
|
||||
(gl.voucher_type == voucher_type)
|
||||
& (gl.voucher_no == voucher_no)
|
||||
& (gl.posting_date >= posting_date)
|
||||
& (gl.is_cancelled == 0)
|
||||
)
|
||||
.orderby(gl.posting_date, gl.account, gl.creation)
|
||||
)
|
||||
|
||||
if additional_columns:
|
||||
for col in additional_columns:
|
||||
query = query.select(gl[col])
|
||||
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
if additional_columns:
|
||||
j = 4
|
||||
for col in additional_columns:
|
||||
doc.assertEqual(expected_gle[i][j], gle[col])
|
||||
j += 1
|
||||
|
||||
|
||||
def create_tax_witholding_category(category_name, company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"manufacturer_part_no",
|
||||
"accounting",
|
||||
"expense_account",
|
||||
"wip_composite_asset",
|
||||
"col_break5",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
@@ -467,6 +468,7 @@
|
||||
"label": "Accounting"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Expense Head",
|
||||
@@ -877,12 +879,18 @@
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply TDS"
|
||||
},
|
||||
{
|
||||
"fieldname": "wip_composite_asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 17:22:21.501152",
|
||||
"modified": "2023-10-03 21:01:01.824892",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@@ -892,4 +900,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
@@ -97,6 +98,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": ":Company",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<style>
|
||||
.print-format {
|
||||
padding: 4mm;
|
||||
font-size: 8.0pt !important;
|
||||
}
|
||||
.print-format td {
|
||||
vertical-align:middle !important;
|
||||
}
|
||||
.old {
|
||||
background-color: #FFB3C0;
|
||||
}
|
||||
.new {
|
||||
background-color: #B3FFCC;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<table class="table table-bordered table-condensed">
|
||||
<colgroup>
|
||||
{% for col in gl_columns%}
|
||||
<col style="width: 18mm;">
|
||||
{% endfor %}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in gl_columns%}
|
||||
<td>{{ col.label }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for gl in gl_data%}
|
||||
{% if gl["old"]%}
|
||||
<tr class="old">
|
||||
{% else %}
|
||||
<tr class="new">
|
||||
{% endif %}
|
||||
{% for col in gl_columns %}
|
||||
<td class="text-right">
|
||||
{{ gl[col.fieldname] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Repost Accounting Ledger", {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
|
||||
if (doc.company) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__('Show Preview'), () => {
|
||||
frm.call({
|
||||
method: 'generate_preview',
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __('Generating Preview'),
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
let content = r.message;
|
||||
let opts = {
|
||||
title: "Preview",
|
||||
subtitle: "preview",
|
||||
content: content,
|
||||
print_settings: {orientation: "landscape"},
|
||||
columns: [],
|
||||
data: [],
|
||||
}
|
||||
frappe.render_grid(opts);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"column_break_vpup",
|
||||
"delete_cancelled_entries",
|
||||
"section_break_metl",
|
||||
"vouchers",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Repost Accounting Ledger",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Vouchers",
|
||||
"options": "Repost Accounting Ledger Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vpup",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_metl",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delete_cancelled_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delete Cancelled Ledger Entries"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = set(
|
||||
["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
self.validate_for_closed_fiscal_year()
|
||||
self.validate_for_deferred_accounting()
|
||||
|
||||
def validate_for_deferred_accounting(self):
|
||||
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
||||
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
||||
validate_docs_for_deferred_accounting(sales_docs, purchase_docs)
|
||||
|
||||
def validate_for_closed_fiscal_year(self):
|
||||
if self.vouchers:
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
limit=1,
|
||||
)
|
||||
or None
|
||||
)
|
||||
if not latest_pcv:
|
||||
return
|
||||
|
||||
for vtype in self._allowed_types:
|
||||
if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
|
||||
latest_voucher = frappe.db.get_all(
|
||||
vtype,
|
||||
filters={"name": ["in", names]},
|
||||
pluck="posting_date",
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)[0]
|
||||
if latest_voucher and latest_pcv[0] >= latest_voucher:
|
||||
frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
|
||||
|
||||
def validate_vouchers(self):
|
||||
if self.vouchers:
|
||||
# Validate voucher types
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
if disallowed_types := voucher_types.difference(self._allowed_types):
|
||||
frappe.throw(
|
||||
_("{0} types are not allowed. Only {1} are.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
frappe.bold(comma_and(list(self._allowed_types))),
|
||||
)
|
||||
)
|
||||
|
||||
def get_existing_ledger_entries(self):
|
||||
vouchers = [x.voucher_no for x in self.vouchers]
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gles = (
|
||||
qb.from_(gl)
|
||||
.select(gl.star)
|
||||
.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.gles = frappe._dict({})
|
||||
|
||||
for gle in existing_gles:
|
||||
self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
|
||||
"existing", []
|
||||
).append(gle.update({"old": True}))
|
||||
|
||||
def generate_preview_data(self):
|
||||
self.gl_entries = []
|
||||
self.get_existing_ledger_entries()
|
||||
for x in self.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
old_entries = self.gles.get((x.voucher_type, x.voucher_no))
|
||||
if old_entries:
|
||||
self.gl_entries.extend(old_entries.existing)
|
||||
self.gl_entries.extend(gle_map)
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_preview(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
|
||||
|
||||
gl_columns = []
|
||||
gl_data = []
|
||||
|
||||
self.generate_preview_data()
|
||||
if self.gl_entries:
|
||||
filters = {"company": self.company, "include_dimensions": 1}
|
||||
for x in get_gl_columns(filters):
|
||||
if x["fieldname"] == "gl_entry":
|
||||
x["fieldname"] = "name"
|
||||
gl_columns.append(x)
|
||||
|
||||
gl_data = self.gl_entries
|
||||
rendered_page = frappe.render_template(
|
||||
"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
|
||||
{"gl_columns": gl_columns, "gl_data": gl_data},
|
||||
)
|
||||
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
else:
|
||||
start_repost(self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
repost_doc.validate_for_deferred_accounting()
|
||||
|
||||
for x in repost_doc.vouchers:
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
|
||||
if repost_doc.delete_cancelled_entries:
|
||||
frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
docs_with_deferred_expense = frappe.db.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||
frappe.throw(
|
||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,202 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def teadDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
preq = frappe.get_doc(
|
||||
make_payment_request(
|
||||
dt=si.doctype,
|
||||
dn=si.name,
|
||||
payment_request_type="Inward",
|
||||
party_type="Customer",
|
||||
party=si.customer,
|
||||
)
|
||||
)
|
||||
preq.save().submit()
|
||||
|
||||
# Test Validation Error
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append(
|
||||
"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
|
||||
) # this should throw validation error
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
ral.vouchers.pop()
|
||||
preq.cancel()
|
||||
preq.delete()
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# manually set an incorrect debit amount in DB
|
||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
|
||||
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Assert incorrect ledger balance
|
||||
self.assertNotEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
# Ledger should reflect correct amount post repost
|
||||
self.assertEqual(res[0], (si.name, 100, 100))
|
||||
|
||||
def test_02_deferred_accounting_valiations(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = True
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue
|
||||
si.items[0].service_start_date = nowdate()
|
||||
si.items[0].service_end_date = add_days(nowdate(), 90)
|
||||
si.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_04_pcv_validation(self):
|
||||
# Clear old GL entries so PCV can be submitted.
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
qb.from_(gl).delete().where(gl.company == self.company).run()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"company": self.company,
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
pcv.reload()
|
||||
pcv.cancel()
|
||||
pcv.delete()
|
||||
|
||||
def test_03_deletion_flag_and_preview_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-07-04 14:14:01.243848",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 14:15:51.165584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Items",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RepostAccountingLedgerItems(Document):
|
||||
pass
|
||||
@@ -99,7 +99,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-08 07:38:40.079038",
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
@@ -155,5 +155,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@@ -177,8 +177,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
|
||||
@@ -714,6 +714,7 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Items",
|
||||
"oldfieldname": "entries",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Invoice Item",
|
||||
|
||||
@@ -11,19 +11,19 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
validate_loyalty_points,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
@@ -176,6 +176,12 @@ class SalesInvoice(SellingController):
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
for d in self.get("items"):
|
||||
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
||||
@@ -399,6 +405,10 @@ class SalesInvoice(SellingController):
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
@@ -525,89 +535,22 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
needs_repost = 0
|
||||
|
||||
# Check if any field affecting accounting entry is altered
|
||||
doc_before_update = self.get_doc_before_save()
|
||||
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
|
||||
|
||||
# Check if opening entry check updated
|
||||
if doc_before_update.get("is_opening") != self.is_opening:
|
||||
needs_repost = 1
|
||||
|
||||
if not needs_repost:
|
||||
# Parent Level Accounts excluding party account
|
||||
for field in (
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
):
|
||||
if doc_before_update.get(field) != self.get(field):
|
||||
needs_repost = 1
|
||||
break
|
||||
|
||||
# Check for parent accounting dimensions
|
||||
for dimension in accounting_dimensions:
|
||||
if doc_before_update.get(dimension) != self.get(dimension):
|
||||
needs_repost = 1
|
||||
break
|
||||
|
||||
# Check for child tables
|
||||
if self.check_if_child_table_updated(
|
||||
"items",
|
||||
doc_before_update,
|
||||
("income_account", "expense_account", "discount_account"),
|
||||
accounting_dimensions,
|
||||
):
|
||||
needs_repost = 1
|
||||
|
||||
if self.check_if_child_table_updated(
|
||||
"taxes", doc_before_update, ("account_head",), accounting_dimensions
|
||||
):
|
||||
needs_repost = 1
|
||||
|
||||
self.validate_accounts()
|
||||
|
||||
# validate if deferred revenue is enabled for any item
|
||||
# Don't allow to update the invoice if deferred revenue is enabled
|
||||
for item in self.get("items"):
|
||||
if item.enable_deferred_revenue:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
|
||||
).format(item.item_code)
|
||||
)
|
||||
|
||||
self.db_set("repost_required", needs_repost)
|
||||
|
||||
def check_if_child_table_updated(
|
||||
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
|
||||
):
|
||||
# Check if any field affecting accounting entry is altered
|
||||
for index, item in enumerate(self.get(child_table)):
|
||||
for field in fields_to_check:
|
||||
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
|
||||
return True
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
self.docstatus = 2
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.docstatus = 1
|
||||
self.make_gl_entries()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
@@ -1046,7 +989,10 @@ class SalesInvoice(SellingController):
|
||||
merge_entries=False,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
@@ -1071,7 +1017,6 @@ class SalesInvoice(SellingController):
|
||||
self.make_customer_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
@@ -1665,15 +1610,13 @@ class SalesInvoice(SellingController):
|
||||
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def get_returned_amount(self):
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
doc = frappe.qb.DocType(self.doctype)
|
||||
returned_amount = (
|
||||
frappe.qb.from_(doc)
|
||||
.select(Sum(doc.grand_total))
|
||||
.where(
|
||||
(doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
|
||||
)
|
||||
.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
|
||||
).run()
|
||||
|
||||
return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
|
||||
|
||||
@@ -15,9 +15,11 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"Delivery Note": ["items", "delivery_note"],
|
||||
"Timesheet": ["timesheets", "time_sheet"],
|
||||
},
|
||||
"internal_and_external_links": {
|
||||
"Delivery Note": ["items", "delivery_note"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Payment"),
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -38,13 +38,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class TestSalesInvoice(unittest.TestCase):
|
||||
class TestSalesInvoice(FrappeTestCase):
|
||||
def setUp(self):
|
||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||
|
||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||
create_internal_parties()
|
||||
setup_accounts()
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def make(self):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
@@ -172,6 +176,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -1293,6 +1298,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
dn.submit()
|
||||
return dn
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_sales_invoice_with_advance(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -1801,6 +1807,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
||||
"""Test impact of advance PE submission/cancellation on SI and SO."""
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
@@ -1820,10 +1830,25 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"paid_to": "_Test Cash - _TC",
|
||||
}
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": "Sales Order",
|
||||
"reference_name": sales_order.name,
|
||||
"total_amount": sales_order.grand_total,
|
||||
"outstanding_amount": sales_order.grand_total,
|
||||
"allocated_amount": 300,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
sales_order.reload()
|
||||
self.assertEqual(sales_order.advance_paid, 300)
|
||||
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.items[0].sales_order = sales_order.name
|
||||
si.items[0].so_detail = sales_order.get("items")[0].name
|
||||
si.is_pos = 0
|
||||
si.append(
|
||||
"advances",
|
||||
@@ -1831,6 +1856,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 300,
|
||||
"allocated_amount": 300,
|
||||
"remarks": pe.remarks,
|
||||
@@ -1839,7 +1865,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
si.load_from_db()
|
||||
si.reload()
|
||||
pe.reload()
|
||||
sales_order.reload()
|
||||
|
||||
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(sales_order.advance_paid, 0.0)
|
||||
|
||||
# check outstanding after advance allocation
|
||||
self.assertEqual(
|
||||
@@ -1847,11 +1879,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
|
||||
)
|
||||
|
||||
# added to avoid Document has been modified exception
|
||||
pe = frappe.get_doc("Payment Entry", pe.name)
|
||||
pe.cancel()
|
||||
si.reload()
|
||||
|
||||
si.load_from_db()
|
||||
# check outstanding after advance cancellation
|
||||
self.assertEqual(
|
||||
flt(si.outstanding_amount),
|
||||
@@ -2322,7 +2352,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||
item.no_of_months = 12
|
||||
item.save()
|
||||
|
||||
@@ -2467,12 +2497,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"stock_received_but_not_billed",
|
||||
"Stock Received But Not Billed - _TC1",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Company 1",
|
||||
"expenses_included_in_valuation",
|
||||
"Expenses Included In Valuation - _TC1",
|
||||
)
|
||||
|
||||
# begin test
|
||||
si = create_sales_invoice(
|
||||
@@ -2510,7 +2534,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
# tear down
|
||||
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
|
||||
|
||||
def test_sle_for_target_warehouse(self):
|
||||
se = make_stock_entry(
|
||||
@@ -2522,6 +2546,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.customer = "_Test Internal Customer 3"
|
||||
si.update_stock = 1
|
||||
si.set_warehouse = "Finished Goods - _TC"
|
||||
si.set_target_warehouse = "Stores - _TC"
|
||||
@@ -2750,6 +2775,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
tds_payable_account = create_account(
|
||||
account_name="TDS Payable",
|
||||
account_type="Tax",
|
||||
parent_account="Duties and Taxes - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.additional_discount_account = additional_discount_account
|
||||
@@ -3048,8 +3080,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.commission_rate = commission_rate
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.save()
|
||||
@@ -3058,8 +3090,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
|
||||
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3088,6 +3118,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"book_deferred_entries_via_journal_entry": 1,
|
||||
"submit_journal_entries": 1,
|
||||
},
|
||||
)
|
||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -3095,14 +3132,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_single("Accounts Settings")
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
||||
acc_settings.submit_journal_entries = 1
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
@@ -3165,13 +3197,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
acc_settings = frappe.get_single("Accounts Settings")
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||
acc_settings.submit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
|
||||
|
||||
def test_standalone_serial_no_return(self):
|
||||
si = create_sales_invoice(
|
||||
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
||||
@@ -3213,17 +3238,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
account.disabled = 0
|
||||
account.save()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
|
||||
|
||||
jv.accounts[0].exchange_rate = 70
|
||||
@@ -3256,17 +3274,28 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Receivable USD - _TC", 7500.0, 500],
|
||||
["Exchange Gain/Loss - _TC", 500.0, 0.0],
|
||||
["Sales - _TC", 0.0, 7500.0],
|
||||
["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()],
|
||||
["Sales - _TC", 0.0, 7500.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, nowdate())
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1},
|
||||
pluck="parent",
|
||||
)
|
||||
journals = [x for x in journals if x != jv.name]
|
||||
self.assertEqual(len(journals), 1)
|
||||
je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type")
|
||||
self.assertEqual(je_type, "Exchange Gain Or Loss")
|
||||
ledger_outstanding = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"against_voucher_no": si.name, "delinked": 0},
|
||||
fields=["sum(amount), sum(amount_in_account_currency)"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_sales_invoice_return(self):
|
||||
@@ -3316,6 +3345,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
|
||||
def test_sales_return_negative_rate(self):
|
||||
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
@@ -3544,6 +3574,20 @@ def create_internal_parties():
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer 3",
|
||||
represents_company="_Test Company",
|
||||
allowed_to_interact_with="_Test Company",
|
||||
)
|
||||
|
||||
account = create_account(
|
||||
account_name="Unrealized Profit",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier",
|
||||
represents_company="Wind Power LLC",
|
||||
|
||||
@@ -18,6 +18,14 @@ frappe.ui.form.on('Subscription', {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('sales_tax_template', function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -90,10 +91,14 @@ def create_parties():
|
||||
customer.insert()
|
||||
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
class TestSubscription(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
create_parties()
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
@@ -694,3 +699,23 @@ class TestSubscription(unittest.TestCase):
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
def test_plan_rate_for_midmonth_start_date(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Supplier"
|
||||
subscription.party = "_Test Supplier"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.follow_calendar_months = 1
|
||||
subscription.generate_new_invoices_past_due_date = 1
|
||||
subscription.start_date = "2023-04-08"
|
||||
subscription.end_date = "2024-02-27"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
||||
subscription.save()
|
||||
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
pi = frappe.get_doc("Purchase Invoice", subscription.invoices[0].invoice)
|
||||
self.assertEqual(pi.total, 55333.33)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
@@ -57,18 +57,17 @@ def get_plan_rate(
|
||||
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||
|
||||
if prorate:
|
||||
prorate_factor = flt(
|
||||
date_diff(start_date, get_first_day(start_date))
|
||||
/ date_diff(get_last_day(start_date), get_first_day(start_date)),
|
||||
1,
|
||||
)
|
||||
|
||||
prorate_factor += flt(
|
||||
date_diff(get_last_day(end_date), end_date)
|
||||
/ date_diff(get_last_day(end_date), get_first_day(end_date)),
|
||||
1,
|
||||
)
|
||||
|
||||
cost -= plan.cost * prorate_factor
|
||||
|
||||
cost -= plan.cost * get_prorate_factor(start_date, end_date)
|
||||
return cost
|
||||
|
||||
|
||||
def get_prorate_factor(start_date, end_date):
|
||||
total_days_to_skip = date_diff(start_date, get_first_day(start_date))
|
||||
total_days_in_month = int(get_last_day(start_date).strftime("%d"))
|
||||
prorate_factor = flt(total_days_to_skip / total_days_in_month)
|
||||
|
||||
total_days_to_skip = date_diff(get_last_day(end_date), end_date)
|
||||
total_days_in_month = int(get_last_day(end_date).strftime("%d"))
|
||||
prorate_factor += flt(total_days_to_skip / total_days_in_month)
|
||||
|
||||
return prorate_factor
|
||||
|
||||
@@ -262,14 +262,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
|
||||
limit_consumed = get_limit_consumed(ldc, parties)
|
||||
if is_valid_certificate(ldc, posting_date, limit_consumed):
|
||||
tax_amount = get_lower_deduction_amount(
|
||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
tax_amount = net_total * tax_details.rate / 100
|
||||
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
|
||||
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
@@ -416,7 +422,7 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
return sum(entries)
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
@@ -496,15 +502,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
net_total += inv.tax_withholding_net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from,
|
||||
ldc.valid_upto,
|
||||
inv.get("posting_date") or inv.get("transaction_date"),
|
||||
tax_deducted,
|
||||
inv.tax_withholding_net_total,
|
||||
ldc.certificate_limit,
|
||||
):
|
||||
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
@@ -582,8 +583,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
|
||||
return inv.grand_total - tcs_tax_row_amount
|
||||
|
||||
|
||||
def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
def get_limit_consumed(ldc, parties):
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{
|
||||
@@ -597,37 +597,29 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
"sum(tax_withholding_net_total)",
|
||||
)
|
||||
|
||||
if is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit
|
||||
):
|
||||
tds_amount = get_ltds_amount(
|
||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||
)
|
||||
|
||||
return tds_amount
|
||||
return limit_consumed
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
def get_lower_deduction_amount(
|
||||
current_amount, limit_consumed, certificate_limit, rate, tax_details
|
||||
):
|
||||
if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
ltds_amount = certificate_limit - flt(limit_consumed)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
|
||||
|
||||
def is_valid_certificate(
|
||||
valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
|
||||
):
|
||||
valid = False
|
||||
def is_valid_certificate(ldc, posting_date, limit_consumed):
|
||||
available_amount = flt(ldc.certificate_limit) - flt(limit_consumed)
|
||||
if (
|
||||
getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)
|
||||
) and available_amount > 0:
|
||||
return True
|
||||
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
return False
|
||||
|
||||
|
||||
def normal_round(number):
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import today
|
||||
|
||||
@@ -18,6 +19,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
# create relevant supplier, etc
|
||||
create_records()
|
||||
create_tax_withholding_category_records()
|
||||
make_pan_no_field()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
@@ -456,6 +458,40 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pe2.cancel()
|
||||
pe3.cancel()
|
||||
|
||||
def test_lower_deduction_certificate_application(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=2,
|
||||
limit=50000,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 700)
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi2.submit()
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 2300)
|
||||
|
||||
pi3 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
|
||||
pi3.submit()
|
||||
self.assertEqual(pi3.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
@@ -615,6 +651,7 @@ def create_records():
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
"Test TDS Supplier8",
|
||||
"Test LDC Supplier",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
@@ -811,3 +848,39 @@ def create_tax_withholding_category(
|
||||
"accounts": [{"company": "_Test Company", "account": account}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Lower Deduction Certificate",
|
||||
"company": "_Test Company",
|
||||
"supplier": supplier,
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_pan_no_field():
|
||||
pan_field = {
|
||||
"Supplier": [
|
||||
{
|
||||
"fieldname": "pan",
|
||||
"label": "PAN",
|
||||
"fieldtype": "Data",
|
||||
"translatable": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
create_custom_fields(pan_field, update=1)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-08-22 10:28:10.196712",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"allocated_amount",
|
||||
"account_currency",
|
||||
"unlinked"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unlinked",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Unlinked",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-05 09:33:28.620149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payment Entries",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class UnreconcilePaymentEntries(Document):
|
||||
pass
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return si
|
||||
|
||||
def create_payment_entry(self):
|
||||
pe = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.cash,
|
||||
paid_amount=200,
|
||||
save=True,
|
||||
)
|
||||
return pe
|
||||
|
||||
def test_01_unreconcile_invoice(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||
)
|
||||
# Allocation payment against both invoices
|
||||
pe.save().submit()
|
||||
|
||||
# Assert outstanding
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 0)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(pe.unallocated_amount, 0)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 100)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
||||
"""
|
||||
Scenario: 2 payments, both split against 2 different invoices
|
||||
Unreconcile only one payment from one invoice
|
||||
"""
|
||||
si1 = self.create_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
pe1 = self.create_payment_entry()
|
||||
pe1.paid_amount = 100
|
||||
# Allocate payment against both invoices
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = self.create_payment_entry()
|
||||
pe2.paid_amount = 100
|
||||
# Allocate payment against both invoices
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 0.0)
|
||||
self.assertEqual(si2.outstanding_amount, 0.0)
|
||||
self.assertEqual(pe1.unallocated_amount, 0.0)
|
||||
self.assertEqual(pe2.unallocated_amount, 0.0)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe2
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 50)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe1.references), 2)
|
||||
self.assertEqual(len(pe2.references), 1)
|
||||
self.assertEqual(pe1.unallocated_amount, 0)
|
||||
self.assertEqual(pe2.unallocated_amount, 50)
|
||||
|
||||
def test_03_unreconciliation_on_multi_currency_invoice(self):
|
||||
self.create_customer("_Test MC Customer USD", "USD")
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.currency = "USD"
|
||||
si1.debit_to = self.debtors_usd
|
||||
si1.conversion_rate = 80
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.currency = "USD"
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.conversion_rate = 80
|
||||
si2.save().submit()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_from = self.debtors_usd
|
||||
pe.paid_from_account_currency = "USD"
|
||||
pe.source_exchange_rate = 75
|
||||
pe.received_amount = 75 * 200
|
||||
pe.save()
|
||||
# Allocate payment against both invoices
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 100)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
# Exc gain/loss JE should've been cancelled as well
|
||||
self.assertEqual(
|
||||
frappe.db.count(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
def test_04_unreconciliation_on_multi_currency_invoice(self):
|
||||
"""
|
||||
2 payments split against 2 foreign currency invoices
|
||||
"""
|
||||
self.create_customer("_Test MC Customer USD", "USD")
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.currency = "USD"
|
||||
si1.debit_to = self.debtors_usd
|
||||
si1.conversion_rate = 80
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.currency = "USD"
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.conversion_rate = 80
|
||||
si2.save().submit()
|
||||
|
||||
pe1 = self.create_payment_entry()
|
||||
pe1.paid_from = self.debtors_usd
|
||||
pe1.paid_from_account_currency = "USD"
|
||||
pe1.source_exchange_rate = 75
|
||||
pe1.received_amount = 75 * 100
|
||||
pe1.save()
|
||||
# Allocate payment against both invoices
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = self.create_payment_entry()
|
||||
pe2.paid_from = self.debtors_usd
|
||||
pe2.paid_from_account_currency = "USD"
|
||||
pe2.source_exchange_rate = 75
|
||||
pe2.received_amount = 75 * 100
|
||||
pe2.save()
|
||||
# Allocate payment against both invoices
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.save().submit()
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe2
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 50)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe1.references), 2)
|
||||
self.assertEqual(len(pe2.references), 1)
|
||||
self.assertEqual(pe1.unallocated_amount, 0)
|
||||
self.assertEqual(pe2.unallocated_amount, 50)
|
||||
|
||||
# Exc gain/loss JE from PE1 should be available
|
||||
self.assertEqual(
|
||||
frappe.db.count(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||
),
|
||||
1,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Unreconcile Payments", {
|
||||
refresh(frm) {
|
||||
frm.set_query("voucher_type", function() {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Payment Entry", "Journal Entry"]]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
frm.set_query("voucher_no", function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
get_allocations: function(frm) {
|
||||
frm.clear_table("allocations");
|
||||
frappe.call({
|
||||
method: "get_allocations_from_payment",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
r.message.forEach(x => {
|
||||
frm.add_child("allocations", x)
|
||||
})
|
||||
frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:UNREC-{#####}",
|
||||
"creation": "2023-08-22 10:26:34.421423",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"get_allocations",
|
||||
"allocations",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Unreconcile Payments",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_allocations",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Allocations"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allocations",
|
||||
"options": "Unreconcile Payment Entries"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payments",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
|
||||
|
||||
class UnreconcilePayments(Document):
|
||||
def validate(self):
|
||||
self.supported_types = ["Payment Entry", "Journal Entry"]
|
||||
if not self.voucher_type in self.supported_types:
|
||||
frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_allocations_from_payment(self):
|
||||
allocated_references = []
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
allocated_references = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(
|
||||
(ple.docstatus == 1)
|
||||
& (ple.voucher_type == self.voucher_type)
|
||||
& (ple.voucher_no == self.voucher_no)
|
||||
& (ple.voucher_no != ple.against_voucher_no)
|
||||
)
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return allocated_references
|
||||
|
||||
def add_references(self):
|
||||
allocations = self.get_allocations_from_payment()
|
||||
|
||||
for alloc in allocations:
|
||||
self.append("allocations", alloc)
|
||||
|
||||
def on_submit(self):
|
||||
# todo: more granular unreconciliation
|
||||
for alloc in self.allocations:
|
||||
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
|
||||
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
|
||||
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def doc_has_references(doctype: str = None, docname: str = None):
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
|
||||
)
|
||||
else:
|
||||
return frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_payments_for_doc(
|
||||
company: str = None, doctype: str = None, docname: str = None
|
||||
) -> list:
|
||||
if company and doctype and docname:
|
||||
_dt = doctype
|
||||
_dn = docname
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
if _dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
criteria = [
|
||||
(ple.company == company),
|
||||
(ple.delinked == 0),
|
||||
(ple.against_voucher_no == _dn),
|
||||
(ple.amount < 0),
|
||||
]
|
||||
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
criteria = [
|
||||
(ple.company == company),
|
||||
(ple.delinked == 0),
|
||||
(ple.voucher_no == _dn),
|
||||
(ple.against_voucher_no != _dn),
|
||||
]
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
return []
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payments")
|
||||
unrecon.company = row.get("company")
|
||||
unrecon.voucher_type = row.get("voucher_type")
|
||||
unrecon.voucher_no = row.get("voucher_no")
|
||||
unrecon.add_references()
|
||||
|
||||
# remove unselected references
|
||||
unrecon.allocations = [
|
||||
x
|
||||
for x in unrecon.allocations
|
||||
if x.reference_doctype == row.get("against_voucher_type")
|
||||
and x.reference_name == row.get("against_voucher_no")
|
||||
]
|
||||
unrecon.save().submit()
|
||||
@@ -28,6 +28,7 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if not cancel:
|
||||
make_acc_dimensions_offsetting_entry(gl_map)
|
||||
validate_accounting_period(gl_map)
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
@@ -40,7 +41,7 @@ def make_gl_entries(
|
||||
from_repost=from_repost,
|
||||
)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
# Post GL Map process there may no be any GL Entries
|
||||
elif gl_map:
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -51,6 +52,63 @@ def make_gl_entries(
|
||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
|
||||
|
||||
def make_acc_dimensions_offsetting_entry(gl_map):
|
||||
accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(
|
||||
gl_map, gl_map[0].company
|
||||
)
|
||||
no_of_dimensions = len(accounting_dimensions_to_offset)
|
||||
if no_of_dimensions == 0:
|
||||
return
|
||||
|
||||
offsetting_entries = []
|
||||
|
||||
for gle in gl_map:
|
||||
for dimension in accounting_dimensions_to_offset:
|
||||
offsetting_entry = gle.copy()
|
||||
debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0
|
||||
credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0
|
||||
offsetting_entry.update(
|
||||
{
|
||||
"account": dimension.offsetting_account,
|
||||
"debit": debit,
|
||||
"credit": credit,
|
||||
"debit_in_account_currency": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name),
|
||||
"against_voucher": None,
|
||||
}
|
||||
)
|
||||
offsetting_entry["against_voucher_type"] = None
|
||||
offsetting_entries.append(offsetting_entry)
|
||||
|
||||
gl_map += offsetting_entries
|
||||
|
||||
|
||||
def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
acc_dimension = frappe.qb.DocType("Accounting Dimension")
|
||||
dimension_detail = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
|
||||
acc_dimensions = (
|
||||
frappe.qb.from_(acc_dimension)
|
||||
.inner_join(dimension_detail)
|
||||
.on(acc_dimension.name == dimension_detail.parent)
|
||||
.select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account)
|
||||
.where(
|
||||
(acc_dimension.disabled == 0)
|
||||
& (dimension_detail.company == company)
|
||||
& (dimension_detail.automatically_post_balancing_accounting_entry == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
accounting_dimensions_to_offset = []
|
||||
for acc_dimension in acc_dimensions:
|
||||
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
|
||||
if len(values) > 1:
|
||||
accounting_dimensions_to_offset.append(acc_dimension)
|
||||
|
||||
return accounting_dimensions_to_offset
|
||||
|
||||
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
|
||||
@@ -5,16 +5,11 @@
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
get_address_display,
|
||||
get_company_address,
|
||||
get_default_address,
|
||||
)
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe import _, msgprint, qb, scrub
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -133,6 +128,7 @@ def _get_party_details(
|
||||
party_address,
|
||||
company_address,
|
||||
shipping_address,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
set_other_values(party_details, party, party_type)
|
||||
@@ -193,6 +189,8 @@ def set_address_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
*,
|
||||
ignore_permissions=False
|
||||
):
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
@@ -205,13 +203,17 @@ def set_address_details(
|
||||
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
||||
)
|
||||
# address display
|
||||
party_details.address_display = get_address_display(party_details[billing_address_field])
|
||||
party_details.address_display = render_address(
|
||||
party_details[billing_address_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
# shipping address
|
||||
if party_type in ["Customer", "Lead"]:
|
||||
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
||||
party_type, party.name
|
||||
)
|
||||
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
|
||||
party_details.shipping_address = render_address(
|
||||
party_details["shipping_address_name"], check_permissions=not ignore_permissions
|
||||
)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
||||
@@ -229,7 +231,7 @@ def set_address_details(
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
shipping_address_display=get_address_display(shipping_address),
|
||||
shipping_address_display=render_address(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||
)
|
||||
|
||||
@@ -238,7 +240,8 @@ def set_address_details(
|
||||
party_details.update(
|
||||
billing_address=party_details.company_address,
|
||||
billing_address_display=(
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
party_details.company_address_display
|
||||
or render_address(party_details.company_address, check_permissions=False)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||
)
|
||||
@@ -290,7 +293,34 @@ def set_contact_details(party_details, party, party_type):
|
||||
}
|
||||
)
|
||||
else:
|
||||
party_details.update(get_contact_details(party_details.contact_person))
|
||||
fields = [
|
||||
"name as contact_person",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email_id as contact_email",
|
||||
"mobile_no as contact_mobile",
|
||||
"phone as contact_phone",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
]
|
||||
|
||||
contact_details = frappe.db.get_value(
|
||||
"Contact", party_details.contact_person, fields, as_dict=True
|
||||
)
|
||||
|
||||
contact_details.contact_display = " ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
contact_details.get("salutation"),
|
||||
contact_details.get("first_name"),
|
||||
contact_details.get("last_name"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
@@ -429,11 +459,19 @@ def get_party_account_currency(party_type, party, company):
|
||||
|
||||
def get_party_gle_currency(party_type, party, company):
|
||||
def generator():
|
||||
existing_gle_currency = frappe.db.sql(
|
||||
"""select account_currency from `tabGL Entry`
|
||||
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
|
||||
limit 1""",
|
||||
{"company": company, "party_type": party_type, "party": party},
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gle_currency = (
|
||||
qb.from_(gl)
|
||||
.select(gl.account_currency)
|
||||
.where(
|
||||
(gl.docstatus == 1)
|
||||
& (gl.company == company)
|
||||
& (gl.party_type == party_type)
|
||||
& (gl.party == party)
|
||||
& (gl.is_cancelled == 0)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
return existing_gle_currency[0][0] if existing_gle_currency else None
|
||||
@@ -884,35 +922,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.party)
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.party, Abs(Sum(ple.amount).as_("amount")))
|
||||
.where(
|
||||
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
|
||||
(ple.party_type.isin(party_type))
|
||||
& (ple.amount < 0)
|
||||
& (ple.against_voucher_no == ple.voucher_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
.groupby(gle.party)
|
||||
.groupby(ple.party)
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
query = query.select(Sum(gle.credit).as_("amount"))
|
||||
else:
|
||||
query = query.select(Sum(gle.debit).as_("amount"))
|
||||
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
|
||||
query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
|
||||
else:
|
||||
query = query.where(gle.posting_date <= posting_date)
|
||||
query = query.where(ple.posting_date <= posting_date)
|
||||
|
||||
if company:
|
||||
query = query.where(gle.company == company)
|
||||
query = query.where(ple.company == company)
|
||||
|
||||
if party:
|
||||
query = query.where(gle.party == party)
|
||||
query = query.where(ple.party == party)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
data = query.run()
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
@@ -958,3 +995,13 @@ def add_party_account(party_type, party, company, account):
|
||||
doc.append("accounts", accounts)
|
||||
|
||||
doc.save()
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -37,24 +37,6 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier",
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
on_change: () => {
|
||||
var supplier = frappe.query_report.get_filter_value('supplier');
|
||||
if (supplier) {
|
||||
frappe.db.get_value('Supplier', supplier, "tax_id", function(value) {
|
||||
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
|
||||
});
|
||||
} else {
|
||||
frappe.query_report.set_filter_value('tax_id', "");
|
||||
}
|
||||
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account",
|
||||
"label": __("Payable Account"),
|
||||
@@ -112,11 +94,35 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"fieldtype": "Link",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_group",
|
||||
"label": __("Supplier Group"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group"
|
||||
"options": "Supplier Group",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "group_by_party",
|
||||
@@ -133,17 +139,17 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"label": __("Tax Id"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_future_payments",
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
@@ -164,3 +170,15 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Payable', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
|
||||
self.create_usd_payable_account()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_accounts_payable_for_foreign_currency_supplier(self):
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.currency = "USD"
|
||||
pi.conversion_rate = 80
|
||||
pi.credit_to = self.creditors_usd
|
||||
pi = pi.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Supplier",
|
||||
"party": [self.supplier],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
self.assertEqual(data[1][0].get("outstanding"), 300)
|
||||
self.assertEqual(data[1][0].get("currency"), "USD")
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||
do_not_save=1,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
qty=1,
|
||||
)
|
||||
|
||||
pi = pi.save()
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
@@ -72,10 +72,27 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"supplier",
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier"
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname":"payment_terms_template",
|
||||
@@ -105,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.utils");
|
||||
|
||||
frappe.query_reports["Accounts Receivable"] = {
|
||||
"filters": [
|
||||
{
|
||||
@@ -38,34 +40,28 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"label": __("Customer"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
on_change: () => {
|
||||
var customer = frappe.query_report.get_filter_value('customer');
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
if (customer) {
|
||||
frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "payment_terms"], function(value) {
|
||||
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
|
||||
frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
|
||||
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
|
||||
});
|
||||
|
||||
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
|
||||
["credit_limit"], function(value) {
|
||||
if (value) {
|
||||
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
|
||||
}
|
||||
}, "Customer");
|
||||
} else {
|
||||
frappe.query_report.set_filter_value('tax_id', "");
|
||||
frappe.query_report.set_filter_value('customer_name', "");
|
||||
frappe.query_report.set_filter_value('credit_limit', "");
|
||||
frappe.query_report.set_filter_value('payment_terms', "");
|
||||
}
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account",
|
||||
"label": __("Receivable Account"),
|
||||
@@ -172,35 +168,17 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"label": __("Show Sales Person"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"label": __("Tax Id"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_name",
|
||||
"label": __("Customer Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"label": __("Payment Tems"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_limit",
|
||||
"label": __("Credit Limit"),
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
@@ -221,3 +199,16 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Receivable', 9);
|
||||
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,12 @@ class ReceivablePayableReport(object):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ingore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if not key in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
@@ -183,7 +188,10 @@ class ReceivablePayableReport(object):
|
||||
):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
if self.filters.get("ingore_accounts"):
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
@@ -192,13 +200,19 @@ class ReceivablePayableReport(object):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
if self.filters.get("ingore_accounts"):
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
if self.filters.get("ingore_accounts"):
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
else:
|
||||
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
@@ -211,9 +225,8 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
@@ -426,22 +439,20 @@ class ReceivablePayableReport(object):
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
row.currency = row.account_currency
|
||||
break
|
||||
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
||||
def allocate_outstanding_based_on_payment_terms(self, row):
|
||||
self.get_payment_terms(row)
|
||||
for term in row.payment_terms:
|
||||
|
||||
# update "paid" and "oustanding" for this term
|
||||
# update "paid" and "outstanding" for this term
|
||||
if not term.paid:
|
||||
self.allocate_closing_to_term(row, term, "paid")
|
||||
|
||||
# update "credit_note" and "oustanding" for this term
|
||||
# update "credit_note" and "outstanding" for this term
|
||||
if term.outstanding:
|
||||
self.allocate_closing_to_term(row, term, "credit_note")
|
||||
|
||||
@@ -453,7 +464,8 @@ class ReceivablePayableReport(object):
|
||||
"""
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
|
||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{0}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
@@ -469,6 +481,14 @@ class ReceivablePayableReport(object):
|
||||
original_row = frappe._dict(row)
|
||||
row.payment_terms = []
|
||||
|
||||
# Cr Note's don't have Payment Terms
|
||||
if not payment_terms_details:
|
||||
return
|
||||
|
||||
# Advance allocated during invoicing is not considered in payment terms
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
# If no or single payment terms, no need to split the row
|
||||
if len(payment_terms_details) <= 1:
|
||||
return
|
||||
@@ -483,7 +503,7 @@ class ReceivablePayableReport(object):
|
||||
) and d.currency == d.party_account_currency:
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
|
||||
invoiced = d.base_payment_amount
|
||||
|
||||
row.payment_terms.append(
|
||||
term.update(
|
||||
@@ -761,16 +781,14 @@ class ReceivablePayableReport(object):
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
self.or_filters = []
|
||||
|
||||
for party_type in self.party_type:
|
||||
party_type_field = scrub(party_type)
|
||||
self.or_filters.append(self.ple.party_type == party_type)
|
||||
self.add_common_filters()
|
||||
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
if party_type_field == "customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
elif self.account_type == "Payable":
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
@@ -786,15 +804,18 @@ class ReceivablePayableReport(object):
|
||||
]
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
def add_common_filters(self, party_type_field):
|
||||
def add_common_filters(self):
|
||||
if self.filters.company:
|
||||
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
||||
|
||||
if self.filters.finance_book:
|
||||
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
||||
|
||||
if self.filters.get(party_type_field):
|
||||
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
|
||||
if self.filters.get("party_type"):
|
||||
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
|
||||
|
||||
if self.filters.get("party"):
|
||||
self.qb_selection_filter.append(self.ple.party.isin(self.filters.party))
|
||||
|
||||
if self.filters.party_account:
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
@@ -956,6 +977,20 @@ class ReceivablePayableReport(object):
|
||||
fieldtype="Link",
|
||||
options="Contact",
|
||||
)
|
||||
if self.filters.party_type == "Customer":
|
||||
self.add_column(
|
||||
_("Customer Name"),
|
||||
fieldname="customer_name",
|
||||
fieldtype="Link",
|
||||
options="Customer",
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
_("Supplier Name"),
|
||||
fieldname="supplier_name",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
@@ -1086,7 +1121,10 @@ class ReceivablePayableReport(object):
|
||||
.where(
|
||||
(je.company == self.filters.company)
|
||||
& (je.posting_date.lte(self.filters.report_date))
|
||||
& (je.voucher_type == "Exchange Rate Revaluation")
|
||||
& (
|
||||
(je.voucher_type == "Exchange Rate Revaluation")
|
||||
| (je.voucher_type == "Exchange Gain Or Loss")
|
||||
)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -8,70 +9,99 @@ from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAccountsReceivable(FrappeTestCase):
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
|
||||
|
||||
self.create_usd_account()
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_usd_receivable_account()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_usd_account(self):
|
||||
name = "Debtors USD"
|
||||
exists = frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
if exists:
|
||||
self.debtors_usd = exists[0].name
|
||||
else:
|
||||
debtors = frappe.get_doc(
|
||||
"Account",
|
||||
frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
|
||||
)[0].name,
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
debtors_usd = frappe.new_doc("Account")
|
||||
debtors_usd.company = debtors.company
|
||||
debtors_usd.account_name = "Debtors USD"
|
||||
debtors_usd.account_currency = "USD"
|
||||
debtors_usd.parent_account = debtors.parent_account
|
||||
debtors_usd.account_type = debtors.account_type
|
||||
self.debtors_usd = debtors_usd.save().name
|
||||
def create_payment_entry(self, docname):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
def create_credit_note(self, docname):
|
||||
credit_note = create_sales_invoice(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item=self.item,
|
||||
qty=-1,
|
||||
debit_to=self.debit_to,
|
||||
cost_center=self.cost_center,
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
name = make_sales_invoice().name
|
||||
si = self.create_sales_invoice()
|
||||
name = si.name
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
make_payment(name)
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
|
||||
@@ -84,10 +114,10 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
make_credit_note(name)
|
||||
self.create_credit_note(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
@@ -108,21 +138,20 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
"""
|
||||
|
||||
so = make_sales_order(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
warehouse="Finished Goods - _TC2",
|
||||
currency="EUR",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name)
|
||||
pe = pe.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 0,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
@@ -147,34 +176,32 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_exchange_revaluation_for_party(self):
|
||||
"""
|
||||
Exchange Revaluation for party on Receivable/Payable shoule be included
|
||||
Exchange Revaluation for party on Receivable/Payable should be included
|
||||
"""
|
||||
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
# Using Exchange Gain/Loss account for unrealized as well.
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
company_doc = frappe.get_doc("Company", self.company)
|
||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||
company_doc.save()
|
||||
|
||||
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 0.90
|
||||
si.conversion_rate = 80
|
||||
si.debit_to = self.debtors_usd
|
||||
si = si.save().submit()
|
||||
|
||||
# Exchange Revaluation
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
err.accounts[0].new_exchange_rate = 0.95
|
||||
err.accounts[0].new_exchange_rate = 85
|
||||
row = err.accounts[0]
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
@@ -189,7 +216,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
je = je.submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
@@ -198,7 +225,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_for_err = [0, -5, 0, 5]
|
||||
expected_data_for_err = [0, -500, 0, 500]
|
||||
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
|
||||
self.assertEqual(
|
||||
expected_data_for_err,
|
||||
@@ -214,46 +241,43 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
si1 = self.create_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
cr_note = self.create_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.company = self.company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"party": self.customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"party": self.customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
@@ -261,7 +285,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
@@ -271,64 +295,420 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
def test_group_by_party(self):
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.posting_date = add_days(today(), -1)
|
||||
si1.save().submit()
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.items[0].rate = 85
|
||||
si2.save().submit()
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"group_by_party": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 5)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
warehouse="Finished Goods - _TC2",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
do_not_save=1,
|
||||
)
|
||||
# assert voucher rows
|
||||
expected_voucher_rows = [
|
||||
[100.0, 100.0, 100.0, 100.0],
|
||||
[85.0, 85.0, 85.0, 85.0],
|
||||
]
|
||||
voucher_rows = []
|
||||
for x in report[0:2]:
|
||||
voucher_rows.append(
|
||||
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
|
||||
)
|
||||
self.assertEqual(expected_voucher_rows, voucher_rows)
|
||||
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||
# assert total rows
|
||||
expected_total_rows = [
|
||||
[self.customer, 185.0, 185.0], # party total
|
||||
{}, # empty row for padding
|
||||
["Total", 185.0, 185.0], # grand total
|
||||
]
|
||||
party_total_row = report[2]
|
||||
self.assertEqual(
|
||||
expected_total_rows[0],
|
||||
[
|
||||
party_total_row.get("party"),
|
||||
party_total_row.get("invoiced"),
|
||||
party_total_row.get("outstanding"),
|
||||
],
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||
)
|
||||
si.append(
|
||||
"payment_schedule",
|
||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||
empty_row = report[3]
|
||||
self.assertEqual(expected_total_rows[1], empty_row)
|
||||
grand_total_row = report[4]
|
||||
self.assertEqual(
|
||||
expected_total_rows[2],
|
||||
[
|
||||
grand_total_row.get("party"),
|
||||
grand_total_row.get("invoiced"),
|
||||
grand_total_row.get("outstanding"),
|
||||
],
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
def test_future_payments(self):
|
||||
si = self.create_sales_invoice()
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.paid_amount = 90.0
|
||||
pe.references[0].allocated_amount = 90.0
|
||||
pe.save().submit()
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_future_payments": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
expected_data = [100.0, 100.0, 10.0, 90.0]
|
||||
|
||||
return si
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
pe.cancel()
|
||||
# full payment in future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.save().submit()
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, 0.0, 100.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def make_payment(docname):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
# over payment in future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.paid_amount = 110
|
||||
pe.save().submit()
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 2)
|
||||
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
|
||||
for idx, row in enumerate(report):
|
||||
self.assertEqual(
|
||||
expected_data[idx],
|
||||
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||
)
|
||||
|
||||
def test_sales_person(self):
|
||||
sales_person = (
|
||||
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
|
||||
si.save().submit()
|
||||
|
||||
def make_credit_note(docname):
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
qty=-1,
|
||||
warehouse="Finished Goods - _TC2",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"sales_person": sales_person.name,
|
||||
"show_sales_person": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
|
||||
return credit_note
|
||||
expected_data = [100.0, 100.0, sales_person.name]
|
||||
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
|
||||
|
||||
def test_cost_center_filter(self):
|
||||
si = self.create_sales_invoice()
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, self.cost_center]
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
|
||||
|
||||
def test_customer_group_filter(self):
|
||||
si = self.create_sales_invoice()
|
||||
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer_group": cus_group,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, cus_group]
|
||||
row = report[0]
|
||||
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
|
||||
|
||||
filters.update({"customer_group": "Individual"})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 0)
|
||||
|
||||
def test_party_account_filter(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
self.customer2 = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.posting_date = add_days(today(), -1)
|
||||
si2.customer = self.customer2
|
||||
si2.currency = "USD"
|
||||
si2.conversion_rate = 80
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.save().submit()
|
||||
|
||||
# Filter on company currency receivable account
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"party_account": self.debit_to,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
)
|
||||
|
||||
# Filter on USD receivable account
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
)
|
||||
|
||||
# without filter on party account
|
||||
filters.pop("party_account")
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 2)
|
||||
expected_data = [
|
||||
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
|
||||
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
|
||||
]
|
||||
for idx, row in enumerate(report):
|
||||
self.assertEqual(
|
||||
expected_data[idx],
|
||||
[
|
||||
row.invoiced,
|
||||
row.outstanding,
|
||||
row.invoiced_in_account_currency,
|
||||
row.outstanding_in_account_currency,
|
||||
row.party_account,
|
||||
row.account_currency,
|
||||
],
|
||||
)
|
||||
|
||||
def test_usd_customer_filter(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.debit_to = self.debtors_usd
|
||||
si.save().submit()
|
||||
name = si.name
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
report = execute(filters)
|
||||
|
||||
expected = {
|
||||
"voucher_type": si.doctype,
|
||||
"voucher_no": si.name,
|
||||
"party_account": self.debtors_usd,
|
||||
"customer_name": self.customer,
|
||||
"invoiced": 100.0,
|
||||
"outstanding": 100.0,
|
||||
"account_currency": "USD",
|
||||
}
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
report_output = report[1][0]
|
||||
for field in expected:
|
||||
with self.subTest(field=field):
|
||||
self.assertEqual(report_output.get(field), expected.get(field))
|
||||
|
||||
def test_multi_select_party_filter(self):
|
||||
self.customer1 = self.customer
|
||||
self.create_customer("_Test Customer 2")
|
||||
self.customer2 = self.customer
|
||||
self.create_customer("_Test Customer 3")
|
||||
self.customer3 = self.customer
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer1, self.customer3],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si1.customer = self.customer1
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si2.customer = self.customer2
|
||||
si2.save().submit()
|
||||
|
||||
si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si3.customer = self.customer3
|
||||
si3.save().submit()
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
report = execute(filters)
|
||||
|
||||
expected_output = {self.customer1, self.customer3}
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
output_for = set([x.party for x in report[1]])
|
||||
self.assertEqual(output_for, expected_output)
|
||||
|
||||
def test_report_output_if_party_is_missing(self):
|
||||
acc_name = "Additional Debtors"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": acc_name, "company": self.company}
|
||||
):
|
||||
additional_receivable_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": acc_name,
|
||||
"parent_account": "Accounts Receivable - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
"account_type": "Receivable",
|
||||
}
|
||||
).save()
|
||||
self.debtors2 = additional_receivable_acc.name
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.posting_date = today()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 150,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debtors2,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 200,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 350,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
# manually remove party from Payment Ledger
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
report_ouput = execute(filters)[1]
|
||||
expected_data = [
|
||||
[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
|
||||
[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
|
||||
]
|
||||
self.assertEqual(len(report_ouput), 2)
|
||||
# fetch only required fields
|
||||
report_output = [
|
||||
[
|
||||
x.party_account,
|
||||
x.voucher_type,
|
||||
x.voucher_no,
|
||||
"Customer",
|
||||
self.customer,
|
||||
x.invoiced,
|
||||
x.paid,
|
||||
x.credit_note,
|
||||
x.outstanding,
|
||||
]
|
||||
for x in report_ouput
|
||||
]
|
||||
# use account name to sort
|
||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||
report_output = sorted(report_output, key=lambda x: x[0])
|
||||
self.assertEqual(expected_data, report_output)
|
||||
|
||||
@@ -72,10 +72,27 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"customer",
|
||||
"label": __("Customer"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer"
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname":"customer_group",
|
||||
@@ -133,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=party,
|
||||
account_type=self.account_type,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if party_dict.outstanding == 0:
|
||||
@@ -100,13 +99,11 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# Add all amount columns
|
||||
for k in list(self.party_total[d.party]):
|
||||
if k not in ["currency", "sales_person"]:
|
||||
|
||||
self.party_total[d.party][k] += d.get(k, 0.0)
|
||||
if isinstance(self.party_total[d.party][k], float):
|
||||
self.party_total[d.party][k] += d.get(k) or 0.0
|
||||
|
||||
# set territory, customer_group, sales person etc
|
||||
self.set_party_details(d)
|
||||
self.party_total[d.party].update({"party_type": d.party_type})
|
||||
|
||||
def init_party_total(self, row):
|
||||
self.party_total.setdefault(
|
||||
@@ -125,6 +122,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -134,13 +132,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
for key in ("territory", "customer_group", "supplier_group"):
|
||||
if row.get(key):
|
||||
self.party_total[row.party][key] = row.get(key)
|
||||
|
||||
self.party_total[row.party][key] = row.get(key, "")
|
||||
if row.sales_person:
|
||||
self.party_total[row.party].sales_person.append(row.sales_person)
|
||||
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
@@ -233,12 +230,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
|
||||
def get_gl_balance(report_date):
|
||||
def get_gl_balance(report_date, company):
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"GL Entry",
|
||||
fields=["party", "sum(debit - credit)"],
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
|
||||
group_by="party",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
Test for Invoices, Paid, Advance and Outstanding
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=200,
|
||||
price_list_rate=200,
|
||||
)
|
||||
|
||||
customer_group, customer_territory = frappe.db.get_all(
|
||||
"Customer",
|
||||
filters={"name": self.customer},
|
||||
fields=["customer_group", "territory"],
|
||||
as_list=True,
|
||||
)[0]
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
expected_data = {
|
||||
"party_type": "Customer",
|
||||
"advance": 0,
|
||||
"party": self.customer,
|
||||
"invoiced": 200.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 200.0,
|
||||
"range1": 200.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 200.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"currency": si.currency,
|
||||
"territory": customer_territory,
|
||||
"customer_group": customer_group,
|
||||
}
|
||||
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# simulate advance payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 50
|
||||
pe.references[0].allocated_amount = 0 # this essitially removes the reference
|
||||
pe.save().submit()
|
||||
|
||||
# update expected data with advance
|
||||
expected_data.update(
|
||||
{
|
||||
"advance": 50.0,
|
||||
"outstanding": 150.0,
|
||||
"range1": 150.0,
|
||||
"total_due": 150.0,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# make partial payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 125
|
||||
pe.references[0].allocated_amount = 125
|
||||
pe.save().submit()
|
||||
|
||||
# update expected data after advance and partial payment
|
||||
expected_data.update(
|
||||
{"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
@change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
|
||||
def test_02_various_filters_and_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=200,
|
||||
price_list_rate=200,
|
||||
)
|
||||
# make partial payment
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 150
|
||||
pe.references[0].allocated_amount = 150
|
||||
pe.save().submit()
|
||||
|
||||
customer_group, customer_territory = frappe.db.get_all(
|
||||
"Customer",
|
||||
filters={"name": self.customer},
|
||||
fields=["customer_group", "territory"],
|
||||
as_list=True,
|
||||
)[0]
|
||||
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
expected_data = {
|
||||
"party_type": "Customer",
|
||||
"advance": 0,
|
||||
"party": self.customer,
|
||||
"party_name": self.customer,
|
||||
"invoiced": 200.0,
|
||||
"paid": 150.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 50.0,
|
||||
"range1": 50.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 50.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"currency": si.currency,
|
||||
"territory": customer_territory,
|
||||
"customer_group": customer_group,
|
||||
}
|
||||
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# with gl balance filter
|
||||
filters.update({"show_gl_balance": True})
|
||||
expected_data.update({"gl_balance": 50.0, "diff": 0.0})
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# with gl balance and future payments filter
|
||||
filters.update({"show_future_payments": True})
|
||||
expected_data.update({"remaining_balance": 50.0})
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertDictEqual(rpt_output[0], expected_data)
|
||||
|
||||
# invoice fully paid
|
||||
pe = get_payment_entry(si.doctype, si.name).save().submit()
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 0)
|
||||
@@ -58,6 +58,9 @@ def get_data(filters):
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT asset_category,
|
||||
@@ -98,15 +101,25 @@ def get_asset_categories(filters):
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
|
||||
group by asset_category
|
||||
""",
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT results.asset_category,
|
||||
@@ -138,7 +151,7 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
@@ -154,10 +167,12 @@ def get_assets(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ class TestBankReconciliationStatement(FrappeTestCase):
|
||||
"Payment Entry",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def test_loan_entries_in_bank_reco_statement(self):
|
||||
create_loan_accounts()
|
||||
|
||||
@@ -67,7 +67,7 @@ def setup_mappers(mappers):
|
||||
mapping["finance_costs"] = []
|
||||
mapping["finance_costs_adjustments"] = []
|
||||
doc = frappe.get_doc("Cash Flow Mapper", mapping["name"])
|
||||
mapping_names = [item.name for item in doc.accounts]
|
||||
mapping_names = [item.mapping for item in doc.accounts]
|
||||
|
||||
if not mapping_names:
|
||||
continue
|
||||
|
||||
@@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
|
||||
if from_date:
|
||||
additional_conditions.append(gle.posting_date >= from_date)
|
||||
|
||||
finance_book = filters.get("finance_book")
|
||||
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
|
||||
finance_books = []
|
||||
finance_books.append("")
|
||||
if filter_fb := filters.get("finance_book"):
|
||||
finance_books.append(filter_fb)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
|
||||
if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
|
||||
finance_books.append(company_fb)
|
||||
|
||||
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
|
||||
else:
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
|
||||
additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
|
||||
|
||||
return additional_conditions
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = self.deferred_revenue_account
|
||||
item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account
|
||||
item.no_of_months = 3
|
||||
item.save()
|
||||
|
||||
@@ -150,7 +150,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = self.deferred_expense_account
|
||||
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 3
|
||||
item.save()
|
||||
|
||||
|
||||
@@ -335,12 +335,10 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
|
||||
for period in period_list:
|
||||
total_row.setdefault(period.key, 0.0)
|
||||
total_row[period.key] += row.get(period.key, 0.0)
|
||||
row[period.key] = row.get(period.key, 0.0)
|
||||
|
||||
total_row.setdefault("total", 0.0)
|
||||
total_row["total"] += flt(row["total"])
|
||||
total_row["opening_balance"] += row["opening_balance"]
|
||||
row["total"] = ""
|
||||
|
||||
if "total" in total_row:
|
||||
out.append(total_row)
|
||||
@@ -639,7 +637,13 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
if periodicity != "Yearly":
|
||||
if not accumulated_values:
|
||||
columns.append(
|
||||
{"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150}
|
||||
{
|
||||
"fieldname": "total",
|
||||
"label": _("Total"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
"options": "currency",
|
||||
}
|
||||
)
|
||||
|
||||
return columns
|
||||
|
||||
@@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object):
|
||||
self.gle_balances = set(val.gle) | self.gle_balances
|
||||
self.ple_balances = set(val.ple) | self.ple_balances
|
||||
|
||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
||||
self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
|
||||
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.diff1:
|
||||
for x in self.variation_in_payment_ledger:
|
||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||
|
||||
for x in self.diff2:
|
||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
||||
for x in self.variation_in_general_ledger:
|
||||
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
|
||||
frappe._dict({"pl_balance": x[4]})
|
||||
)
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
|
||||
@@ -272,20 +272,19 @@ def get_conditions(filters):
|
||||
if match_conditions:
|
||||
conditions.append(match_conditions)
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if not dimension.disabled:
|
||||
if filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
else:
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if not dimension.disabled:
|
||||
if filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
else:
|
||||
conditions.append("{0} in %({0})s".format(dimension.fieldname))
|
||||
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user