mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-09 08:22:25 +00:00
Compare commits
1051 Commits
ankush-pat
...
nextchamp-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8b241d5a | ||
|
|
2f9856436e | ||
|
|
6851b5ba97 | ||
|
|
c957a5cd2e | ||
|
|
934e1b4e6a | ||
|
|
6f6928fa7b | ||
|
|
02d5d43eb1 | ||
|
|
299fb2619f | ||
|
|
4a9a5f03fc | ||
|
|
ba553dba62 | ||
|
|
29e025c012 | ||
|
|
90dee8b663 | ||
|
|
edaa0bc3f7 | ||
|
|
984905109d | ||
|
|
45291aaa43 | ||
|
|
f7fb001126 | ||
|
|
a575bd50ef | ||
|
|
7d2d2dd0af | ||
|
|
7934441f3c | ||
|
|
cfd4da3e8a | ||
|
|
a55b818119 | ||
|
|
82a136f991 | ||
|
|
91a26608ee | ||
|
|
0d1df26b88 | ||
|
|
bc39dfab5d | ||
|
|
8503d581a5 | ||
|
|
9bf87d708e | ||
|
|
56f5078357 | ||
|
|
f193393f57 | ||
|
|
fd3fb64aa3 | ||
|
|
869289a6e4 | ||
|
|
e6a9b6ee95 | ||
|
|
5cc3c08059 | ||
|
|
1101b7bfdf | ||
|
|
ef4bd77196 | ||
|
|
9c66ebb108 | ||
|
|
b70615ef18 | ||
|
|
12325cb685 | ||
|
|
682730a498 | ||
|
|
5f2874743b | ||
|
|
d56070301c | ||
|
|
4571bd58d2 | ||
|
|
6ec7590c21 | ||
|
|
1eefd517d1 | ||
|
|
4c61ee30bb | ||
|
|
74b29eb5e2 | ||
|
|
1e3eb4deed | ||
|
|
7c7cfc4373 | ||
|
|
32a4ca6b6c | ||
|
|
2c2b5a5b6e | ||
|
|
770640f4e1 | ||
|
|
b4ace345f9 | ||
|
|
e56d40f48a | ||
|
|
d0d3961a1e | ||
|
|
986daa6578 | ||
|
|
576575c227 | ||
|
|
706be2a415 | ||
|
|
7c42b72ee7 | ||
|
|
edb0666b2c | ||
|
|
c4a0842a4f | ||
|
|
06eac67796 | ||
|
|
ddb17a8880 | ||
|
|
d999dea3e4 | ||
|
|
345e6facbe | ||
|
|
cbdaab940d | ||
|
|
a7a45fbe59 | ||
|
|
af3e807607 | ||
|
|
b6c0a675f9 | ||
|
|
a4112c75c5 | ||
|
|
448afa2b7c | ||
|
|
0ac81e3793 | ||
|
|
33ff1b5a72 | ||
|
|
2b0470d1f5 | ||
|
|
17131e5a02 | ||
|
|
ad11934d39 | ||
|
|
7c5720ddb3 | ||
|
|
ddc3050e99 | ||
|
|
f5453adf4d | ||
|
|
d0660ad222 | ||
|
|
517b5f8567 | ||
|
|
867d898304 | ||
|
|
738dd57904 | ||
|
|
07c9b99072 | ||
|
|
7c13533f30 | ||
|
|
7aafc90d58 | ||
|
|
393bc25e2d | ||
|
|
50c1172f29 | ||
|
|
12ad2aa2e5 | ||
|
|
dae40dfbb4 | ||
|
|
d52f7e2820 | ||
|
|
3c553b0938 | ||
|
|
17f8080168 | ||
|
|
216a46bd66 | ||
|
|
1142e69d1a | ||
|
|
f7780cdb58 | ||
|
|
8c7fa5712b | ||
|
|
0cd870fc29 | ||
|
|
282a0ee2ce | ||
|
|
980cca1772 | ||
|
|
34c190b7d6 | ||
|
|
e6b0196493 | ||
|
|
87108be11a | ||
|
|
8ce1da111e | ||
|
|
f8cff09129 | ||
|
|
b7dcf27b01 | ||
|
|
30eb6c8512 | ||
|
|
3fd95200da | ||
|
|
528ab503f2 | ||
|
|
f7bf1b8a0c | ||
|
|
829bc6a51f | ||
|
|
24a0833980 | ||
|
|
76cea7dd6a | ||
|
|
6d6a7bcb50 | ||
|
|
fa991b2dcf | ||
|
|
f9397a87ac | ||
|
|
d8e73b63f3 | ||
|
|
79911734e9 | ||
|
|
0df3a1a3af | ||
|
|
08fc686513 | ||
|
|
59c2e7ec3e | ||
|
|
6966fa4d88 | ||
|
|
ee6c107d58 | ||
|
|
2d19cee713 | ||
|
|
af5f59e856 | ||
|
|
aaa4d1eb55 | ||
|
|
c5da7f5fb1 | ||
|
|
109a9f1390 | ||
|
|
4c35a37eaa | ||
|
|
44053db010 | ||
|
|
fc86a8568f | ||
|
|
d791dc11a3 | ||
|
|
1eea585d29 | ||
|
|
2d14d92b32 | ||
|
|
0aadb680eb | ||
|
|
00fe3042b2 | ||
|
|
7f11373b58 | ||
|
|
d11a64ddcb | ||
|
|
a07a5572ea | ||
|
|
ca10e2bb9f | ||
|
|
2ac079210d | ||
|
|
f2c78163bf | ||
|
|
d8ece86463 | ||
|
|
7b630217bd | ||
|
|
be723bb9d4 | ||
|
|
6b0dc62681 | ||
|
|
0753aa5ab7 | ||
|
|
3fb64cacfc | ||
|
|
6e0f1c6ad6 | ||
|
|
cc61daeec4 | ||
|
|
d0715a82eb | ||
|
|
e2f19c6a14 | ||
|
|
ec075122b6 | ||
|
|
861387f164 | ||
|
|
48fae0c1ce | ||
|
|
db1f2d6279 | ||
|
|
b04a101c11 | ||
|
|
1d767d58f2 | ||
|
|
558db13040 | ||
|
|
563ee92043 | ||
|
|
b6b8392413 | ||
|
|
bd2dd7dfac | ||
|
|
357d4994e4 | ||
|
|
ea269c6a1c | ||
|
|
f1752fcd7a | ||
|
|
e0042972c8 | ||
|
|
e9f5ea6ede | ||
|
|
55489d0056 | ||
|
|
e0b288fbf0 | ||
|
|
258242f8d7 | ||
|
|
f95ad039e4 | ||
|
|
b89ecd482d | ||
|
|
70c78d0d67 | ||
|
|
22ad9a1903 | ||
|
|
d6d0163514 | ||
|
|
0dbf4557db | ||
|
|
185a8910d2 | ||
|
|
fe672aa9b1 | ||
|
|
6c88fb7a8d | ||
|
|
48a88b7064 | ||
|
|
72e9c05f46 | ||
|
|
c6999fc687 | ||
|
|
91f7b0bbd6 | ||
|
|
76775a3e49 | ||
|
|
492e994c22 | ||
|
|
fa776d2987 | ||
|
|
d8e54a21fb | ||
|
|
5c06620f97 | ||
|
|
d267111e13 | ||
|
|
796ad01b87 | ||
|
|
c8cc3fc65f | ||
|
|
4416ddc4af | ||
|
|
4a05f3736f | ||
|
|
66f650061d | ||
|
|
9d49afebde | ||
|
|
e1d28063b0 | ||
|
|
b8703b0b9b | ||
|
|
b8a61be080 | ||
|
|
097be8bdea | ||
|
|
f2e9a67f37 | ||
|
|
442ee3adba | ||
|
|
5bf6fb43a9 | ||
|
|
7101e7acc4 | ||
|
|
caa1a3dccf | ||
|
|
9f26e3c27a | ||
|
|
da37573b73 | ||
|
|
e371bb6672 | ||
|
|
fa6d37542b | ||
|
|
aae53bb910 | ||
|
|
a9989b12d5 | ||
|
|
9d0a1149d8 | ||
|
|
a43304b01b | ||
|
|
7fcd74ed03 | ||
|
|
1744f1d4e4 | ||
|
|
6308b509fc | ||
|
|
0157fa15eb | ||
|
|
430378bb6e | ||
|
|
9c1e566394 | ||
|
|
f9cfabf78e | ||
|
|
ae0a8a6529 | ||
|
|
ebb6953274 | ||
|
|
9abf0ef615 | ||
|
|
b09c2381ca | ||
|
|
9a8f8e8b7d | ||
|
|
428274df56 | ||
|
|
87b5734f5b | ||
|
|
c312692af5 | ||
|
|
046834a97a | ||
|
|
baef5ae1ef | ||
|
|
719ac5c8f3 | ||
|
|
f0267feca8 | ||
|
|
761f68d7bf | ||
|
|
a1ad9587fa | ||
|
|
2d66384cbc | ||
|
|
52ab11389b | ||
|
|
3249a79f07 | ||
|
|
b53dcb04ed | ||
|
|
a65b80911b | ||
|
|
ba1162ff3f | ||
|
|
336e0b9666 | ||
|
|
71de72bdd0 | ||
|
|
6de826b8c4 | ||
|
|
4d1f620d75 | ||
|
|
2feb27e399 | ||
|
|
4cd16a839b | ||
|
|
f02fc8acf0 | ||
|
|
10632d75b0 | ||
|
|
7d0199d743 | ||
|
|
ea8e23384d | ||
|
|
3952b3373d | ||
|
|
8ad9e99cea | ||
|
|
ee12313dbb | ||
|
|
de18f98c5c | ||
|
|
53efc8ff77 | ||
|
|
04b346865f | ||
|
|
7f2e7badff | ||
|
|
5dabc98ba5 | ||
|
|
b5f80815a1 | ||
|
|
c217bb2018 | ||
|
|
2ae5834290 | ||
|
|
2f157fa5d3 | ||
|
|
310f71c313 | ||
|
|
4a2046dfb6 | ||
|
|
502a37a864 | ||
|
|
ac87fca0d7 | ||
|
|
70de444b7b | ||
|
|
0185096e09 | ||
|
|
e6a02719f7 | ||
|
|
31ed4ef6d6 | ||
|
|
0e1b7760a8 | ||
|
|
53701c37b1 | ||
|
|
6190b4cf63 | ||
|
|
dc2998f544 | ||
|
|
75ec0a0a85 | ||
|
|
619824855a | ||
|
|
768c3a4927 | ||
|
|
d7da8928ac | ||
|
|
8bcbc45add | ||
|
|
fc1088d9c4 | ||
|
|
f3993783a3 | ||
|
|
f5937f46cb | ||
|
|
c3d455653e | ||
|
|
c089c4156c | ||
|
|
abf9a28d6a | ||
|
|
6e43d6882a | ||
|
|
90e1a6bc16 | ||
|
|
310643d215 | ||
|
|
6e9d001819 | ||
|
|
50dc0c18eb | ||
|
|
330ae419be | ||
|
|
cb0b6de4b9 | ||
|
|
a9f0a11ce6 | ||
|
|
790f0aac12 | ||
|
|
6a89cb98ce | ||
|
|
e0a3869874 | ||
|
|
6cd2ba5c66 | ||
|
|
46a722d51c | ||
|
|
32b64db4d6 | ||
|
|
28dd1a25cb | ||
|
|
e5f603c9d9 | ||
|
|
95c9aafda9 | ||
|
|
436fc03eda | ||
|
|
b38fe24090 | ||
|
|
d88fe27be6 | ||
|
|
f9c75055bb | ||
|
|
9bb8e351a2 | ||
|
|
dfddc4efc3 | ||
|
|
2942cee31e | ||
|
|
35c70f39fa | ||
|
|
a8445da02a | ||
|
|
58c027d4cc | ||
|
|
26ced47ddc | ||
|
|
8e86553717 | ||
|
|
90abb28cb4 | ||
|
|
eab775ef32 | ||
|
|
f18ae5856f | ||
|
|
a51bec0269 | ||
|
|
3ebe7d861d | ||
|
|
1de531e56e | ||
|
|
ecd15d2a65 | ||
|
|
9cd42dd60f | ||
|
|
c09a61f360 | ||
|
|
c22641092a | ||
|
|
bda1340760 | ||
|
|
bbb6a62a7d | ||
|
|
81651beaef | ||
|
|
5dcb723fc7 | ||
|
|
83f3e317e1 | ||
|
|
d8b9b83b11 | ||
|
|
9e38b61bd9 | ||
|
|
feeebb1262 | ||
|
|
dd74839eba | ||
|
|
a663df376c | ||
|
|
6607c8bd82 | ||
|
|
f92c63fb10 | ||
|
|
7fa7d6b5e4 | ||
|
|
e18336ebe7 | ||
|
|
6d6e3cb015 | ||
|
|
a11d3327df | ||
|
|
b6bad728cd | ||
|
|
7d10dd9ea8 | ||
|
|
e26c6dc76b | ||
|
|
04f00fd141 | ||
|
|
8e46aebc50 | ||
|
|
49af5ba434 | ||
|
|
670304fe8e | ||
|
|
6417ae0ee8 | ||
|
|
c3f39c3f32 | ||
|
|
36c08d0835 | ||
|
|
47ab26dedf | ||
|
|
a9920715ab | ||
|
|
88d888d9d0 | ||
|
|
3692f960d0 | ||
|
|
60d3b86cdb | ||
|
|
e3de216e06 | ||
|
|
6412583e98 | ||
|
|
e3c000d0be | ||
|
|
19c0b7a523 | ||
|
|
a8f03ebf7f | ||
|
|
80e94a08cf | ||
|
|
b44a19bd1a | ||
|
|
a06d24037b | ||
|
|
dde5bbc9a7 | ||
|
|
47add0b751 | ||
|
|
9739d8b52a | ||
|
|
4a7b1de2d8 | ||
|
|
c88444a6c4 | ||
|
|
5e3a034395 | ||
|
|
b781c57dcb | ||
|
|
3547252217 | ||
|
|
76861eb332 | ||
|
|
35cdd996a9 | ||
|
|
18541ef54f | ||
|
|
77b88961f6 | ||
|
|
6b789e2f04 | ||
|
|
19456127cf | ||
|
|
3f2a2c96f6 | ||
|
|
5e9620b98b | ||
|
|
3871cf7110 | ||
|
|
0e388ba872 | ||
|
|
3c96791d52 | ||
|
|
44ee9f0f19 | ||
|
|
1b010add26 | ||
|
|
5bbe8728ad | ||
|
|
e84e778151 | ||
|
|
c8fda8a8b5 | ||
|
|
183e42af1a | ||
|
|
2a90fad710 | ||
|
|
2959285c82 | ||
|
|
f342e6e765 | ||
|
|
c722f2819c | ||
|
|
2c3973156f | ||
|
|
5b7d23de15 | ||
|
|
5e48e61c66 | ||
|
|
9c6466f15b | ||
|
|
c76352340d | ||
|
|
07c6ad6fa7 | ||
|
|
35489fbbf9 | ||
|
|
6b144baa69 | ||
|
|
dff9655d46 | ||
|
|
f1383b5ef9 | ||
|
|
c292859b59 | ||
|
|
91983fb4c6 | ||
|
|
b93c18bd4a | ||
|
|
47264481ad | ||
|
|
6b3028d77b | ||
|
|
e50bd52162 | ||
|
|
ee21850ab8 | ||
|
|
067f8c1d70 | ||
|
|
68202639f5 | ||
|
|
4399a93843 | ||
|
|
758f47bba6 | ||
|
|
70660191a6 | ||
|
|
320f3c1ac0 | ||
|
|
156e45970a | ||
|
|
4d79ffe3c6 | ||
|
|
e8b8c51d82 | ||
|
|
fb5796f18f | ||
|
|
3f44ef8790 | ||
|
|
471e77b931 | ||
|
|
c793fb0bb4 | ||
|
|
c7a540b7e2 | ||
|
|
6c6195bae6 | ||
|
|
421e34a181 | ||
|
|
0cd69c8700 | ||
|
|
46afbb594a | ||
|
|
ffa9c6e4d9 | ||
|
|
20bdc63b03 | ||
|
|
74fab53e28 | ||
|
|
d7ef5ad955 | ||
|
|
22010b745c | ||
|
|
d3ff289779 | ||
|
|
ef56060166 | ||
|
|
2e43123048 | ||
|
|
9235918d65 | ||
|
|
d5b7f2e49e | ||
|
|
8e40c04494 | ||
|
|
f7fd30fecf | ||
|
|
4c5147f270 | ||
|
|
e69ea17f59 | ||
|
|
5b6128848f | ||
|
|
662ebc5c1a | ||
|
|
d003370f61 | ||
|
|
b0cfb0b9ef | ||
|
|
a58f2e6c03 | ||
|
|
75d9dbca43 | ||
|
|
447310702e | ||
|
|
960085853a | ||
|
|
5faa62c49a | ||
|
|
3014bac293 | ||
|
|
00a4191966 | ||
|
|
770369e5c1 | ||
|
|
86be259341 | ||
|
|
ce748cec3a | ||
|
|
231fe4156f | ||
|
|
c533213225 | ||
|
|
ff4ae69408 | ||
|
|
a67284e96d | ||
|
|
4f2553e7b6 | ||
|
|
192a3395a5 | ||
|
|
e5a2b15fba | ||
|
|
148703bfc2 | ||
|
|
ce3f9622cb | ||
|
|
48bb2c942b | ||
|
|
77f6789706 | ||
|
|
3e5691072a | ||
|
|
b9a7ff7c3d | ||
|
|
b0e8dfc1d0 | ||
|
|
d76759e066 | ||
|
|
a0eb5e5535 | ||
|
|
e4953df4a3 | ||
|
|
0df28c7174 | ||
|
|
201573ab9a | ||
|
|
0efdc6c13a | ||
|
|
ab7293bcd3 | ||
|
|
6ea0a69d3a | ||
|
|
e478a5d0ce | ||
|
|
47c91324b1 | ||
|
|
185c543b73 | ||
|
|
5685492995 | ||
|
|
bd7e6e264e | ||
|
|
db2076db69 | ||
|
|
9d8502f406 | ||
|
|
4686217d3c | ||
|
|
52435a3a7f | ||
|
|
396f455258 | ||
|
|
f8270f093d | ||
|
|
05f4b659a7 | ||
|
|
2fedd595a9 | ||
|
|
0352e1dace | ||
|
|
7d794bc70a | ||
|
|
67b5471d09 | ||
|
|
82490d9f55 | ||
|
|
07a0e2b7a8 | ||
|
|
c8cd351b39 | ||
|
|
4a6a7593c3 | ||
|
|
3380dc5dea | ||
|
|
9d0096ad9e | ||
|
|
bb424965ee | ||
|
|
b961321de5 | ||
|
|
2d54bf7199 | ||
|
|
e5da0d7a63 | ||
|
|
8ea9e4576f | ||
|
|
be1f941996 | ||
|
|
c80aaad437 | ||
|
|
dc0ddf8d7e | ||
|
|
ce8a1086a7 | ||
|
|
56356ffbb9 | ||
|
|
d7a665cb84 | ||
|
|
9ae7578b07 | ||
|
|
6d513e2519 | ||
|
|
03321f5f13 | ||
|
|
2afed7de03 | ||
|
|
ae2de3cf65 | ||
|
|
cb1d33880c | ||
|
|
b3fe7c6dad | ||
|
|
2884738864 | ||
|
|
fbf2d7e9de | ||
|
|
f8c852c54c | ||
|
|
ffeda0cee9 | ||
|
|
2cc7239dd5 | ||
|
|
940ad6e3f2 | ||
|
|
3518d4be1d | ||
|
|
3c7b460fd8 | ||
|
|
103a91ed40 | ||
|
|
9bb64107c5 | ||
|
|
4ac983cf09 | ||
|
|
7156184933 | ||
|
|
ece6358e60 | ||
|
|
c5834c1db4 | ||
|
|
ddb4396117 | ||
|
|
510b265830 | ||
|
|
6ffdeb1af8 | ||
|
|
22d0e1373b | ||
|
|
423f2b5627 | ||
|
|
6e015fd6be | ||
|
|
57c8871de0 | ||
|
|
0b76a26c8a | ||
|
|
bb7fe795fe | ||
|
|
207eeefc85 | ||
|
|
5138ef0160 | ||
|
|
7b3d496ce0 | ||
|
|
be41052dc8 | ||
|
|
b642718f08 | ||
|
|
b3dc22de29 | ||
|
|
e007776ada | ||
|
|
11d165c274 | ||
|
|
3e5f8323b0 | ||
|
|
5d8a2d3d3b | ||
|
|
f487660a7c | ||
|
|
2bad86d8d8 | ||
|
|
5f38859a04 | ||
|
|
fa9b327501 | ||
|
|
d69c839369 | ||
|
|
1f6ab86a65 | ||
|
|
94cacb60de | ||
|
|
6b781d78e0 | ||
|
|
a34a1f8fd2 | ||
|
|
faecf3ee40 | ||
|
|
6674edface | ||
|
|
428b099f63 | ||
|
|
8abe0ce0ec | ||
|
|
fee0ca8cd9 | ||
|
|
f270880735 | ||
|
|
1b967f37af | ||
|
|
0d7f98b496 | ||
|
|
9e50aa4833 | ||
|
|
3598bcc9a8 | ||
|
|
9dd6b3c950 | ||
|
|
de10f2dc00 | ||
|
|
7e592fdf91 | ||
|
|
a835c1a418 | ||
|
|
b31b850db8 | ||
|
|
c5ca8d74c4 | ||
|
|
cef7dfd0b4 | ||
|
|
e3d7426b47 | ||
|
|
21f425660d | ||
|
|
33b68552f1 | ||
|
|
da323cbb40 | ||
|
|
41222659a6 | ||
|
|
1f4fa90072 | ||
|
|
58dab9ca8b | ||
|
|
def759d2e8 | ||
|
|
f19eadab9a | ||
|
|
a504b6eac0 | ||
|
|
92d857d49c | ||
|
|
91982d1e4f | ||
|
|
a067a904a1 | ||
|
|
302f5e27be | ||
|
|
840f9d8987 | ||
|
|
a5478c61af | ||
|
|
d155042edd | ||
|
|
84383087a6 | ||
|
|
4e6066f929 | ||
|
|
7800db7c0d | ||
|
|
7d5df407c4 | ||
|
|
bbde1f611f | ||
|
|
9ae3a54ce9 | ||
|
|
b3a216c8f8 | ||
|
|
62141b0b63 | ||
|
|
a3c89dbc10 | ||
|
|
ad437fd5ba | ||
|
|
7a8e89e2fe | ||
|
|
72be489b1b | ||
|
|
547d37b1db | ||
|
|
2f03dcb5b6 | ||
|
|
57c3216683 | ||
|
|
5b76e8b193 | ||
|
|
58dd40a2d7 | ||
|
|
f6f64083d2 | ||
|
|
29bf787313 | ||
|
|
4cbd63edbf | ||
|
|
49aed7ff69 | ||
|
|
e4bceaaf66 | ||
|
|
b5d2eedfa6 | ||
|
|
11b2994fe8 | ||
|
|
02566a02a8 | ||
|
|
d462d6de06 | ||
|
|
1e607cd4c7 | ||
|
|
597ef0a498 | ||
|
|
327b6fdb32 | ||
|
|
a94aa7a79f | ||
|
|
17045f88a1 | ||
|
|
ef90e24931 | ||
|
|
e8e20da78e | ||
|
|
cceb7922a5 | ||
|
|
333b2ae721 | ||
|
|
ecfebfec57 | ||
|
|
2c83fff1a1 | ||
|
|
cc569c955f | ||
|
|
60ec7b6cde | ||
|
|
f89600aee8 | ||
|
|
e8e21ea4b4 | ||
|
|
1de4742ffb | ||
|
|
eee3bd2554 | ||
|
|
6b31c27ed6 | ||
|
|
c4e1f81ccb | ||
|
|
d7522d40c0 | ||
|
|
c6cfacc108 | ||
|
|
db9beb3cdd | ||
|
|
f19212b5c8 | ||
|
|
d97127bff2 | ||
|
|
eeed3356ac | ||
|
|
e68c4e2aa8 | ||
|
|
dbde3a3421 | ||
|
|
c4d3a8a600 | ||
|
|
64f7f3a12a | ||
|
|
1cadef97bf | ||
|
|
e4cf0dced8 | ||
|
|
fb950417bf | ||
|
|
0639d9e32a | ||
|
|
e19161a8ee | ||
|
|
86cf5c89ab | ||
|
|
cebb5a42f4 | ||
|
|
011a4c3faa | ||
|
|
66ae807a26 | ||
|
|
d6915df81d | ||
|
|
20c8873208 | ||
|
|
a144bb01fe | ||
|
|
070bc8badd | ||
|
|
cf7b43f7f0 | ||
|
|
1bcff80074 | ||
|
|
179a31ed5e | ||
|
|
61cd0f98fd | ||
|
|
352dcddd6d | ||
|
|
1769b45d4b | ||
|
|
0ed6552655 | ||
|
|
adaeba1554 | ||
|
|
312625fdc5 | ||
|
|
9627b46ee7 | ||
|
|
56aa1866da | ||
|
|
dceef0397a | ||
|
|
be382054e5 | ||
|
|
333907b7a5 | ||
|
|
8119442c94 | ||
|
|
9069643504 | ||
|
|
b3759890d7 | ||
|
|
2c50f43cdd | ||
|
|
828eaf0930 | ||
|
|
11cf694d9a | ||
|
|
67cf7e1728 | ||
|
|
906ad10d16 | ||
|
|
bfbc9ea8f7 | ||
|
|
242af681e9 | ||
|
|
f70d757b82 | ||
|
|
736afdf85a | ||
|
|
c2b3843d80 | ||
|
|
e22d56484d | ||
|
|
75e52d7108 | ||
|
|
d63258ee9b | ||
|
|
2ca4d3fb71 | ||
|
|
29088e1777 | ||
|
|
afb33f2049 | ||
|
|
232726288a | ||
|
|
cfb0bb1eaa | ||
|
|
e0db2670f9 | ||
|
|
9ed7594f6e | ||
|
|
b26e96cdf4 | ||
|
|
391f42db04 | ||
|
|
f028bd6e69 | ||
|
|
a0e2a93f3f | ||
|
|
6878f40d1d | ||
|
|
434aa594d5 | ||
|
|
974e12c837 | ||
|
|
467a2b5cb4 | ||
|
|
694fc3e20c | ||
|
|
9ab8aa49d6 | ||
|
|
887ae1c434 | ||
|
|
be05aea101 | ||
|
|
aa1f2a7297 | ||
|
|
e0f5ecdad6 | ||
|
|
0f0bc9a462 | ||
|
|
0ed938a490 | ||
|
|
58ad79d3e8 | ||
|
|
a5cbdea8e4 | ||
|
|
ef2bf3c223 | ||
|
|
8a56df695d | ||
|
|
c51f9e0a97 | ||
|
|
f0c0a64984 | ||
|
|
ddc0127e05 | ||
|
|
b559245f2a | ||
|
|
59628f1934 | ||
|
|
a424acbfcb | ||
|
|
461f8e7724 | ||
|
|
d749c4aac2 | ||
|
|
20fe1875e7 | ||
|
|
4d5067d6d4 | ||
|
|
bbe5e5d9d6 | ||
|
|
dd3b79b511 | ||
|
|
6bc8bb26b6 | ||
|
|
ec171fc7c1 | ||
|
|
b56c1ed050 | ||
|
|
f056e9e7c5 | ||
|
|
57a6c37833 | ||
|
|
917b2190aa | ||
|
|
fa4af2acce | ||
|
|
99f5e869e0 | ||
|
|
05df8579cd | ||
|
|
7ee151880a | ||
|
|
d1d4671320 | ||
|
|
7249657d15 | ||
|
|
a4cb0313dd | ||
|
|
1ad1fc4c7d | ||
|
|
44f32f7521 | ||
|
|
789e448f0e | ||
|
|
e35ac4f8c2 | ||
|
|
c717e87c9e | ||
|
|
525f054c45 | ||
|
|
b0baba84a0 | ||
|
|
51b082b43c | ||
|
|
9fd1c3efb4 | ||
|
|
6b0b49ee3f | ||
|
|
bc1bdd58a7 | ||
|
|
f4a1218467 | ||
|
|
348dc32514 | ||
|
|
f6da6ece0a | ||
|
|
ee94127974 | ||
|
|
3ae47ab32c | ||
|
|
524c0994e0 | ||
|
|
c78399c618 | ||
|
|
1a83a67d41 | ||
|
|
ae039777f9 | ||
|
|
d3b74a868e | ||
|
|
9f59330fa9 | ||
|
|
a3ab8f973a | ||
|
|
751bdc98ed | ||
|
|
9a3d947e89 | ||
|
|
d9c91fa072 | ||
|
|
312e51e8b2 | ||
|
|
adb5249257 | ||
|
|
7b6b5ae581 | ||
|
|
e501c2db51 | ||
|
|
357c17214d | ||
|
|
6a4e25c1f9 | ||
|
|
aba83849a6 | ||
|
|
72c2e77a5d | ||
|
|
7c5168eeac | ||
|
|
914b23038c | ||
|
|
23fbe86d51 | ||
|
|
1b78fae6fc | ||
|
|
a7b011aed4 | ||
|
|
b425c2d7ec | ||
|
|
332f4dc028 | ||
|
|
e18b0ebe92 | ||
|
|
49025db33c | ||
|
|
3c393bfdc5 | ||
|
|
d054f37602 | ||
|
|
010718ffed | ||
|
|
98c39c4f5f | ||
|
|
7126d71627 | ||
|
|
239a5f8bf4 | ||
|
|
8d62cdfd5f | ||
|
|
bb4725e9b5 | ||
|
|
12822f7c36 | ||
|
|
ad53ecf2b4 | ||
|
|
48a9cd5ef3 | ||
|
|
6e1ef4f3d0 | ||
|
|
728dc1acf4 | ||
|
|
d072169340 | ||
|
|
c3911a592a | ||
|
|
d65243eb65 | ||
|
|
c716dcc01e | ||
|
|
c764f14f53 | ||
|
|
35c29e0226 | ||
|
|
f1810803e1 | ||
|
|
e2614b8a21 | ||
|
|
3aaa2f5326 | ||
|
|
617518389a | ||
|
|
123920d0bc | ||
|
|
e3a0ce5d63 | ||
|
|
cabaed9ed2 | ||
|
|
882f92e732 | ||
|
|
6f5824cb21 | ||
|
|
8e271fd719 | ||
|
|
0b75aa5390 | ||
|
|
d2686ce75b | ||
|
|
21a09e7431 | ||
|
|
97f8588202 | ||
|
|
c916fb0513 | ||
|
|
57adcc0b03 | ||
|
|
4ff70db668 | ||
|
|
8263bf9a9a | ||
|
|
482116db9f | ||
|
|
6f4e6fafa9 | ||
|
|
6d791cabd1 | ||
|
|
4e2291f52d | ||
|
|
88edc412e3 | ||
|
|
67a7ccf3ce | ||
|
|
8a79efab4b | ||
|
|
7d9f3f23dd | ||
|
|
9858d9d968 | ||
|
|
32f3b64a89 | ||
|
|
7e1b6b3c2a | ||
|
|
03662be150 | ||
|
|
a31e9d3afe | ||
|
|
a0484793c3 | ||
|
|
89229d9b58 | ||
|
|
bfa511cb65 | ||
|
|
40769998c6 | ||
|
|
a05c47e499 | ||
|
|
b17ef34042 | ||
|
|
3e91a56c19 | ||
|
|
8f5d8e0474 | ||
|
|
13c4420f42 | ||
|
|
a26698b88e | ||
|
|
5918bb03f7 | ||
|
|
e684eb32d0 | ||
|
|
81e5f71172 | ||
|
|
6b5276398e | ||
|
|
645869e6ff | ||
|
|
76be35aaea | ||
|
|
ade83cbab3 | ||
|
|
1da14b7ec3 | ||
|
|
925387550e | ||
|
|
9dc1509cf5 | ||
|
|
31c95deb88 | ||
|
|
ff48634cbb | ||
|
|
e25b98b620 | ||
|
|
068df9f815 | ||
|
|
d0dbfec052 | ||
|
|
1ed2230e4c | ||
|
|
eaa04abe29 | ||
|
|
f31612376a | ||
|
|
b1721b79ce | ||
|
|
7851adb49a | ||
|
|
88ce11f03d | ||
|
|
2b4eae5f84 | ||
|
|
65db9cea25 | ||
|
|
2ed86760d7 | ||
|
|
b09eade3e4 | ||
|
|
4ecce242a8 | ||
|
|
67c5cec505 | ||
|
|
6238fd9d9e | ||
|
|
05b6fce03d | ||
|
|
2a4eec245a | ||
|
|
3b66920342 | ||
|
|
0e86cde64b | ||
|
|
9989ccbb02 | ||
|
|
4802d719ed | ||
|
|
6bb22c703a | ||
|
|
50421b3d7d | ||
|
|
b76129c1dd | ||
|
|
5c50a46822 | ||
|
|
5d0b5c8d2a | ||
|
|
1a40c04b72 | ||
|
|
408c89df03 | ||
|
|
da1d8da355 | ||
|
|
56f3ac15d8 | ||
|
|
4bfe2ea572 | ||
|
|
5cbbb5d59e | ||
|
|
ae31ff1c48 | ||
|
|
ac51c27500 | ||
|
|
9a00b3bdbd | ||
|
|
25f0b26a17 | ||
|
|
ae8dd2b2bc | ||
|
|
0f28074e5a | ||
|
|
d126c8030f | ||
|
|
246d3b4075 | ||
|
|
a358f77774 | ||
|
|
a998a8a2da | ||
|
|
dc178984ae | ||
|
|
7f5e761c04 | ||
|
|
3a8a467edb | ||
|
|
076527bb88 | ||
|
|
f73976946e | ||
|
|
b7e9942681 | ||
|
|
9bf2682218 | ||
|
|
26277cfcf3 | ||
|
|
973ef33eb5 | ||
|
|
8717148d9b | ||
|
|
ce9626fead | ||
|
|
d404a03947 | ||
|
|
8eb93004f7 | ||
|
|
21c86bd649 | ||
|
|
6d9d730759 | ||
|
|
410a58b3de | ||
|
|
c99cd74a15 | ||
|
|
41fc3be339 | ||
|
|
36997d9788 | ||
|
|
8534390627 | ||
|
|
22ef342ac7 | ||
|
|
df134c7c5b | ||
|
|
5ea6080a6e | ||
|
|
c5d21b2461 | ||
|
|
3db446e35a | ||
|
|
15e3b7f218 | ||
|
|
f9be137cf5 | ||
|
|
723c64ba73 | ||
|
|
aa787e4030 | ||
|
|
2175784142 | ||
|
|
f20370c5ef | ||
|
|
cb49055aac | ||
|
|
e057e1dfe7 | ||
|
|
78b438f6cf | ||
|
|
7b3316dc31 | ||
|
|
5c0f8ee44f | ||
|
|
7d64bf78cf | ||
|
|
28d4942d6c | ||
|
|
1ea2ba0dea | ||
|
|
b997d1eb87 | ||
|
|
f8cd424559 | ||
|
|
ec6505d747 | ||
|
|
915e0347b0 | ||
|
|
593626f502 | ||
|
|
d512919f5c | ||
|
|
71aa8c5e1c | ||
|
|
d193a14b8f | ||
|
|
90e1b9cb3d | ||
|
|
97ddfcfc7c | ||
|
|
16365bfca3 | ||
|
|
2010b1b6e8 | ||
|
|
77dc8e7966 | ||
|
|
187e1a324a | ||
|
|
1fd73af744 | ||
|
|
7d41f63c5d | ||
|
|
6db52930d8 | ||
|
|
6192af5cf0 | ||
|
|
8e7c8a6482 | ||
|
|
b179514273 | ||
|
|
b6bd408f19 | ||
|
|
0b86b1baca | ||
|
|
447272aa4d | ||
|
|
07c815f35f | ||
|
|
81397ba3ee | ||
|
|
f0487451f7 | ||
|
|
e5a1189bec | ||
|
|
1d37c0143f | ||
|
|
e451639ff6 | ||
|
|
f598da7c81 | ||
|
|
5e0f4eb580 | ||
|
|
632c08f7e0 | ||
|
|
cf1e3dc8ea | ||
|
|
922375c3ba | ||
|
|
a6fe79f29d | ||
|
|
18fc2b5695 | ||
|
|
2cd42dbb10 | ||
|
|
e7d404a30a | ||
|
|
8365d6bdb7 | ||
|
|
7cbb6c4de9 | ||
|
|
b602a0dcb0 | ||
|
|
26adbc6282 | ||
|
|
861aa9e08a | ||
|
|
c1a82dc9e5 | ||
|
|
b1242bc56c | ||
|
|
af1f98e188 | ||
|
|
e5566b31d5 | ||
|
|
a6794c3606 | ||
|
|
19db7e2989 | ||
|
|
d23b5d8f2f | ||
|
|
01b84a9751 | ||
|
|
3aa6f97420 | ||
|
|
63393fa503 | ||
|
|
921f8edde8 | ||
|
|
5853b80d25 | ||
|
|
8301d3b13f | ||
|
|
a26a29f33b | ||
|
|
a074ffa880 | ||
|
|
e526a0e282 | ||
|
|
a2abc879c9 | ||
|
|
748c74ba52 | ||
|
|
d0478ec3b8 | ||
|
|
713330cbf6 | ||
|
|
0581000990 | ||
|
|
7bd0e977bf | ||
|
|
3814db02eb | ||
|
|
55c0770280 | ||
|
|
5f821b93a5 | ||
|
|
ef9d126254 | ||
|
|
83ed93fbb6 | ||
|
|
31db0e7c79 | ||
|
|
2c4eb371a6 | ||
|
|
ca8c827492 | ||
|
|
eb66b749b2 | ||
|
|
083a954b5d | ||
|
|
b741ae143c | ||
|
|
f54838ab56 | ||
|
|
03f7bfbbde | ||
|
|
826f45ad60 | ||
|
|
f20238fa3e | ||
|
|
9b8d6fe411 | ||
|
|
96ede2fcf9 | ||
|
|
06e094b5fc | ||
|
|
7d09440579 | ||
|
|
4e63ba3ac6 | ||
|
|
de17367a36 | ||
|
|
f35b19eac3 | ||
|
|
7db66b0916 | ||
|
|
76f28de7eb | ||
|
|
a66d9f8e8e | ||
|
|
417180e6ba | ||
|
|
ffd41703de | ||
|
|
ca3581d055 | ||
|
|
ed495dc846 | ||
|
|
97ab7bd36b | ||
|
|
710d2452b7 | ||
|
|
5a274176d3 | ||
|
|
ef6480803b | ||
|
|
199b8dd0e3 | ||
|
|
6a6c560375 | ||
|
|
179e2d2c74 | ||
|
|
591b5917a9 | ||
|
|
0328874018 | ||
|
|
7855a59843 | ||
|
|
494bbf0124 | ||
|
|
f3f26d2c78 | ||
|
|
d259c2e36a | ||
|
|
f39422afd8 | ||
|
|
5a402a5709 | ||
|
|
4d0fa85a75 | ||
|
|
47f867b77a | ||
|
|
1d1c975da1 | ||
|
|
695e2bcfbc |
3
.github/helper/.flake8_strict
vendored
3
.github/helper/.flake8_strict
vendored
@@ -66,7 +66,8 @@ ignore =
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B023
|
||||
B023,
|
||||
B028
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
|
||||
99
.github/helper/documentation.py
vendored
99
.github/helper/documentation.py
vendored
@@ -3,52 +3,71 @@ import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
docs_repos = [
|
||||
"frappe_docs",
|
||||
"erpnext_documentation",
|
||||
WEBSITE_REPOS = [
|
||||
"erpnext_com",
|
||||
"frappe_io",
|
||||
]
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
def uri_validator(x):
|
||||
result = urlparse(x)
|
||||
return all([result.scheme, result.netloc, result.path])
|
||||
|
||||
def docs_link_exists(body):
|
||||
for line in body.splitlines():
|
||||
for word in line.split():
|
||||
if word.startswith('http') and uri_validator(word):
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
elif parsed_url.netloc == "docs.erpnext.com":
|
||||
return True
|
||||
def is_valid_url(url: str) -> bool:
|
||||
parts = urlparse(url)
|
||||
return all((parts.scheme, parts.netloc, parts.path))
|
||||
|
||||
|
||||
def is_documentation_link(word: str) -> bool:
|
||||
if not word.startswith("http") or not is_valid_url(word):
|
||||
return False
|
||||
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
|
||||
return True
|
||||
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split("/")
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_documentation_link(body: str) -> bool:
|
||||
return any(
|
||||
is_documentation_link(word)
|
||||
for line in body.splitlines()
|
||||
for word in line.split()
|
||||
)
|
||||
|
||||
|
||||
def check_pull_request(number: str) -> "tuple[int, str]":
|
||||
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
|
||||
if not response.ok:
|
||||
return 1, "Pull Request Not Found! ⚠️"
|
||||
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (
|
||||
not title.startswith("feat")
|
||||
or not head_sha
|
||||
or "no-docs" in body
|
||||
or "backport" in body
|
||||
):
|
||||
return 0, "Skipping documentation checks... 🏃"
|
||||
|
||||
if contains_documentation_link(body):
|
||||
return 0, "Documentation Link Found. You're Awesome! 🎉"
|
||||
|
||||
return 1, "Documentation Link Not Found! ⚠️"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pr = sys.argv[1]
|
||||
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
|
||||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (title.startswith("feat")
|
||||
and head_sha
|
||||
and "no-docs" not in body
|
||||
and "backport" not in body
|
||||
):
|
||||
if docs_link_exists(body):
|
||||
print("Documentation Link Found. You're Awesome! 🎉")
|
||||
|
||||
else:
|
||||
print("Documentation Link Not Found! ⚠️")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print("Skipping documentation checks... 🏃")
|
||||
exit_code, message = check_pull_request(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(exit_code)
|
||||
|
||||
33
.github/helper/install.sh
vendored
33
.github/helper/install.sh
vendored
@@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
|
||||
frappebranch=${FRAPPE_BRANCH:-$githubbranch}
|
||||
|
||||
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
|
||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||
@@ -24,15 +25,14 @@ fi
|
||||
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
||||
fi
|
||||
|
||||
if [ "$DB" == "postgres" ];then
|
||||
@@ -42,12 +42,17 @@ fi
|
||||
|
||||
|
||||
install_whktml() {
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
if [ "$(lsb_release -rs)" = "22.04" ]; then
|
||||
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
||||
sudo apt install /tmp/wkhtmltox.deb
|
||||
else
|
||||
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
install_whktml &
|
||||
wkpid=$!
|
||||
|
||||
|
||||
cd ~/frappe-bench || exit
|
||||
|
||||
@@ -56,11 +61,13 @@ sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app payments
|
||||
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
||||
wait $wkpid
|
||||
|
||||
bench start &> bench_run_logs.txt &
|
||||
CI=Yes bench build --app frappe &
|
||||
bench --site test_site reinstall --yes
|
||||
|
||||
4
.github/helper/site_config_mariadb.json
vendored
4
.github/helper/site_config_mariadb.json
vendored
@@ -9,8 +9,8 @@
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"root_login": "root",
|
||||
"root_password": "travis",
|
||||
"root_password": "root",
|
||||
"host_name": "http://test_site:8000",
|
||||
"install_apps": ["erpnext"],
|
||||
"install_apps": ["payments", "erpnext"],
|
||||
"throttle_user_limit": 100
|
||||
}
|
||||
|
||||
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
@@ -28,4 +28,4 @@ jobs:
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
run: npx semantic-release
|
||||
|
||||
9
.github/workflows/server-tests-mariadb.yml
vendored
9
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths-ignore:
|
||||
@@ -16,12 +15,12 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
user:
|
||||
description: 'user'
|
||||
description: 'Frappe Framework repository user (add your username for forks)'
|
||||
required: true
|
||||
default: 'frappe'
|
||||
type: string
|
||||
branch:
|
||||
description: 'Branch name'
|
||||
description: 'Frappe Framework branch'
|
||||
default: 'develop'
|
||||
required: false
|
||||
type: string
|
||||
@@ -45,9 +44,9 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
@@ -32,8 +32,8 @@ repos:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.1
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
25
CODEOWNERS
25
CODEOWNERS
@@ -3,26 +3,23 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
|
||||
|
||||
.github/ @ankush
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @ankush
|
||||
|
||||
@@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
|
||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
|
||||
@@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
if ancestors and not allow_independent_account_creation:
|
||||
for ancestor in ancestors:
|
||||
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
|
||||
old_name = frappe.db.get_value(
|
||||
"Account",
|
||||
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ def create_charts(
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
@@ -95,7 +96,17 @@ def identify_is_group(child):
|
||||
is_group = child.get("is_group")
|
||||
elif len(
|
||||
set(child.keys())
|
||||
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
|
||||
- set(
|
||||
[
|
||||
"account_name",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
]
|
||||
)
|
||||
):
|
||||
is_group = 1
|
||||
else:
|
||||
@@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
|
||||
"root_type",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
],
|
||||
order_by="lft, rgt",
|
||||
)
|
||||
@@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
continue
|
||||
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
{
|
||||
"country_code": "de",
|
||||
"name": "SKR03 mit Kontonummern",
|
||||
"tree": {
|
||||
"Aktiva": {
|
||||
"is_group": 1,
|
||||
"country_code": "de",
|
||||
"name": "SKR03 mit Kontonummern",
|
||||
"tree": {
|
||||
"Aktiva": {
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"A - Anlagevermögen": {
|
||||
"is_group": 1,
|
||||
"EDV-Software": {
|
||||
"account_number": "0027",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Gesch\u00e4ftsausstattung": {
|
||||
"account_number": "0410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"B\u00fcroeinrichtung": {
|
||||
"account_number": "0420",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "0565"
|
||||
},
|
||||
"Maschinen": {
|
||||
"account_number": "0210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Betriebsausstattung": {
|
||||
"account_number": "0400",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Ladeneinrichtung": {
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
"A - Anlagevermögen": {
|
||||
"is_group": 1,
|
||||
"EDV-Software": {
|
||||
"account_number": "0027",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Geschäftsausstattung": {
|
||||
"account_number": "0410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Büroeinrichtung": {
|
||||
"account_number": "0420",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "0565"
|
||||
},
|
||||
"Maschinen": {
|
||||
"account_number": "0210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Betriebsausstattung": {
|
||||
"account_number": "0400",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Ladeneinrichtung": {
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
@@ -60,36 +60,46 @@
|
||||
"Durchlaufende Posten": {
|
||||
"account_number": "1590"
|
||||
},
|
||||
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
|
||||
"Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
|
||||
"account_number": "1371"
|
||||
},
|
||||
"Abziehbare Vorsteuer": {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"Abziehbare Vorsteuer 7%": {
|
||||
"account_number": "1571"
|
||||
"Abziehbare Vorsteuer 7 %": {
|
||||
"account_number": "1571",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 7.0
|
||||
},
|
||||
"Abziehbare Vorsteuer 19%": {
|
||||
"account_number": "1576"
|
||||
"Abziehbare Vorsteuer 19 %": {
|
||||
"account_number": "1576",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
|
||||
"account_number": "1577"
|
||||
},
|
||||
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
|
||||
"account_number": "3120"
|
||||
"Abziehbare Vorsteuer nach § 13b UStG 19 %": {
|
||||
"account_number": "1577",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"III. Wertpapiere": {
|
||||
"is_group": 1
|
||||
"is_group": 1,
|
||||
"Anteile an verbundenen Unternehmen (Umlaufvermögen)": {
|
||||
"account_number": "1340"
|
||||
},
|
||||
"Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": {
|
||||
"account_number": "1344"
|
||||
},
|
||||
"Sonstige Wertpapiere": {
|
||||
"account_number": "1348"
|
||||
}
|
||||
},
|
||||
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
|
||||
"is_group": 1,
|
||||
"Kasse": {
|
||||
"account_type": "Cash",
|
||||
"is_group": 1,
|
||||
"account_type": "Cash",
|
||||
"Kasse": {
|
||||
"is_group": 1,
|
||||
"account_number": "1000",
|
||||
"account_type": "Cash"
|
||||
}
|
||||
@@ -111,21 +121,21 @@
|
||||
"C - Rechnungsabgrenzungsposten": {
|
||||
"is_group": 1,
|
||||
"Aktive Rechnungsabgrenzung": {
|
||||
"account_number": "0980"
|
||||
"account_number": "0980"
|
||||
}
|
||||
},
|
||||
"D - Aktive latente Steuern": {
|
||||
"is_group": 1,
|
||||
"Aktive latente Steuern": {
|
||||
"account_number": "0983"
|
||||
"account_number": "0983"
|
||||
}
|
||||
},
|
||||
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
|
||||
"is_group": 1
|
||||
}
|
||||
},
|
||||
"Passiva": {
|
||||
"is_group": 1,
|
||||
},
|
||||
"Passiva": {
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"A. Eigenkapital": {
|
||||
"is_group": 1,
|
||||
@@ -200,26 +210,32 @@
|
||||
},
|
||||
"Umsatzsteuer": {
|
||||
"is_group": 1,
|
||||
"account_type": "Tax",
|
||||
"Umsatzsteuer 7%": {
|
||||
"account_number": "1771"
|
||||
"Umsatzsteuer 7 %": {
|
||||
"account_number": "1771",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 7.0
|
||||
},
|
||||
"Umsatzsteuer 19%": {
|
||||
"account_number": "1776"
|
||||
"Umsatzsteuer 19 %": {
|
||||
"account_number": "1776",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Umsatzsteuer-Vorauszahlung": {
|
||||
"account_number": "1780"
|
||||
"account_number": "1780",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Umsatzsteuer-Vorauszahlung 1/11": {
|
||||
"account_number": "1781"
|
||||
},
|
||||
"Umsatzsteuer \u00a7 13b UStG 19%": {
|
||||
"account_number": "1787"
|
||||
"Umsatzsteuer nach § 13b UStG 19 %": {
|
||||
"account_number": "1787",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": 19.0
|
||||
},
|
||||
"Umsatzsteuer Vorjahr": {
|
||||
"account_number": "1790"
|
||||
},
|
||||
"Umsatzsteuer fr\u00fchere Jahre": {
|
||||
"Umsatzsteuer frühere Jahre": {
|
||||
"account_number": "1791"
|
||||
}
|
||||
}
|
||||
@@ -234,44 +250,56 @@
|
||||
"E. Passive latente Steuern": {
|
||||
"is_group": 1
|
||||
}
|
||||
},
|
||||
"Erl\u00f6se u. Ertr\u00e4ge 2/8": {
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Erl\u00f6skonten 8": {
|
||||
},
|
||||
"Erlöse u. Erträge 2/8": {
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Erlöskonten 8": {
|
||||
"is_group": 1,
|
||||
"Erl\u00f6se": {
|
||||
"account_number": "8200",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erl\u00f6se USt. 19%": {
|
||||
"account_number": "8400",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erl\u00f6se USt. 7%": {
|
||||
"account_number": "8300",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
},
|
||||
"Ertragskonten 2": {
|
||||
"is_group": 1,
|
||||
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
|
||||
"account_number": "2650",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Au\u00dferordentliche Ertr\u00e4ge": {
|
||||
"account_number": "2500",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sonstige Ertr\u00e4ge": {
|
||||
"account_number": "2700",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Aufwendungen 2/4": {
|
||||
"is_group": 1,
|
||||
"Erlöse": {
|
||||
"account_number": "8200",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erlöse USt. 19 %": {
|
||||
"account_number": "8400",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Erlöse USt. 7 %": {
|
||||
"account_number": "8300",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
},
|
||||
"Ertragskonten 2": {
|
||||
"is_group": 1,
|
||||
"sonstige Zinsen und ähnliche Erträge": {
|
||||
"account_number": "2650",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Außerordentliche Erträge": {
|
||||
"account_number": "2500",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Sonstige Erträge": {
|
||||
"account_number": "2700",
|
||||
"account_type": "Income Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Aufwendungen 2/4": {
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Fremdleistungen": {
|
||||
"account_number": "3100",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdleistungen ohne Vorsteuer": {
|
||||
"account_number": "3109",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": {
|
||||
"account_number": "3120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Wareneingang": {
|
||||
"account_number": "3200"
|
||||
},
|
||||
@@ -298,234 +326,234 @@
|
||||
"Gegenkonto 4996-4998": {
|
||||
"account_number": "4999"
|
||||
},
|
||||
"Abschreibungen": {
|
||||
"is_group": 1,
|
||||
"Abschreibungen": {
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
"account_type": "Depreciation"
|
||||
"account_number": "4831",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Kfz": {
|
||||
"account_number": "4832",
|
||||
"account_type": "Depreciation"
|
||||
"account_number": "4832",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Sofortabschreibung GWG": {
|
||||
"account_number": "4855",
|
||||
"account_type": "Expense Account"
|
||||
"account_number": "4855",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Kfz-Kosten": {
|
||||
"is_group": 1,
|
||||
"Kfz-Steuer": {
|
||||
"account_number": "4510",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Versicherungen": {
|
||||
"account_number": "4520",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"laufende Kfz-Betriebskosten": {
|
||||
"account_number": "4530",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Reparaturen": {
|
||||
"account_number": "4540",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdfahrzeuge": {
|
||||
"account_number": "4570",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Kfz-Kosten": {
|
||||
"account_number": "4580",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Personalkosten": {
|
||||
"is_group": 1,
|
||||
"Geh\u00e4lter": {
|
||||
"account_number": "4120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"gesetzliche soziale Aufwendungen": {
|
||||
"account_number": "4130",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufwendungen f\u00fcr Altersvorsorge": {
|
||||
"account_number": "4165",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verm\u00f6genswirksame Leistungen": {
|
||||
"account_number": "4170",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aushilfsl\u00f6hne": {
|
||||
"account_number": "4190",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Raumkosten": {
|
||||
"is_group": 1,
|
||||
"Miete und Nebenkosten": {
|
||||
"account_number": "4210",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
|
||||
"account_number": "4240",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reinigung": {
|
||||
"account_number": "4250",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Reparatur/Instandhaltung": {
|
||||
"is_group": 1,
|
||||
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
|
||||
"account_number": "4805",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Versicherungsbeitr\u00e4ge": {
|
||||
"is_group": 1,
|
||||
"Versicherungen": {
|
||||
"account_number": "4360",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Beitr\u00e4ge": {
|
||||
"account_number": "4380",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Ausgaben": {
|
||||
"account_number": "4390",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
|
||||
"account_number": "4396",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Werbe-/Reisekosten": {
|
||||
"is_group": 1,
|
||||
"Werbekosten": {
|
||||
"account_number": "4610",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufmerksamkeiten": {
|
||||
"account_number": "4653",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
|
||||
"account_number": "4665",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reisekosten Unternehmer": {
|
||||
"account_number": "4670",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"verschiedene Kosten": {
|
||||
"is_group": 1,
|
||||
"Porto": {
|
||||
"account_number": "4910",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telekom": {
|
||||
"account_number": "4920",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Mobilfunk D2": {
|
||||
"account_number": "4921",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Internet": {
|
||||
"account_number": "4922",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"B\u00fcrobedarf": {
|
||||
"account_number": "4930",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zeitschriften, B\u00fccher": {
|
||||
"account_number": "4940",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fortbildungskosten": {
|
||||
"account_number": "4945",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Buchf\u00fchrungskosten": {
|
||||
"account_number": "4955",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Abschlu\u00df- u. Pr\u00fcfungskosten": {
|
||||
"account_number": "4957",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Nebenkosten des Geldverkehrs": {
|
||||
"account_number": "4970",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Werkzeuge und Kleinger\u00e4te": {
|
||||
"account_number": "4985",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Zinsaufwendungen": {
|
||||
"is_group": 1,
|
||||
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2110",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
|
||||
"account_number": "2121",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anfangsbestand 9": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Saldenvortragskonten": {
|
||||
"is_group": 1,
|
||||
"Saldenvortrag Sachkonten": {
|
||||
"account_number": "9000"
|
||||
},
|
||||
"Saldenvortr\u00e4ge Debitoren": {
|
||||
"account_number": "9008"
|
||||
},
|
||||
"Saldenvortr\u00e4ge Kreditoren": {
|
||||
"account_number": "9009"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Privatkonten 1": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Privatentnahmen/-einlagen": {
|
||||
"is_group": 1,
|
||||
"Privatentnahme allgemein": {
|
||||
"account_number": "1800"
|
||||
},
|
||||
"Privatsteuern": {
|
||||
"account_number": "1810"
|
||||
},
|
||||
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
|
||||
"account_number": "1820"
|
||||
},
|
||||
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
|
||||
"account_number": "1830"
|
||||
},
|
||||
"Au\u00dfergew\u00f6hnliche Belastungen": {
|
||||
"account_number": "1850"
|
||||
},
|
||||
"Privateinlagen": {
|
||||
"account_number": "1890"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Kfz-Kosten": {
|
||||
"is_group": 1,
|
||||
"Kfz-Steuer": {
|
||||
"account_number": "4510",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Versicherungen": {
|
||||
"account_number": "4520",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"laufende Kfz-Betriebskosten": {
|
||||
"account_number": "4530",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Kfz-Reparaturen": {
|
||||
"account_number": "4540",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fremdfahrzeuge": {
|
||||
"account_number": "4570",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Kfz-Kosten": {
|
||||
"account_number": "4580",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Personalkosten": {
|
||||
"is_group": 1,
|
||||
"Gehälter": {
|
||||
"account_number": "4120",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"gesetzliche soziale Aufwendungen": {
|
||||
"account_number": "4130",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufwendungen für Altersvorsorge": {
|
||||
"account_number": "4165",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Vermögenswirksame Leistungen": {
|
||||
"account_number": "4170",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aushilfslöhne": {
|
||||
"account_number": "4190",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Raumkosten": {
|
||||
"is_group": 1,
|
||||
"Miete und Nebenkosten": {
|
||||
"account_number": "4210",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
|
||||
"account_number": "4240",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reinigung": {
|
||||
"account_number": "4250",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Reparatur/Instandhaltung": {
|
||||
"is_group": 1,
|
||||
"Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
|
||||
"account_number": "4805",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Versicherungsbeiträge": {
|
||||
"is_group": 1,
|
||||
"Versicherungen": {
|
||||
"account_number": "4360",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Beiträge": {
|
||||
"account_number": "4380",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"sonstige Ausgaben": {
|
||||
"account_number": "4390",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
|
||||
"account_number": "4396",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Werbe-/Reisekosten": {
|
||||
"is_group": 1,
|
||||
"Werbekosten": {
|
||||
"account_number": "4610",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Aufmerksamkeiten": {
|
||||
"account_number": "4653",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
|
||||
"account_number": "4665",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Reisekosten Unternehmer": {
|
||||
"account_number": "4670",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"verschiedene Kosten": {
|
||||
"is_group": 1,
|
||||
"Porto": {
|
||||
"account_number": "4910",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telekom": {
|
||||
"account_number": "4920",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Mobilfunk D2": {
|
||||
"account_number": "4921",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Internet": {
|
||||
"account_number": "4922",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Bürobedarf": {
|
||||
"account_number": "4930",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zeitschriften, Bücher": {
|
||||
"account_number": "4940",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Fortbildungskosten": {
|
||||
"account_number": "4945",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Buchführungskosten": {
|
||||
"account_number": "4955",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Abschluß- u. Prüfungskosten": {
|
||||
"account_number": "4957",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Nebenkosten des Geldverkehrs": {
|
||||
"account_number": "4970",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Werkzeuge und Kleingeräte": {
|
||||
"account_number": "4985",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
},
|
||||
"Zinsaufwendungen": {
|
||||
"is_group": 1,
|
||||
"Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2110",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Zinsaufwendungen für KFZ Finanzierung": {
|
||||
"account_number": "2121",
|
||||
"account_type": "Expense Account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anfangsbestand 9": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Saldenvortragskonten": {
|
||||
"is_group": 1,
|
||||
"Saldenvortrag Sachkonten": {
|
||||
"account_number": "9000"
|
||||
},
|
||||
"Saldenvorträge Debitoren": {
|
||||
"account_number": "9008"
|
||||
},
|
||||
"Saldenvorträge Kreditoren": {
|
||||
"account_number": "9009"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Privatkonten 1": {
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Privatentnahmen/-einlagen": {
|
||||
"is_group": 1,
|
||||
"Privatentnahme allgemein": {
|
||||
"account_number": "1800"
|
||||
},
|
||||
"Privatsteuern": {
|
||||
"account_number": "1810"
|
||||
},
|
||||
"Sonderausgaben beschränkt abzugsfähig": {
|
||||
"account_number": "1820"
|
||||
},
|
||||
"Sonderausgaben unbeschränkt abzugsfähig": {
|
||||
"account_number": "1830"
|
||||
},
|
||||
"Außergewöhnliche Belastungen": {
|
||||
"account_number": "1850"
|
||||
},
|
||||
"Privateinlagen": {
|
||||
"account_number": "1890"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Account Closing Balance", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-02-21 15:20:59.586811",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"closing_date",
|
||||
"account",
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"project",
|
||||
"company",
|
||||
"finance_book",
|
||||
"period_closing_voucher",
|
||||
"is_period_closing_voucher_entry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "closing_date",
|
||||
"fieldtype": "Date",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Closing Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account",
|
||||
"oldfieldname": "account",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Cost Center",
|
||||
"oldfieldname": "cost_center",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount",
|
||||
"oldfieldname": "debit",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount",
|
||||
"oldfieldname": "credit",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_account_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Account Currency",
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_account_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Account Currency",
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Company",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_closing_voucher",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Period Closing Voucher",
|
||||
"options": "Period Closing Voucher",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-06 08:56:36.393237",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"export": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
pass
|
||||
|
||||
|
||||
def make_closing_entries(closing_entries, voucher_name):
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
company = closing_entries[0].get("company")
|
||||
closing_date = closing_entries[0].get("closing_date")
|
||||
|
||||
previous_closing_entries = get_previous_closing_entries(
|
||||
company, closing_date, accounting_dimensions
|
||||
)
|
||||
combined_entries = closing_entries + previous_closing_entries
|
||||
|
||||
merged_entries = aggregate_with_last_account_closing_balance(
|
||||
combined_entries, accounting_dimensions
|
||||
)
|
||||
|
||||
for key, value in merged_entries.items():
|
||||
cle = frappe.new_doc("Account Closing Balance")
|
||||
cle.update(value)
|
||||
cle.update(value["dimensions"])
|
||||
cle.update(
|
||||
{
|
||||
"period_closing_voucher": voucher_name,
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
cle.submit()
|
||||
|
||||
|
||||
def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
|
||||
merged_entries = {}
|
||||
for entry in entries:
|
||||
key, key_values = generate_key(entry, accounting_dimensions)
|
||||
merged_entries.setdefault(
|
||||
key,
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
},
|
||||
)
|
||||
|
||||
merged_entries[key]["dimensions"] = key_values
|
||||
merged_entries[key]["debit"] += entry.get("debit")
|
||||
merged_entries[key]["credit"] += entry.get("credit")
|
||||
merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
|
||||
merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
|
||||
|
||||
return merged_entries
|
||||
|
||||
|
||||
def generate_key(entry, accounting_dimensions):
|
||||
key = [
|
||||
cstr(entry.get("account")),
|
||||
cstr(entry.get("account_currency")),
|
||||
cstr(entry.get("cost_center")),
|
||||
cstr(entry.get("project")),
|
||||
cstr(entry.get("finance_book")),
|
||||
cint(entry.get("is_period_closing_voucher_entry")),
|
||||
]
|
||||
|
||||
key_values = {
|
||||
"company": cstr(entry.get("company")),
|
||||
"account": cstr(entry.get("account")),
|
||||
"account_currency": cstr(entry.get("account_currency")),
|
||||
"cost_center": cstr(entry.get("cost_center")),
|
||||
"project": cstr(entry.get("project")),
|
||||
"finance_book": cstr(entry.get("finance_book")),
|
||||
"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
|
||||
}
|
||||
for dimension in accounting_dimensions:
|
||||
key.append(cstr(entry.get(dimension)))
|
||||
key_values[dimension] = cstr(entry.get(dimension))
|
||||
|
||||
return tuple(key), key_values
|
||||
|
||||
|
||||
def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = []
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
|
||||
fields=["name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if last_period_closing_voucher:
|
||||
account_closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
query = frappe.qb.from_(account_closing_balance).select(
|
||||
account_closing_balance.company,
|
||||
account_closing_balance.account,
|
||||
account_closing_balance.account_currency,
|
||||
account_closing_balance.debit,
|
||||
account_closing_balance.credit,
|
||||
account_closing_balance.debit_in_account_currency,
|
||||
account_closing_balance.credit_in_account_currency,
|
||||
account_closing_balance.cost_center,
|
||||
account_closing_balance.project,
|
||||
account_closing_balance.finance_book,
|
||||
account_closing_balance.is_period_closing_voucher_entry,
|
||||
)
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
query = query.select(account_closing_balance[dimension])
|
||||
|
||||
query = query.where(
|
||||
account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
|
||||
)
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestAccountClosingBalance(FrappeTestCase):
|
||||
pass
|
||||
@@ -31,6 +31,7 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"book_tax_discount_loss",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"column_break_12",
|
||||
@@ -56,7 +57,9 @@
|
||||
"acc_frozen_upto",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb"
|
||||
"report_settings_sb",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -347,6 +350,24 @@
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account "
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -354,7 +375,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-27 21:49:52.538655",
|
||||
"modified": "2023-03-28 09:50:20.375233",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
|
||||
response: response,
|
||||
}).then(() => {
|
||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,14 +37,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__('Get Payment Entries'), () =>
|
||||
frm.trigger("get_payment_entries")
|
||||
);
|
||||
|
||||
if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
|
||||
frm.add_custom_button(__('Get Payment Entries'), () =>
|
||||
frm.trigger("get_payment_entries")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
|
||||
}
|
||||
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
|
||||
},
|
||||
|
||||
update_clearance_date: function(frm) {
|
||||
|
||||
@@ -81,7 +81,7 @@ class BankClearance(Document):
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
loan_disbursements = (
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
@@ -90,17 +90,22 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_disbursement.clearance_date.isnull())
|
||||
|
||||
loan_disbursements = query.run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
@@ -113,16 +118,19 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.clearance_date.as_("clearance_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_repayment.clearance_date.isnull())
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
|
||||
@@ -18,16 +18,30 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
// Set default filter dates
|
||||
today = frappe.datetime.get_today()
|
||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||
frm.doc.bank_statement_to_date = today;
|
||||
frm.trigger('bank_account');
|
||||
},
|
||||
|
||||
filter_by_reference_date: function (frm) {
|
||||
if (frm.doc.filter_by_reference_date) {
|
||||
frm.set_value("bank_statement_from_date", "");
|
||||
frm.set_value("bank_statement_to_date", "");
|
||||
} else {
|
||||
frm.set_value("from_reference_date", "");
|
||||
frm.set_value("to_reference_date", "");
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.disable_save();
|
||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||
frm.trigger("make_reconciliation_tool")
|
||||
);
|
||||
frm.upload_statement_button = frm.page.set_secondary_action(
|
||||
__("Upload Bank Statement"),
|
||||
() =>
|
||||
|
||||
frm.add_custom_button(__("Upload Bank Statement"), () =>
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
|
||||
@@ -49,10 +63,26 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
frm.add_custom_button(__('Auto Reconcile'), function() {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
from_date: frm.doc.bank_statement_from_date,
|
||||
to_date: frm.doc.bank_statement_to_date,
|
||||
filter_by_reference_date: frm.doc.filter_by_reference_date,
|
||||
from_reference_date: frm.doc.from_reference_date,
|
||||
to_reference_date: frm.doc.to_reference_date,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
});
|
||||
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@@ -66,7 +96,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
r.account,
|
||||
"account_currency",
|
||||
(r) => {
|
||||
frm.currency = r.account_currency;
|
||||
frm.doc.account_currency = r.account_currency;
|
||||
frm.trigger("render_chart");
|
||||
}
|
||||
);
|
||||
@@ -132,19 +162,19 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
}
|
||||
},
|
||||
|
||||
render_chart: frappe.utils.debounce((frm) => {
|
||||
render_chart(frm) {
|
||||
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
|
||||
{
|
||||
$reconciliation_tool_cards: frm.get_field(
|
||||
"reconciliation_tool_cards"
|
||||
).$wrapper,
|
||||
bank_statement_closing_balance:
|
||||
frm.doc.bank_statement_closing_balance,
|
||||
frm.doc.bank_statement_closing_balance,
|
||||
cleared_balance: frm.cleared_balance,
|
||||
currency: frm.currency,
|
||||
currency: frm.doc.account_currency,
|
||||
}
|
||||
);
|
||||
}, 500),
|
||||
},
|
||||
|
||||
render(frm) {
|
||||
if (frm.doc.bank_account) {
|
||||
@@ -160,6 +190,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
).$wrapper,
|
||||
bank_statement_from_date: frm.doc.bank_statement_from_date,
|
||||
bank_statement_to_date: frm.doc.bank_statement_to_date,
|
||||
filter_by_reference_date: frm.doc.filter_by_reference_date,
|
||||
from_reference_date: frm.doc.from_reference_date,
|
||||
to_reference_date: frm.doc.to_reference_date,
|
||||
bank_statement_closing_balance:
|
||||
frm.doc.bank_statement_closing_balance,
|
||||
cards_manager: frm.cards_manager,
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
"column_break_1",
|
||||
"bank_statement_from_date",
|
||||
"bank_statement_to_date",
|
||||
"from_reference_date",
|
||||
"to_reference_date",
|
||||
"filter_by_reference_date",
|
||||
"column_break_2",
|
||||
"account_currency",
|
||||
"account_opening_balance",
|
||||
"bank_statement_closing_balance",
|
||||
"section_break_1",
|
||||
@@ -36,13 +40,13 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_account",
|
||||
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
|
||||
"fieldname": "bank_statement_from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_statement_from_date",
|
||||
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
|
||||
"fieldname": "bank_statement_to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date"
|
||||
@@ -56,7 +60,7 @@
|
||||
"fieldname": "account_opening_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Opening Balance",
|
||||
"options": "Currency",
|
||||
"options": "account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -64,7 +68,7 @@
|
||||
"fieldname": "bank_statement_closing_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Closing Balance",
|
||||
"options": "Currency"
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_1",
|
||||
@@ -81,14 +85,40 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "no_bank_transactions",
|
||||
"fieldtype": "HTML"
|
||||
"fieldtype": "HTML",
|
||||
"options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.filter_by_reference_date",
|
||||
"fieldname": "from_reference_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Reference Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.filter_by_reference_date",
|
||||
"fieldname": "to_reference_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Reference Date"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "filter_by_reference_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Filter by Reference Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Account Currency",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-21 11:13:49.831769",
|
||||
"modified": "2023-03-07 11:02:24.535714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
@@ -107,5 +137,6 @@
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
filters = []
|
||||
filters.append(["bank_account", "=", bank_account])
|
||||
filters.append(["docstatus", "=", 1])
|
||||
filters.append(["unallocated_amount", ">", 0])
|
||||
filters.append(["unallocated_amount", ">", 0.0])
|
||||
if to_date:
|
||||
filters.append(["date", "<=", to_date])
|
||||
if from_date:
|
||||
@@ -50,6 +50,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
"party",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="date",
|
||||
)
|
||||
return transactions
|
||||
|
||||
@@ -57,7 +58,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
@frappe.whitelist()
|
||||
def get_account_balance(bank_account, till_date):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.get_cached_value("Bank Account", bank_account, "account")
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict(
|
||||
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
||||
)
|
||||
@@ -65,7 +66,7 @@ def get_account_balance(bank_account, till_date):
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
|
||||
total_debit, total_credit = 0, 0
|
||||
total_debit, total_credit = 0.0, 0.0
|
||||
for d in data:
|
||||
total_debit += flt(d.debit)
|
||||
total_credit += flt(d.credit)
|
||||
@@ -130,10 +131,8 @@ def create_journal_entry_bts(
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
company_account = frappe.get_cached_value(
|
||||
"Bank Account", bank_transaction.bank_account, "account"
|
||||
)
|
||||
account_type = frappe.get_cached_value("Account", second_account, "account_type")
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
account_type = frappe.db.get_value("Account", second_account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (party_type and party):
|
||||
frappe.throw(
|
||||
@@ -146,10 +145,8 @@ def create_journal_entry_bts(
|
||||
accounts.append(
|
||||
{
|
||||
"account": second_account,
|
||||
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal
|
||||
if bank_transaction.withdrawal > 0
|
||||
else 0,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
}
|
||||
@@ -159,14 +156,12 @@ def create_journal_entry_bts(
|
||||
{
|
||||
"account": company_account,
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal
|
||||
if bank_transaction.withdrawal > 0
|
||||
else 0,
|
||||
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
}
|
||||
)
|
||||
|
||||
company = frappe.get_cached_value("Account", company_account, "company")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
@@ -186,16 +181,22 @@ def create_journal_entry_bts(
|
||||
journal_entry.insert()
|
||||
journal_entry.submit()
|
||||
|
||||
if bank_transaction.deposit > 0:
|
||||
if bank_transaction.deposit > 0.0:
|
||||
paid_amount = bank_transaction.deposit
|
||||
else:
|
||||
paid_amount = bank_transaction.withdrawal
|
||||
|
||||
vouchers = json.dumps(
|
||||
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Journal Entry",
|
||||
"payment_name": journal_entry.name,
|
||||
"amount": paid_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -219,12 +220,10 @@ def create_payment_entry_bts(
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
company_account = frappe.get_cached_value(
|
||||
"Bank Account", bank_transaction.bank_account, "account"
|
||||
)
|
||||
company = frappe.get_cached_value("Account", company_account, "company")
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
@@ -260,9 +259,89 @@ def create_payment_entry_bts(
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_reconcile_vouchers(
|
||||
bank_account,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
document_types = ["payment_entry", "journal_entry"]
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
matched_transaction = []
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
vouchers = []
|
||||
for r in linked_payments:
|
||||
vouchers.append(
|
||||
{
|
||||
"payment_doctype": r[1],
|
||||
"payment_name": r[2],
|
||||
"amount": r[4],
|
||||
}
|
||||
)
|
||||
transaction = frappe.get_doc("Bank Transaction", transaction.name)
|
||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
matched_trans = 0
|
||||
for voucher in vouchers:
|
||||
gl_entry = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
dict(
|
||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
||||
),
|
||||
["credit", "debit"],
|
||||
as_dict=1,
|
||||
)
|
||||
gl_amount, transaction_amount = (
|
||||
(gl_entry.credit, transaction.deposit)
|
||||
if gl_entry.credit > 0
|
||||
else (gl_entry.debit, transaction.withdrawal)
|
||||
)
|
||||
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
|
||||
transaction.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
matched_transaction.append(str(transaction.name))
|
||||
transaction.save()
|
||||
transaction.update_allocations()
|
||||
matched_transaction_len = len(set(matched_transaction))
|
||||
if matched_transaction_len == 0:
|
||||
frappe.msgprint(_("No matching references found for auto reconciliation"))
|
||||
elif matched_transaction_len == 1:
|
||||
frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
|
||||
else:
|
||||
frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
|
||||
|
||||
frappe.flags.auto_reconcile_vouchers = False
|
||||
|
||||
return frappe.get_doc("Bank Transaction", transaction.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -270,80 +349,88 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
# updated clear date of all the vouchers based on the bank transaction
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
|
||||
|
||||
if transaction.unallocated_amount == 0:
|
||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
||||
total_amount = 0
|
||||
for voucher in vouchers:
|
||||
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
|
||||
total_amount += get_paid_amount(
|
||||
frappe._dict(
|
||||
{
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
}
|
||||
),
|
||||
transaction.currency,
|
||||
company_account,
|
||||
)
|
||||
|
||||
if total_amount > transaction.unallocated_amount:
|
||||
frappe.throw(
|
||||
_(
|
||||
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
|
||||
)
|
||||
)
|
||||
account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
|
||||
|
||||
for voucher in vouchers:
|
||||
gl_entry = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
dict(
|
||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
||||
),
|
||||
["credit", "debit"],
|
||||
as_dict=1,
|
||||
)
|
||||
gl_amount, transaction_amount = (
|
||||
(gl_entry.credit, transaction.deposit)
|
||||
if gl_entry.credit > 0
|
||||
else (gl_entry.debit, transaction.withdrawal)
|
||||
)
|
||||
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
|
||||
|
||||
transaction.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_entry"].doctype,
|
||||
"payment_entry": voucher["payment_entry"].name,
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
|
||||
transaction.save()
|
||||
transaction.update_allocations()
|
||||
transaction.add_payment_entries(vouchers)
|
||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_payments(bank_transaction_name, document_types=None):
|
||||
def get_linked_payments(
|
||||
bank_transaction_name,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
# get all matching payments for a bank transaction
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
bank_account = frappe.db.get_values(
|
||||
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
||||
)[0]
|
||||
(account, company) = (bank_account.account, bank_account.company)
|
||||
matching = check_matching(account, company, transaction, document_types)
|
||||
return matching
|
||||
(gl_account, company) = (bank_account.account, bank_account.company)
|
||||
matching = check_matching(
|
||||
gl_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
return subtract_allocations(gl_account, matching)
|
||||
|
||||
|
||||
def check_matching(bank_account, company, transaction, document_types):
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher[1], voucher[2])
|
||||
amount = None
|
||||
for row in rows:
|
||||
if row["gl_account"] == gl_account:
|
||||
amount = row["total"]
|
||||
break
|
||||
|
||||
if amount:
|
||||
l = list(voucher)
|
||||
l[3] -= amount
|
||||
copied.append(tuple(l))
|
||||
else:
|
||||
copied.append(voucher)
|
||||
return copied
|
||||
|
||||
|
||||
def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
# combine all types of vouchers
|
||||
subquery = get_queries(bank_account, company, transaction, document_types)
|
||||
subquery = get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
)
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
|
||||
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||
"reference_no": transaction.reference_number,
|
||||
"party_type": transaction.party_type,
|
||||
"party": transaction.party,
|
||||
@@ -352,7 +439,9 @@ def check_matching(bank_account, company, transaction, document_types):
|
||||
|
||||
matching_vouchers = []
|
||||
|
||||
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
|
||||
matching_vouchers.extend(
|
||||
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
|
||||
)
|
||||
|
||||
for query in subquery:
|
||||
matching_vouchers.extend(
|
||||
@@ -361,14 +450,23 @@ def check_matching(bank_account, company, transaction, document_types):
|
||||
filters,
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
|
||||
|
||||
|
||||
def get_queries(bank_account, company, transaction, document_types):
|
||||
def get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
amount_condition = "=" if "exact_match" in document_types else "<="
|
||||
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
queries = []
|
||||
|
||||
# get matching queries from all the apps
|
||||
@@ -379,8 +477,13 @@ def get_queries(bank_account, company, transaction, document_types):
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
amount_condition,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
or []
|
||||
)
|
||||
@@ -389,43 +492,106 @@ def get_queries(bank_account, company, transaction, document_types):
|
||||
|
||||
|
||||
def get_matching_queries(
|
||||
bank_account, company, transaction, document_types, amount_condition, account_from_to
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
):
|
||||
queries = []
|
||||
if "payment_entry" in document_types:
|
||||
pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
|
||||
queries.extend([pe_amount_matching])
|
||||
query = get_pe_matching_query(
|
||||
exact_match,
|
||||
account_from_to,
|
||||
transaction,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
queries.append(query)
|
||||
|
||||
if "journal_entry" in document_types:
|
||||
je_amount_matching = get_je_matching_query(amount_condition, transaction)
|
||||
queries.extend([je_amount_matching])
|
||||
query = get_je_matching_query(
|
||||
exact_match,
|
||||
transaction,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0 and "sales_invoice" in document_types:
|
||||
si_amount_matching = get_si_matching_query(amount_condition)
|
||||
queries.extend([si_amount_matching])
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0:
|
||||
if transaction.withdrawal > 0.0:
|
||||
if "purchase_invoice" in document_types:
|
||||
pi_amount_matching = get_pi_matching_query(amount_condition)
|
||||
queries.extend([pi_amount_matching])
|
||||
query = get_pi_matching_query(exact_match)
|
||||
queries.append(query)
|
||||
|
||||
if "bank_transaction" in document_types:
|
||||
query = get_bt_matching_query(exact_match, transaction)
|
||||
queries.append(query)
|
||||
|
||||
return queries
|
||||
|
||||
|
||||
def get_loan_vouchers(bank_account, transaction, document_types, filters):
|
||||
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
|
||||
vouchers = []
|
||||
amount_condition = True if "exact_match" in document_types else False
|
||||
|
||||
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
|
||||
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
|
||||
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
|
||||
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
|
||||
|
||||
if transaction.deposit > 0 and "loan_repayment" in document_types:
|
||||
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
|
||||
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
|
||||
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_ld_matching_query(bank_account, amount_condition, filters):
|
||||
def get_bt_matching_query(exact_match, transaction):
|
||||
# get matching bank transaction query
|
||||
# find bank transactions in the same bank account with opposite sign
|
||||
# same bank account must have same company and currency
|
||||
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
||||
|
||||
return f"""
|
||||
|
||||
SELECT
|
||||
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1) AS rank,
|
||||
'Bank Transaction' AS doctype,
|
||||
name,
|
||||
unallocated_amount AS paid_amount,
|
||||
reference_number AS reference_no,
|
||||
date AS reference_date,
|
||||
party,
|
||||
party_type,
|
||||
date AS posting_date,
|
||||
currency
|
||||
FROM
|
||||
`tabBank Transaction`
|
||||
WHERE
|
||||
status != 'Reconciled'
|
||||
AND name != '{transaction.name}'
|
||||
AND bank_account = '{transaction.bank_account}'
|
||||
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
|
||||
|
||||
def get_ld_matching_query(bank_account, exact_match, filters):
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
||||
matching_party = loan_disbursement.applicant_type == filters.get(
|
||||
@@ -453,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
|
||||
.where(loan_disbursement.disbursement_account == bank_account)
|
||||
)
|
||||
|
||||
if amount_condition:
|
||||
if exact_match:
|
||||
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
||||
else:
|
||||
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
|
||||
query.where(loan_disbursement.disbursed_amount > 0.0)
|
||||
|
||||
vouchers = query.run(as_list=True)
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_lr_matching_query(bank_account, amount_condition, filters):
|
||||
def get_lr_matching_query(bank_account, exact_match, filters):
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
||||
matching_party = loan_repayment.applicant_type == filters.get(
|
||||
@@ -494,88 +660,129 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
if amount_condition:
|
||||
if exact_match:
|
||||
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
||||
else:
|
||||
query.where(loan_repayment.amount_paid <= filters.get("amount"))
|
||||
query.where(loan_repayment.amount_paid > 0.0)
|
||||
|
||||
vouchers = query.run()
|
||||
|
||||
return vouchers
|
||||
|
||||
|
||||
def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
||||
def get_pe_matching_query(
|
||||
exact_match,
|
||||
account_from_to,
|
||||
transaction,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
):
|
||||
# get matching payment entries query
|
||||
if transaction.deposit > 0:
|
||||
if transaction.deposit > 0.0:
|
||||
currency_field = "paid_to_account_currency as currency"
|
||||
else:
|
||||
currency_field = "paid_from_account_currency as currency"
|
||||
filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
|
||||
order_by = " posting_date"
|
||||
filter_by_reference_no = ""
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " reference_date"
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
SELECT
|
||||
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Payment Entry' as doctype,
|
||||
name,
|
||||
paid_amount,
|
||||
reference_no,
|
||||
reference_date,
|
||||
party,
|
||||
party_type,
|
||||
posting_date,
|
||||
{currency_field}
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
paid_amount {amount_condition} %(amount)s
|
||||
AND docstatus = 1
|
||||
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
||||
AND ifnull(clearance_date, '') = ""
|
||||
AND {account_from_to} = %(bank_account)s
|
||||
SELECT
|
||||
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Payment Entry' as doctype,
|
||||
name,
|
||||
paid_amount,
|
||||
reference_no,
|
||||
reference_date,
|
||||
party,
|
||||
party_type,
|
||||
posting_date,
|
||||
{currency_field}
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
docstatus = 1
|
||||
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
||||
AND ifnull(clearance_date, '') = ""
|
||||
AND {account_from_to} = %(bank_account)s
|
||||
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
{filter_by_date}
|
||||
{filter_by_reference_no}
|
||||
order by{order_by}
|
||||
"""
|
||||
|
||||
|
||||
def get_je_matching_query(amount_condition, transaction):
|
||||
def get_je_matching_query(
|
||||
exact_match,
|
||||
transaction,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
):
|
||||
# get matching journal entry query
|
||||
|
||||
# We have mapping at the bank level
|
||||
# So one bank could have both types of bank accounts like asset and liability
|
||||
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
|
||||
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
||||
order_by = " je.posting_date"
|
||||
filter_by_reference_no = ""
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " je.cheque_date"
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
|
||||
SELECT
|
||||
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1) AS rank ,
|
||||
'Journal Entry' as doctype,
|
||||
'Journal Entry' AS doctype,
|
||||
je.name,
|
||||
jea.{cr_or_dr}_in_account_currency as paid_amount,
|
||||
je.cheque_no as reference_no,
|
||||
je.cheque_date as reference_date,
|
||||
je.pay_to_recd_from as party,
|
||||
jea.{cr_or_dr}_in_account_currency AS paid_amount,
|
||||
je.cheque_no AS reference_no,
|
||||
je.cheque_date AS reference_date,
|
||||
je.pay_to_recd_from AS party,
|
||||
jea.party_type,
|
||||
je.posting_date,
|
||||
jea.account_currency as currency
|
||||
jea.account_currency AS currency
|
||||
FROM
|
||||
`tabJournal Entry Account` as jea
|
||||
`tabJournal Entry Account` AS jea
|
||||
JOIN
|
||||
`tabJournal Entry` as je
|
||||
`tabJournal Entry` AS je
|
||||
ON
|
||||
jea.parent = je.name
|
||||
WHERE
|
||||
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
||||
je.docstatus = 1
|
||||
AND je.voucher_type NOT IN ('Opening Entry')
|
||||
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
|
||||
AND jea.account = %(bank_account)s
|
||||
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
|
||||
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
AND je.docstatus = 1
|
||||
{filter_by_date}
|
||||
{filter_by_reference_no}
|
||||
order by {order_by}
|
||||
"""
|
||||
|
||||
|
||||
def get_si_matching_query(amount_condition):
|
||||
# get matchin sales invoice query
|
||||
def get_si_matching_query(exact_match):
|
||||
# get matching sales invoice query
|
||||
return f"""
|
||||
SELECT
|
||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Sales Invoice' as doctype,
|
||||
si.name,
|
||||
@@ -593,18 +800,20 @@ def get_si_matching_query(amount_condition):
|
||||
`tabSales Invoice` as si
|
||||
ON
|
||||
sip.parent = si.name
|
||||
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
||||
WHERE
|
||||
si.docstatus = 1
|
||||
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
||||
AND sip.account = %(bank_account)s
|
||||
AND sip.amount {amount_condition} %(amount)s
|
||||
AND si.docstatus = 1
|
||||
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
|
||||
|
||||
def get_pi_matching_query(amount_condition):
|
||||
# get matching purchase invoice query
|
||||
def get_pi_matching_query(exact_match):
|
||||
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||
return f"""
|
||||
SELECT
|
||||
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
||||
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||
+ 1 ) AS rank,
|
||||
'Purchase Invoice' as doctype,
|
||||
name,
|
||||
@@ -618,9 +827,9 @@ def get_pi_matching_query(amount_condition):
|
||||
FROM
|
||||
`tabPurchase Invoice`
|
||||
WHERE
|
||||
paid_amount {amount_condition} %(amount)s
|
||||
AND docstatus = 1
|
||||
docstatus = 1
|
||||
AND is_paid = 1
|
||||
AND ifnull(clearance_date, '') = ""
|
||||
AND cash_bank_account = %(bank_account)s
|
||||
AND cash_bank_account = %(bank_account)s
|
||||
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||
"""
|
||||
|
||||
@@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
bank_account: function(frm) {
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__('Unreconcile Transaction'), () => {
|
||||
frm.call('remove_payment_entries')
|
||||
.then( () => frm.refresh() );
|
||||
});
|
||||
},
|
||||
bank_account: function (frm) {
|
||||
set_bank_statement_filter(frm);
|
||||
},
|
||||
|
||||
@@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", {
|
||||
"Journal Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Bank Transaction",
|
||||
];
|
||||
}
|
||||
});
|
||||
@@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
|
||||
frappe
|
||||
.xcall(
|
||||
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
|
||||
{ doctype: cdt, docname: cdn }
|
||||
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
|
||||
)
|
||||
.then((e) => {
|
||||
if (e == "success") {
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
"currency",
|
||||
"section_break_10",
|
||||
"description",
|
||||
"section_break_14",
|
||||
"reference_number",
|
||||
"column_break_10",
|
||||
"transaction_id",
|
||||
"transaction_type",
|
||||
"section_break_14",
|
||||
"payment_entries",
|
||||
"section_break_18",
|
||||
"allocated_amount",
|
||||
@@ -190,11 +192,21 @@
|
||||
"label": "Withdrawal",
|
||||
"oldfieldname": "credit",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction Type",
|
||||
"length": 50
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-21 19:05:04.208222",
|
||||
"modified": "2022-05-29 18:36:50.475964",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -248,4 +260,4 @@
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
@@ -18,72 +15,135 @@ class BankTransaction(StatusUpdater):
|
||||
self.clear_linked_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
def on_update_after_submit(self):
|
||||
self.update_allocations()
|
||||
self.clear_linked_payment_entries()
|
||||
self.set_status(update=True)
|
||||
"Run on save(). Avoid recursion caused by multiple saves"
|
||||
if not self._saving_flag:
|
||||
self._saving_flag = True
|
||||
self.clear_linked_payment_entries()
|
||||
self.update_allocations()
|
||||
self._saving_flag = False
|
||||
|
||||
def on_cancel(self):
|
||||
self.clear_linked_payment_entries(for_cancel=True)
|
||||
self.set_status(update=True)
|
||||
|
||||
def update_allocations(self):
|
||||
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||
if self.payment_entries:
|
||||
allocated_amount = reduce(
|
||||
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
|
||||
)
|
||||
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||
else:
|
||||
allocated_amount = 0
|
||||
allocated_amount = 0.0
|
||||
|
||||
if allocated_amount:
|
||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
|
||||
frappe.db.set_value(
|
||||
self.doctype,
|
||||
self.name,
|
||||
"unallocated_amount",
|
||||
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
|
||||
)
|
||||
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
self.db_set("allocated_amount", flt(allocated_amount))
|
||||
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||
self.reload()
|
||||
self.set_status(update=True)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
|
||||
frappe.db.set_value(
|
||||
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
)
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
amount = self.deposit or self.withdrawal
|
||||
if amount == self.allocated_amount:
|
||||
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
# Can't add same voucher twice
|
||||
found = False
|
||||
for pe in self.payment_entries:
|
||||
if (
|
||||
pe.payment_document == voucher["payment_doctype"]
|
||||
and pe.payment_entry == voucher["payment_name"]
|
||||
):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
pe = {
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": 0.0, # Temporary
|
||||
}
|
||||
child = self.append("payment_entries", pe)
|
||||
added = True
|
||||
|
||||
# runs on_update_after_submit
|
||||
if added:
|
||||
self.save()
|
||||
|
||||
def allocate_payment_entries(self):
|
||||
"""Refactored from bank reconciliation tool.
|
||||
Non-zero allocations must be amended/cleared manually
|
||||
Get the bank transaction amount (b) and remove as we allocate
|
||||
For each payment_entry if allocated_amount == 0:
|
||||
- get the amount already allocated against all transactions (t), need latest date
|
||||
- get the voucher amount (from gl) (v)
|
||||
- allocate (a = v - t)
|
||||
- a = 0: should already be cleared, so clear & remove payment_entry
|
||||
- 0 < a <= u: allocate a & clear
|
||||
- 0 < a, a > u: allocate u
|
||||
- 0 > a: Error: already over-allocated
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||
remaining_amount = self.unallocated_amount
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self, payment_entry
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
def db_delete_payment_entry(self, payment_entry):
|
||||
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.payment_document == "Sales Invoice":
|
||||
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
|
||||
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
|
||||
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
|
||||
self.remove_payment_entry(payment_entry)
|
||||
# runs on_update_after_submit
|
||||
self.save()
|
||||
|
||||
def clear_simple_entry(self, payment_entry, for_cancel=False):
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
if (
|
||||
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
|
||||
== "Internal Transfer"
|
||||
):
|
||||
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
|
||||
return
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.remove(payment_entry)
|
||||
|
||||
clearance_date = self.date if not for_cancel else None
|
||||
frappe.db.set_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
|
||||
)
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
if for_cancel:
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||
else:
|
||||
self.allocate_payment_entries()
|
||||
|
||||
def clear_sales_invoice(self, payment_entry, for_cancel=False):
|
||||
clearance_date = self.date if not for_cancel else None
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||
)
|
||||
|
||||
|
||||
@@ -93,38 +153,114 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_reconciled_bank_transactions(payment_entry):
|
||||
reconciled_bank_transactions = frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
filters={"payment_entry": payment_entry.payment_entry},
|
||||
fields=["parent"],
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
But if a JE, could affect two banks.
|
||||
Should only clear the voucher if all bank gles are allocated.
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
|
||||
return reconciled_bank_transactions
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
||||
)
|
||||
unmatched_gles = len(gles)
|
||||
latest_transaction = transaction
|
||||
for gle in gles:
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
unallocated_amount = gle["amount"]
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"]:
|
||||
unallocated_amount = gle["amount"] - a["total"]
|
||||
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
|
||||
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
|
||||
else:
|
||||
# Must be a Journal Entry affecting more than one bank
|
||||
for a in bt_allocations:
|
||||
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
|
||||
unmatched_gles -= 1
|
||||
|
||||
return unallocated_amount, unmatched_gles == 0, latest_transaction
|
||||
|
||||
|
||||
def get_total_allocated_amount(payment_entry):
|
||||
return frappe.db.sql(
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
SUM(btp.allocated_amount) as allocated_amount,
|
||||
bt.name
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
gle.account AS gl_account
|
||||
FROM
|
||||
`tabBank Transaction Payments` as btp
|
||||
`tabGL Entry` gle
|
||||
LEFT JOIN
|
||||
`tabBank Transaction` bt ON bt.name=btp.parent
|
||||
`tabAccount` ac ON ac.name=gle.account
|
||||
WHERE
|
||||
btp.payment_document = %s
|
||||
AND
|
||||
btp.payment_entry = %s
|
||||
AND
|
||||
bt.docstatus = 1""",
|
||||
(payment_entry.payment_document, payment_entry.payment_entry),
|
||||
ac.account_type = 'Bank'
|
||||
AND gle.voucher_type = %(doctype)s
|
||||
AND gle.voucher_no = %(docname)s
|
||||
AND is_cancelled = 0
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, bank_account):
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
btp.payment_document = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
return result
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
@@ -137,7 +273,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
)
|
||||
elif doc.payment_type == "Pay":
|
||||
paid_amount_field = (
|
||||
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
|
||||
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
|
||||
)
|
||||
|
||||
return frappe.db.get_value(
|
||||
@@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": bank_account},
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(credit_in_account_currency)",
|
||||
)
|
||||
|
||||
@@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Bank Transaction":
|
||||
dep, wth = frappe.db.get_value(
|
||||
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
|
||||
)
|
||||
return abs(flt(wth) - flt(dep))
|
||||
|
||||
else:
|
||||
frappe.throw(
|
||||
"Please reconcile {0}: {1} manually".format(
|
||||
@@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unclear_reference_payment(doctype, docname):
|
||||
if frappe.db.exists(doctype, docname):
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
|
||||
"clearance_date",
|
||||
None,
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
|
||||
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if doctype in [
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Invoice",
|
||||
"Expense Claim",
|
||||
"Loan Repayment",
|
||||
"Loan Disbursement",
|
||||
]:
|
||||
if (
|
||||
doctype == "Payment Entry"
|
||||
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||
):
|
||||
return
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
return doc.payment_entry
|
||||
elif doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
|
||||
elif doctype == "Bank Transaction":
|
||||
# For when a second bank transaction has fixed another, e.g. refund
|
||||
bt = frappe.get_doc(doctype, docname)
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
bt.remove(pe)
|
||||
bt.save()
|
||||
break
|
||||
|
||||
|
||||
def get_reconciled_bank_transactions(doctype, docname):
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
filters={"payment_document": doctype, "payment_entry": docname},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unclear_reference_payment(doctype, docname, bt_name):
|
||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||
set_voucher_clearance(doctype, docname, None, bt)
|
||||
return docname
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import utils
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
@@ -40,7 +41,12 @@ class TestBankTransaction(FrappeTestCase):
|
||||
"Bank Transaction",
|
||||
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
|
||||
)
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
|
||||
linked_payments = get_linked_payments(
|
||||
bank_transaction.name,
|
||||
["payment_entry", "exact_match"],
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
@@ -81,7 +87,12 @@ class TestBankTransaction(FrappeTestCase):
|
||||
"Bank Transaction",
|
||||
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
|
||||
)
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
|
||||
linked_payments = get_linked_payments(
|
||||
bank_transaction.name,
|
||||
["payment_entry", "exact_match"],
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertTrue(linked_payments[0][3])
|
||||
|
||||
# Check error if already reconciled
|
||||
|
||||
@@ -184,6 +184,11 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
amount = expense_amount or get_amount(args, budget)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
|
||||
)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
budget_amount = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
|
||||
@@ -195,28 +200,28 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
|
||||
)
|
||||
|
||||
if (
|
||||
yearly_action in ("Stop", "Warn")
|
||||
and monthly_action != "Stop"
|
||||
and yearly_action != monthly_action
|
||||
):
|
||||
compare_expense_with_budget(
|
||||
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
|
||||
)
|
||||
|
||||
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
actual_expense = amount or get_actual_expense(args)
|
||||
if actual_expense > budget_amount:
|
||||
diff = actual_expense - budget_amount
|
||||
actual_expense = get_actual_expense(args)
|
||||
total_expense = actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = actual_expense - budget_amount
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
|
||||
currency = frappe.get_cached_value("Company", args.company, "default_currency")
|
||||
|
||||
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
|
||||
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
|
||||
_(action_for),
|
||||
frappe.bold(args.account),
|
||||
args.budget_against_field,
|
||||
frappe.unscrub(args.budget_against_field),
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
error_tense,
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
@@ -227,9 +232,9 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
action = "Warn"
|
||||
|
||||
if action == "Stop":
|
||||
frappe.throw(msg, BudgetError)
|
||||
frappe.throw(msg, BudgetError, title=_("Budget Exceeded"))
|
||||
else:
|
||||
frappe.msgprint(msg, indicator="orange")
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_actions(args, budget):
|
||||
@@ -351,7 +356,9 @@ def get_actual_expense(args):
|
||||
"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where gle.account=%(account)s
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account=%(account)s
|
||||
{condition1}
|
||||
and gle.fiscal_year=%(fiscal_year)s
|
||||
and gle.company=%(company)s
|
||||
|
||||
@@ -36,7 +36,7 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 7:
|
||||
if no_of_columns > 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
title=(_("Wrong Template")),
|
||||
@@ -233,6 +233,7 @@ def build_forest(data):
|
||||
is_group,
|
||||
account_type,
|
||||
root_type,
|
||||
account_currency,
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
@@ -253,6 +254,8 @@ def build_forest(data):
|
||||
charts_map[account_name]["account_type"] = account_type
|
||||
if root_type:
|
||||
charts_map[account_name]["root_type"] = root_type
|
||||
if account_currency:
|
||||
charts_map[account_name]["account_currency"] = account_currency
|
||||
path = return_parent(data, account_name)[::-1]
|
||||
paths.append(path) # List of path is created
|
||||
line_no += 1
|
||||
@@ -315,20 +318,21 @@ def get_template(template_type):
|
||||
"Is Group",
|
||||
"Account Type",
|
||||
"Root Type",
|
||||
"Account Currency",
|
||||
]
|
||||
writer = UnicodeWriter()
|
||||
writer.writerow(fields)
|
||||
|
||||
if template_type == "Blank Template":
|
||||
for root_type in get_root_types():
|
||||
writer.writerow(["", "", "", 1, "", root_type])
|
||||
writer.writerow(["", "", "", "", 1, "", root_type])
|
||||
|
||||
for account in get_mandatory_group_accounts():
|
||||
writer.writerow(["", "", "", 1, account, "Asset"])
|
||||
writer.writerow(["", "", "", "", 1, account, "Asset"])
|
||||
|
||||
for account_type in get_mandatory_account_types():
|
||||
writer.writerow(
|
||||
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
)
|
||||
else:
|
||||
writer = get_sample_template(writer)
|
||||
@@ -485,6 +489,10 @@ def set_default_accounts(company):
|
||||
"default_payable_account": frappe.db.get_value(
|
||||
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
|
||||
),
|
||||
"default_provisional_account": frappe.db.get_value(
|
||||
"Account",
|
||||
{"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -28,9 +28,14 @@ class InvalidDateError(frappe.ValidationError):
|
||||
|
||||
|
||||
class CostCenterAllocation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CostCenterAllocation, self).__init__(*args, **kwargs)
|
||||
self._skip_from_date_validation = False
|
||||
|
||||
def validate(self):
|
||||
self.validate_total_allocation_percentage()
|
||||
self.validate_from_date_based_on_existing_gle()
|
||||
if not self._skip_from_date_validation:
|
||||
self.validate_from_date_based_on_existing_gle()
|
||||
self.validate_backdated_allocation()
|
||||
self.validate_main_cost_center()
|
||||
self.validate_child_cost_centers()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"api_details_section",
|
||||
"disabled",
|
||||
"service_provider",
|
||||
"api_endpoint",
|
||||
"url",
|
||||
@@ -77,12 +78,18 @@
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-10 15:51:14.521174",
|
||||
"modified": "2023-01-09 12:19:03.955906",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -40,7 +40,7 @@ class Dunning(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
if self.dunning_amount:
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def make_gl_entries(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.add_custom_button(__('Journal Entry'), function() {
|
||||
frm.add_custom_button(__('Journal Entries'), function() {
|
||||
return frm.events.make_jv(frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
@@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
}
|
||||
},
|
||||
|
||||
get_entries: function(frm) {
|
||||
get_entries: function(frm, account) {
|
||||
frappe.call({
|
||||
method: "get_accounts_data",
|
||||
doc: cur_frm.doc,
|
||||
account: account,
|
||||
callback: function(r){
|
||||
frappe.model.clear_table(frm.doc, "accounts");
|
||||
if(r.message) {
|
||||
@@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
|
||||
let total_gain_loss = 0;
|
||||
frm.doc.accounts.forEach((d) => {
|
||||
d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d));
|
||||
total_gain_loss += flt(d.gain_loss, precision("gain_loss", d));
|
||||
});
|
||||
|
||||
@@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
},
|
||||
|
||||
make_jv : function(frm) {
|
||||
let revaluation_journal = null;
|
||||
let zero_balance_journal = null;
|
||||
frappe.call({
|
||||
method: "make_jv_entry",
|
||||
method: "make_jv_entries",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: "Making Journal Entries...",
|
||||
callback: function(r){
|
||||
if (r.message) {
|
||||
var doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
let response = r.message;
|
||||
if(response['revaluation_jv'] || response['zero_balance_jv']) {
|
||||
frappe.msgprint(__("Journals have been created"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"get_entries",
|
||||
"accounts",
|
||||
"section_break_6",
|
||||
"gain_loss_unbooked",
|
||||
"gain_loss_booked",
|
||||
"column_break_10",
|
||||
"total_gain_loss",
|
||||
"amended_from"
|
||||
],
|
||||
@@ -59,13 +62,6 @@
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -74,11 +70,37 @@
|
||||
"options": "Exchange Rate Revaluation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_unbooked",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gain/Loss from Revaluation",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency",
|
||||
"fieldname": "gain_loss_booked",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gain/Loss already booked",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-17 10:28:03.911554",
|
||||
"modified": "2022-12-29 19:38:24.416529",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt
|
||||
from frappe.query_builder import Criterion, Order
|
||||
from frappe.query_builder.functions import NullIf, Sum
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
|
||||
@@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
total_gain_loss = 0
|
||||
|
||||
gain_loss_booked = 0
|
||||
gain_loss_unbooked = 0
|
||||
|
||||
for d in self.accounts:
|
||||
d.gain_loss = flt(
|
||||
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
|
||||
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
|
||||
if not d.zero_balance:
|
||||
d.gain_loss = flt(
|
||||
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
|
||||
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
|
||||
|
||||
if d.zero_balance:
|
||||
gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss"))
|
||||
else:
|
||||
gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss"))
|
||||
|
||||
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
|
||||
|
||||
self.gain_loss_booked = gain_loss_booked
|
||||
self.gain_loss_unbooked = gain_loss_unbooked
|
||||
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
|
||||
|
||||
def validate_mandatory(self):
|
||||
@@ -35,98 +51,205 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_journal_entry_condition(self):
|
||||
total_debit = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
|
||||
"sum(debit) as sum",
|
||||
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
|
||||
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
journals = (
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.distinct()
|
||||
.where(
|
||||
(jea.reference_type == "Exchange Rate Revaluation")
|
||||
& (jea.reference_name == self.name)
|
||||
& (jea.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
total_amt = 0
|
||||
for d in self.accounts:
|
||||
total_amt = total_amt + d.new_balance_in_base_currency
|
||||
if journals:
|
||||
gle = qb.DocType("GL Entry")
|
||||
total_amt = (
|
||||
qb.from_(gle)
|
||||
.select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount"))
|
||||
.where(
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& (gle.voucher_no.isin(journals))
|
||||
& (gle.account == exchange_gain_loss_account)
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if total_amt != total_debit:
|
||||
return True
|
||||
if total_amt and total_amt[0][0] != self.total_gain_loss:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self, account=None):
|
||||
accounts = []
|
||||
def get_accounts_data(self):
|
||||
self.validate_mandatory()
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
account_details = self.get_account_balance_from_gle(
|
||||
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
|
||||
)
|
||||
accounts_with_new_balance = self.calculate_new_account_balance(
|
||||
self.company, self.posting_date, account_details
|
||||
)
|
||||
|
||||
if not accounts_with_new_balance:
|
||||
self.throw_invalid_response_message(account_details)
|
||||
|
||||
return accounts_with_new_balance
|
||||
|
||||
@staticmethod
|
||||
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
|
||||
account_details = []
|
||||
|
||||
if company and posting_date:
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
|
||||
acc = qb.DocType("Account")
|
||||
if account:
|
||||
accounts = [account]
|
||||
else:
|
||||
res = (
|
||||
qb.from_(acc)
|
||||
.select(acc.name)
|
||||
.where(
|
||||
(acc.is_group == 0)
|
||||
& (acc.report_type == "Balance Sheet")
|
||||
& (acc.root_type.isin(["Asset", "Liability", "Equity"]))
|
||||
& (acc.account_type != "Stock")
|
||||
& (acc.company == company)
|
||||
& (acc.account_currency != company_currency)
|
||||
)
|
||||
.orderby(acc.name)
|
||||
.run(as_list=True)
|
||||
)
|
||||
accounts = [x[0] for x in res]
|
||||
|
||||
if accounts:
|
||||
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
|
||||
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
|
||||
)
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
# conditions
|
||||
conditions = []
|
||||
conditions.append(gle.account.isin(accounts))
|
||||
conditions.append(gle.posting_date.lte(posting_date))
|
||||
conditions.append(gle.is_cancelled == 0)
|
||||
|
||||
if party_type:
|
||||
conditions.append(gle.party_type == party_type)
|
||||
if party:
|
||||
conditions.append(gle.party == party)
|
||||
|
||||
account_details = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.party_type,
|
||||
gle.party,
|
||||
gle.account_currency,
|
||||
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
|
||||
"balance_in_account_currency"
|
||||
),
|
||||
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
|
||||
(Sum(gle.debit) - Sum(gle.credit) == 0)
|
||||
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
|
||||
"zero_balance"
|
||||
),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
|
||||
.having(having_clause)
|
||||
.orderby(gle.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@staticmethod
|
||||
def calculate_new_account_balance(company, posting_date, account_details):
|
||||
accounts = []
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
company_currency,
|
||||
)
|
||||
|
||||
account_details = self.get_accounts_from_gle()
|
||||
for d in account_details:
|
||||
current_exchange_rate = (
|
||||
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
|
||||
)
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
}
|
||||
if account_details:
|
||||
# Handle Accounts with balance in both Account/Base Currency
|
||||
for d in [x for x in account_details if not x.zero_balance]:
|
||||
current_exchange_rate = (
|
||||
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
|
||||
)
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
self.throw_invalid_response_message(account_details)
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
|
||||
if d.balance != 0:
|
||||
current_exchange_rate = new_exchange_rate = 0
|
||||
|
||||
new_balance_in_account_currency = 0 # this will be '0'
|
||||
new_balance_in_base_currency = 0 # this will be '0'
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
else:
|
||||
new_exchange_rate = 0
|
||||
new_balance_in_base_currency = 0
|
||||
new_balance_in_account_currency = 0
|
||||
|
||||
current_exchange_rate = calculate_exchange_rate_using_last_gle(
|
||||
company, d.account, d.party_type, d.party
|
||||
)
|
||||
|
||||
gain_loss = new_balance_in_account_currency - (
|
||||
current_exchange_rate * d.balance_in_account_currency
|
||||
)
|
||||
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
def get_accounts_from_gle(self):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
accounts = frappe.db.sql_list(
|
||||
"""
|
||||
select name
|
||||
from tabAccount
|
||||
where is_group = 0
|
||||
and report_type = 'Balance Sheet'
|
||||
and root_type in ('Asset', 'Liability', 'Equity')
|
||||
and account_type != 'Stock'
|
||||
and company=%s
|
||||
and account_currency != %s
|
||||
order by name""",
|
||||
(self.company, company_currency),
|
||||
)
|
||||
|
||||
account_details = []
|
||||
if accounts:
|
||||
account_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
account, party_type, party, account_currency,
|
||||
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
|
||||
sum(debit) - sum(credit) as balance
|
||||
from `tabGL Entry`
|
||||
where account in (%s)
|
||||
and posting_date <= %s
|
||||
and is_cancelled = 0
|
||||
group by account, NULLIF(party_type,''), NULLIF(party,'')
|
||||
having sum(debit) != sum(credit)
|
||||
order by account
|
||||
"""
|
||||
% (", ".join(["%s"] * len(accounts)), "%s"),
|
||||
tuple(accounts + [self.posting_date]),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
def throw_invalid_response_message(self, account_details):
|
||||
if account_details:
|
||||
message = _("No outstanding invoices require exchange rate revaluation")
|
||||
@@ -134,11 +257,7 @@ class ExchangeRateRevaluation(Document):
|
||||
message = _("No outstanding invoices found")
|
||||
frappe.msgprint(message)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_jv_entry(self):
|
||||
if self.total_gain_loss == 0:
|
||||
return
|
||||
|
||||
def get_for_unrealized_gain_loss_account(self):
|
||||
unrealized_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "unrealized_exchange_gain_loss_account"
|
||||
)
|
||||
@@ -146,6 +265,130 @@ class ExchangeRateRevaluation(Document):
|
||||
frappe.throw(
|
||||
_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
|
||||
)
|
||||
return unrealized_exchange_gain_loss_account
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_jv_entries(self):
|
||||
zero_balance_jv = self.make_jv_for_zero_balance()
|
||||
if zero_balance_jv:
|
||||
frappe.msgprint(
|
||||
f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}"
|
||||
)
|
||||
|
||||
revaluation_jv = self.make_jv_for_revaluation()
|
||||
if revaluation_jv:
|
||||
frappe.msgprint(
|
||||
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
|
||||
"zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None,
|
||||
}
|
||||
|
||||
def make_jv_for_zero_balance(self):
|
||||
if self.gain_loss_booked == 0:
|
||||
return
|
||||
|
||||
accounts = [x for x in self.accounts if x.zero_balance]
|
||||
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = self.posting_date
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in accounts:
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": d.get("account"),
|
||||
"party_type": d.get("party_type"),
|
||||
"party": d.get("party"),
|
||||
"account_currency": d.get("account_currency"),
|
||||
"balance": flt(
|
||||
d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"exchange_rate": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
# Account Currency has balance
|
||||
if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"):
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if d.get("balance_in_account_currency") > 0
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
reverse_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if dr_or_cr == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
journal_account.update(
|
||||
{
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
reverse_dr_or_cr: 0,
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
|
||||
# Base currency has balance
|
||||
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
journal_account.update(
|
||||
{
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency")
|
||||
),
|
||||
reverse_dr_or_cr: 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(journal_account)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
|
||||
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_total_debit_credit()
|
||||
journal_entry.save()
|
||||
return journal_entry
|
||||
|
||||
def make_jv_for_revaluation(self):
|
||||
if self.gain_loss_unbooked == 0:
|
||||
return
|
||||
|
||||
accounts = [x for x in self.accounts if not x.zero_balance]
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Rate Revaluation"
|
||||
@@ -154,7 +397,10 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in self.accounts:
|
||||
for d in accounts:
|
||||
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||
continue
|
||||
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d.get("balance_in_account_currency") > 0
|
||||
@@ -179,6 +425,7 @@ class ExchangeRateRevaluation(Document):
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
@@ -196,63 +443,122 @@ class ExchangeRateRevaluation(Document):
|
||||
reverse_dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
|
||||
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_amounts_in_company_currency()
|
||||
journal_entry.set_total_debit_credit()
|
||||
return journal_entry.as_dict()
|
||||
|
||||
self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
|
||||
journal_entry.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit_in_account_currency": abs(self.gain_loss_unbooked)
|
||||
if self.gain_loss_unbooked < 0
|
||||
else 0,
|
||||
"credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
},
|
||||
)
|
||||
|
||||
journal_entry.set_amounts_in_company_currency()
|
||||
journal_entry.set_total_debit_credit()
|
||||
journal_entry.save()
|
||||
return journal_entry
|
||||
|
||||
|
||||
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
"""
|
||||
Use last GL entry to calculate exchange rate
|
||||
"""
|
||||
last_exchange_rate = None
|
||||
if company and account:
|
||||
gl = qb.DocType("GL Entry")
|
||||
|
||||
# build conditions
|
||||
conditions = []
|
||||
conditions.append(gl.company == company)
|
||||
conditions.append(gl.account == account)
|
||||
conditions.append(gl.is_cancelled == 0)
|
||||
conditions.append((gl.debit > 0) | (gl.credit > 0))
|
||||
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
|
||||
if party_type:
|
||||
conditions.append(gl.party_type == party_type)
|
||||
if party:
|
||||
conditions.append(gl.party == party)
|
||||
|
||||
voucher_type, voucher_no = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_type, gl.voucher_no)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(gl.posting_date, order=Order.desc)
|
||||
.limit(1)
|
||||
.run()[0]
|
||||
)
|
||||
|
||||
last_exchange_rate = (
|
||||
qb.from_(gl)
|
||||
.select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency))
|
||||
.where(
|
||||
(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
|
||||
)
|
||||
.orderby(gl.posting_date, order=Order.desc)
|
||||
.limit(1)
|
||||
.run()[0][0]
|
||||
)
|
||||
|
||||
return last_exchange_rate
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(account, company, posting_date, party_type=None, party=None):
|
||||
def get_account_details(company, posting_date, account, party_type=None, party=None):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
account_currency, account_type = frappe.get_cached_value(
|
||||
"Account", account, ["account_currency", "account_type"]
|
||||
)
|
||||
|
||||
if account_type in ["Receivable", "Payable"] and not (party_type and party):
|
||||
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
|
||||
|
||||
account_details = {}
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
balance = get_balance_on(
|
||||
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
|
||||
)
|
||||
|
||||
account_details = {
|
||||
"account_currency": account_currency,
|
||||
}
|
||||
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
|
||||
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
|
||||
)
|
||||
|
||||
if balance:
|
||||
balance_in_account_currency = get_balance_on(
|
||||
account, date=posting_date, party_type=party_type, party=party
|
||||
if account_balance and (
|
||||
account_balance[0].balance or account_balance[0].balance_in_account_currency
|
||||
):
|
||||
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
company, posting_date, account_balance
|
||||
)
|
||||
current_exchange_rate = (
|
||||
balance / balance_in_account_currency if balance_in_account_currency else 0
|
||||
)
|
||||
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
|
||||
account_details = account_details.update(
|
||||
row = account_with_new_balance[0]
|
||||
account_details.update(
|
||||
{
|
||||
"balance_in_base_currency": balance,
|
||||
"balance_in_account_currency": balance_in_account_currency,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"balance_in_base_currency": row["balance_in_base_currency"],
|
||||
"balance_in_account_currency": row["balance_in_account_currency"],
|
||||
"current_exchange_rate": row["current_exchange_rate"],
|
||||
"new_exchange_rate": row["new_exchange_rate"],
|
||||
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
|
||||
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
|
||||
"zero_balance": row["zero_balance"],
|
||||
"gain_loss": row["gain_loss"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,14 +10,21 @@
|
||||
"party",
|
||||
"column_break_2",
|
||||
"account_currency",
|
||||
"account_balances",
|
||||
"balance_in_account_currency",
|
||||
"column_break_46yz",
|
||||
"new_balance_in_account_currency",
|
||||
"balances",
|
||||
"current_exchange_rate",
|
||||
"balance_in_base_currency",
|
||||
"column_break_9",
|
||||
"column_break_xown",
|
||||
"new_exchange_rate",
|
||||
"column_break_9",
|
||||
"balance_in_base_currency",
|
||||
"column_break_ukce",
|
||||
"new_balance_in_base_currency",
|
||||
"gain_loss"
|
||||
"section_break_ngrs",
|
||||
"gain_loss",
|
||||
"zero_balance"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -78,7 +85,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "new_exchange_rate",
|
||||
@@ -102,11 +109,45 @@
|
||||
"label": "Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This Account has '0' balance in either Base Currency or Account Currency",
|
||||
"fieldname": "zero_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Zero Balance"
|
||||
},
|
||||
{
|
||||
"fieldname": "new_balance_in_account_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "New Balance In Account Currency",
|
||||
"options": "account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_balances",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_46yz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xown",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ukce",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ngrs",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-17 10:26:18.302728",
|
||||
"modified": "2022-12-29 19:38:52.915295",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation Account",
|
||||
|
||||
@@ -9,10 +9,6 @@ from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
|
||||
class FiscalYearIncorrectDate(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FiscalYear(Document):
|
||||
@frappe.whitelist()
|
||||
def set_as_default(self):
|
||||
@@ -53,23 +49,18 @@ class FiscalYear(Document):
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
# Fiscal Year can be shorter than one year, in some jurisdictions
|
||||
# under certain circumstances. For example, in the USA and Germany.
|
||||
return
|
||||
|
||||
if getdate(self.year_start_date) > getdate(self.year_end_date):
|
||||
frappe.throw(
|
||||
_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
|
||||
FiscalYearIncorrectDate,
|
||||
)
|
||||
|
||||
date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1)
|
||||
|
||||
if getdate(self.year_end_date) != date:
|
||||
frappe.throw(
|
||||
_("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
|
||||
FiscalYearIncorrectDate,
|
||||
frappe.exceptions.InvalidDates,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
@@ -169,5 +160,6 @@ def auto_create_fiscal_year():
|
||||
|
||||
|
||||
def get_from_and_to_date(fiscal_year):
|
||||
fields = ["year_start_date as from_date", "year_end_date as to_date"]
|
||||
return frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
|
||||
fields = ["year_start_date", "year_end_date"]
|
||||
cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
|
||||
return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date)
|
||||
|
||||
@@ -7,8 +7,6 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
|
||||
|
||||
test_ignore = ["Company"]
|
||||
|
||||
|
||||
@@ -26,7 +24,7 @@ class TestFiscalYear(unittest.TestCase):
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
|
||||
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
@@ -35,8 +33,8 @@ def test_record_generator():
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "_Test Short Fiscal Year 2011",
|
||||
"is_short_year": 1,
|
||||
"year_end_date": "2011-04-01",
|
||||
"year_start_date": "2011-12-31",
|
||||
"year_start_date": "2011-04-01",
|
||||
"year_end_date": "2011-12-31",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -95,7 +95,15 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
|
||||
if not (
|
||||
flt(self.debit, self.precision("debit"))
|
||||
or flt(self.credit, self.precision("credit"))
|
||||
or (
|
||||
self.voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} {1}: Either debit or credit amount is required for {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
|
||||
@@ -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'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"label": "Entry Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -539,7 +539,7 @@
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-23 22:01:32.348337",
|
||||
"modified": "2023-03-01 14:58:59.286591",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||
@@ -23,6 +23,9 @@ from erpnext.accounts.utils import (
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
@@ -34,9 +37,6 @@ class JournalEntry(AccountsController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JournalEntry, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_feed(self):
|
||||
return self.voucher_type
|
||||
|
||||
def validate(self):
|
||||
if self.voucher_type == "Opening Entry":
|
||||
self.is_opening = "Yes"
|
||||
@@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_multi_currency()
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
@@ -81,6 +81,7 @@ class JournalEntry(AccountsController):
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
|
||||
@@ -88,7 +89,13 @@ class JournalEntry(AccountsController):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
@@ -225,6 +232,29 @@ class JournalEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
continue
|
||||
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
|
||||
|
||||
asset.set_status()
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
@@ -283,19 +313,45 @@ class JournalEntry(AccountsController):
|
||||
d.db_update()
|
||||
|
||||
def unlink_asset_reference(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.reference_type == "Asset" and d.reference_name:
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
if asset.calculate_depreciation:
|
||||
je_found = False
|
||||
|
||||
asset.set_status()
|
||||
for row in asset.get("finance_books"):
|
||||
if je_found:
|
||||
break
|
||||
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
|
||||
|
||||
for s in depr_schedule or []:
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
row.value_after_depreciation += s.depreciation_amount
|
||||
row.db_update()
|
||||
|
||||
asset.set_status()
|
||||
|
||||
je_found = True
|
||||
break
|
||||
else:
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
|
||||
|
||||
asset.set_status()
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -592,28 +648,29 @@ class JournalEntry(AccountsController):
|
||||
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||
else:
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0):
|
||||
if flt(d.debit) > 0:
|
||||
accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit) > 0:
|
||||
accounts_credited.append(d.party or d.account)
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0):
|
||||
if flt(d.debit) > 0:
|
||||
d.against_account = ", ".join(list(set(accounts_credited)))
|
||||
if flt(d.credit > 0):
|
||||
if flt(d.credit) > 0:
|
||||
d.against_account = ", ".join(list(set(accounts_debited)))
|
||||
|
||||
def validate_debit_credit_amount(self):
|
||||
for d in self.get("accounts"):
|
||||
if not flt(d.debit) and not flt(d.credit):
|
||||
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
for d in self.get("accounts"):
|
||||
if not flt(d.debit) and not flt(d.credit):
|
||||
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
|
||||
|
||||
def validate_total_debit_and_credit(self):
|
||||
self.set_total_debit_credit()
|
||||
if self.difference:
|
||||
frappe.throw(
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
|
||||
)
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
if self.difference:
|
||||
frappe.throw(
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
|
||||
)
|
||||
|
||||
def set_total_debit_credit(self):
|
||||
self.total_debit, self.total_credit, self.difference = 0, 0, 0
|
||||
@@ -651,16 +708,17 @@ class JournalEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
|
||||
def set_amounts_in_company_currency(self):
|
||||
for d in self.get("accounts"):
|
||||
d.debit_in_account_currency = flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
d.credit_in_account_currency = flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
for d in self.get("accounts"):
|
||||
d.debit_in_account_currency = flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
d.credit_in_account_currency = flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
|
||||
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
|
||||
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
|
||||
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
|
||||
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
|
||||
|
||||
def set_exchange_rate(self):
|
||||
for d in self.get("accounts"):
|
||||
@@ -759,7 +817,7 @@ class JournalEntry(AccountsController):
|
||||
pay_to_recd_from = d.party
|
||||
|
||||
if pay_to_recd_from and pay_to_recd_from == d.party:
|
||||
party_amount += d.debit_in_account_currency or d.credit_in_account_currency
|
||||
party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
|
||||
party_account_currency = d.account_currency
|
||||
|
||||
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
|
||||
@@ -789,7 +847,7 @@ class JournalEntry(AccountsController):
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit:
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
@@ -837,7 +895,7 @@ class JournalEntry(AccountsController):
|
||||
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self):
|
||||
def get_balance(self, difference_account=None):
|
||||
if not self.get("accounts"):
|
||||
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
|
||||
else:
|
||||
@@ -852,7 +910,13 @@ class JournalEntry(AccountsController):
|
||||
blank_row = d
|
||||
|
||||
if not blank_row:
|
||||
blank_row = self.append("accounts", {})
|
||||
blank_row = self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": difference_account,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
},
|
||||
)
|
||||
|
||||
blank_row.exchange_rate = 1
|
||||
if diff > 0:
|
||||
|
||||
@@ -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'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.toggle_display("set_exchange_gain_loss",
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
|
||||
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
set_dynamic_labels: function(frm) {
|
||||
@@ -245,8 +244,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
||||
party_account_currency, "references");
|
||||
|
||||
frm.set_currency_labels(["amount"], company_currency, "deductions");
|
||||
|
||||
cur_frm.set_df_property("source_exchange_rate", "description",
|
||||
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
"depends_on": "paid_from",
|
||||
"fieldname": "paid_from_account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"label": "Account Currency (From)",
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
@@ -249,7 +249,7 @@
|
||||
"depends_on": "paid_from",
|
||||
"fieldname": "paid_from_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Balance",
|
||||
"label": "Account Balance (From)",
|
||||
"options": "paid_from_account_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -272,7 +272,7 @@
|
||||
"depends_on": "paid_to",
|
||||
"fieldname": "paid_to_account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"label": "Account Currency (To)",
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
@@ -282,7 +282,7 @@
|
||||
"depends_on": "paid_to",
|
||||
"fieldname": "paid_to_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Balance",
|
||||
"label": "Account Balance (To)",
|
||||
"options": "paid_to_account_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -304,7 +304,8 @@
|
||||
{
|
||||
"fieldname": "source_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"label": "Source Exchange Rate",
|
||||
"precision": "9",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -333,7 +334,8 @@
|
||||
{
|
||||
"fieldname": "target_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"label": "Target Exchange Rate",
|
||||
"precision": "9",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -631,14 +633,14 @@
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "purchase_taxes_and_charges_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Taxes and Charges Template",
|
||||
"label": "Purchase Taxes and Charges Template",
|
||||
"options": "Purchase Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.party_type == 'Customer'",
|
||||
"fieldname": "sales_taxes_and_charges_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Taxes and Charges Template",
|
||||
"label": "Sales Taxes and Charges Template",
|
||||
"options": "Sales Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
@@ -731,7 +733,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-23 20:08:39.559814",
|
||||
"modified": "2023-02-14 04:52:30.478523",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -92,7 +92,13 @@ class PaymentEntry(AccountsController):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
@@ -247,7 +253,7 @@ class PaymentEntry(AccountsController):
|
||||
self.set_target_exchange_rate(ref_doc)
|
||||
|
||||
def set_source_exchange_rate(self, ref_doc=None):
|
||||
if self.paid_from and not self.source_exchange_rate:
|
||||
if self.paid_from:
|
||||
if self.paid_from_account_currency == self.company_currency:
|
||||
self.source_exchange_rate = 1
|
||||
else:
|
||||
@@ -410,7 +416,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
for ref in self.get("references"):
|
||||
if ref.payment_term and ref.reference_name:
|
||||
key = (ref.payment_term, ref.reference_name)
|
||||
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||
|
||||
@@ -418,20 +424,37 @@ class PaymentEntry(AccountsController):
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": ref.reference_name},
|
||||
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
|
||||
fields=[
|
||||
"paid_amount",
|
||||
"payment_amount",
|
||||
"payment_term",
|
||||
"discount",
|
||||
"outstanding",
|
||||
"discount_type",
|
||||
],
|
||||
)
|
||||
for term in payment_schedule:
|
||||
invoice_key = (term.payment_term, ref.reference_name)
|
||||
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
if not (term.discount_type and term.discount):
|
||||
continue
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
else:
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||
|
||||
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
||||
|
||||
allocated_amount = self.get_allocated_amount_in_transaction_currency(
|
||||
allocated_amount, key[2], key[1]
|
||||
)
|
||||
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||
|
||||
@@ -466,6 +489,33 @@ class PaymentEntry(AccountsController):
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
self, allocated_amount, reference_doctype, reference_docname
|
||||
):
|
||||
"""
|
||||
Payment Entry could be in base currency while reference's payment schedule
|
||||
is always in transaction currency.
|
||||
E.g.
|
||||
* SI with base=INR and currency=USD
|
||||
* SI with payment schedule in USD
|
||||
* PE in INR (accounting done in base currency)
|
||||
"""
|
||||
ref_currency, ref_exchange_rate = frappe.db.get_value(
|
||||
reference_doctype, reference_docname, ["currency", "conversion_rate"]
|
||||
)
|
||||
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
|
||||
# PE in different currency
|
||||
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
|
||||
|
||||
if not (is_single_currency and reference_is_multi_currency):
|
||||
return allocated_amount
|
||||
|
||||
allocated_amount = flt(
|
||||
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||
)
|
||||
|
||||
return allocated_amount
|
||||
|
||||
def set_status(self):
|
||||
if self.docstatus == 2:
|
||||
self.status = "Cancelled"
|
||||
@@ -622,7 +672,7 @@ class PaymentEntry(AccountsController):
|
||||
self.payment_type == "Receive"
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
||||
and self.total_allocated_amount
|
||||
< self.paid_amount + (total_deductions / self.source_exchange_rate)
|
||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
||||
@@ -632,7 +682,7 @@ class PaymentEntry(AccountsController):
|
||||
self.payment_type == "Pay"
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
||||
and self.total_allocated_amount
|
||||
< self.received_amount + (total_deductions / self.target_exchange_rate)
|
||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
||||
@@ -684,35 +734,34 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def validate_payment_against_negative_invoice(self):
|
||||
if (self.payment_type == "Pay" and self.party_type == "Customer") or (
|
||||
self.payment_type == "Receive" and self.party_type == "Supplier"
|
||||
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
|
||||
self.payment_type != "Receive" or self.party_type != "Supplier"
|
||||
):
|
||||
return
|
||||
|
||||
total_negative_outstanding = sum(
|
||||
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
|
||||
total_negative_outstanding = sum(
|
||||
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
|
||||
)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
|
||||
additional_charges = sum(flt(d.amount) for d in self.deductions)
|
||||
|
||||
if not total_negative_outstanding:
|
||||
if self.party_type == "Customer":
|
||||
msg = _("Cannot pay to Customer without any negative outstanding invoice")
|
||||
else:
|
||||
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
|
||||
|
||||
frappe.throw(msg, InvalidPaymentEntry)
|
||||
|
||||
elif paid_amount - additional_charges > total_negative_outstanding:
|
||||
frappe.throw(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
),
|
||||
InvalidPaymentEntry,
|
||||
)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
|
||||
additional_charges = sum([flt(d.amount) for d in self.deductions])
|
||||
|
||||
if not total_negative_outstanding:
|
||||
frappe.throw(
|
||||
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
|
||||
_(self.payment_type),
|
||||
(_("to") if self.party_type == "Customer" else _("from")),
|
||||
self.party_type,
|
||||
),
|
||||
InvalidPaymentEntry,
|
||||
)
|
||||
|
||||
elif paid_amount - additional_charges > total_negative_outstanding:
|
||||
frappe.throw(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
),
|
||||
InvalidPaymentEntry,
|
||||
)
|
||||
|
||||
def set_title(self):
|
||||
if frappe.flags.in_import and self.title:
|
||||
# do not set title dynamically if title exists during data import.
|
||||
@@ -1188,6 +1237,7 @@ def get_outstanding_reference_documents(args):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
common_filter = []
|
||||
accounting_dimensions_filter = []
|
||||
posting_and_due_date = []
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
@@ -1217,7 +1267,7 @@ def get_outstanding_reference_documents(args):
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
common_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
date_fields_dict = {
|
||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||
@@ -1243,6 +1293,7 @@ def get_outstanding_reference_documents(args):
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
accounting_dimensions=accounting_dimensions_filter,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
@@ -1635,11 +1686,18 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_entry(
|
||||
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
|
||||
dt,
|
||||
dn,
|
||||
party_amount=None,
|
||||
bank_account=None,
|
||||
bank_amount=None,
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
|
||||
if not party_type:
|
||||
@@ -1662,8 +1720,9 @@ def get_payment_entry(
|
||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||
)
|
||||
|
||||
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc
|
||||
reference_date = getdate(reference_date)
|
||||
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
)
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
@@ -1671,6 +1730,7 @@ def get_payment_entry(
|
||||
pe.company = doc.company
|
||||
pe.cost_center = doc.get("cost_center")
|
||||
pe.posting_date = nowdate()
|
||||
pe.reference_date = reference_date
|
||||
pe.mode_of_payment = doc.get("mode_of_payment")
|
||||
pe.party_type = party_type
|
||||
pe.party = doc.get(scrub(party_type))
|
||||
@@ -1711,7 +1771,7 @@ def get_payment_entry(
|
||||
):
|
||||
|
||||
for reference in get_reference_as_per_payment_terms(
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
pe.append("references", reference)
|
||||
else:
|
||||
@@ -1757,23 +1817,38 @@ def get_payment_entry(
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
|
||||
}
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
|
||||
|
||||
set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
pe.set_difference_amount()
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
return pe
|
||||
|
||||
|
||||
def update_accounting_dimensions(pe, doc):
|
||||
"""
|
||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||
"""
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
pe.set(dimension, doc.get(dimension))
|
||||
|
||||
|
||||
def get_bank_cash_account(doc, bank_account):
|
||||
bank = get_default_bank_cash_account(
|
||||
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
|
||||
@@ -1868,20 +1943,28 @@ def set_paid_amount_and_received_amount(
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
def apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
):
|
||||
total_discount = 0
|
||||
valid_discounts = []
|
||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
||||
is_multi_currency = party_account_currency != doc.company_currency
|
||||
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
|
||||
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||
else:
|
||||
discount_amount = term.discount
|
||||
|
||||
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
|
||||
# if accounting is done in the same currency, paid_amount = received_amount
|
||||
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
|
||||
discount_amount_in_foreign_currency = discount_amount * conversion_rate
|
||||
|
||||
if doc.doctype == "Sales Invoice":
|
||||
paid_amount -= discount_amount
|
||||
@@ -1890,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
received_amount -= discount_amount
|
||||
paid_amount -= discount_amount_in_foreign_currency
|
||||
|
||||
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
|
||||
total_discount += discount_amount
|
||||
|
||||
if total_discount:
|
||||
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
|
||||
currency = doc.get("currency") if is_multi_currency else doc.company_currency
|
||||
money = frappe.utils.fmt_money(total_discount, currency=currency)
|
||||
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
||||
|
||||
return paid_amount, received_amount, total_discount
|
||||
return paid_amount, received_amount, total_discount, valid_discounts
|
||||
|
||||
|
||||
def set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
):
|
||||
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||
if party_account_currency != doc.company_currency:
|
||||
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
# Avoid considering miniscule losses
|
||||
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
|
||||
|
||||
# Set base discount amount (discount loss/pending rounding loss) in deductions
|
||||
if discount_amount > 0.0:
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
# If tax loss booking is enabled, pending loss will be rounding loss.
|
||||
# Otherwise it will be the total discount loss.
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||
"""Split early payment discount into Income Loss & Tax Loss."""
|
||||
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
|
||||
|
||||
if not total_discount_percent:
|
||||
return 0.0
|
||||
|
||||
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
|
||||
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
|
||||
|
||||
# Round off total loss rather than individual losses to reduce rounding error
|
||||
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
|
||||
|
||||
|
||||
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||
"""Get total percentage and amount discount applied as a percentage."""
|
||||
total_discount_percent = (
|
||||
sum(
|
||||
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
# Operate in percentages only as it makes the income & tax split easier
|
||||
total_discount_amount = (
|
||||
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
if total_discount_amount:
|
||||
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
|
||||
total_discount_percent += discount_percentage
|
||||
return total_discount_percent
|
||||
|
||||
return total_discount_percent
|
||||
|
||||
|
||||
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||
"""Add loss on income discount in base currency."""
|
||||
precision = doc.precision("total")
|
||||
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(base_loss_on_income, precision),
|
||||
},
|
||||
)
|
||||
|
||||
return base_loss_on_income # Return loss without rounding
|
||||
|
||||
|
||||
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"""Add loss on tax discount in base currency."""
|
||||
tax_discount_loss = {}
|
||||
base_total_tax_loss = 0
|
||||
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||
total_discount_percentage / 100
|
||||
)
|
||||
|
||||
account = tax.get("account_head")
|
||||
if not tax_discount_loss.get(account):
|
||||
tax_discount_loss[account] = base_tax_loss
|
||||
else:
|
||||
tax_discount_loss[account] += base_tax_loss
|
||||
|
||||
for account, loss in tax_discount_loss.items():
|
||||
base_total_tax_loss += loss
|
||||
if loss == 0.0:
|
||||
continue
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
},
|
||||
)
|
||||
|
||||
return base_total_tax_loss # Return loss without rounding
|
||||
|
||||
|
||||
def get_reference_as_per_payment_terms(
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
references = []
|
||||
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
|
||||
party_account_currency != doc.company_currency
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
payment_term_outstanding = flt(
|
||||
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
||||
)
|
||||
if not is_multi_currency_acc:
|
||||
# If accounting is done in company currency for multi-currency transaction
|
||||
payment_term_outstanding = flt(
|
||||
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||
)
|
||||
|
||||
if payment_term_outstanding:
|
||||
references.append(
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe.paid_amount, 212.4)
|
||||
self.assertEqual(pe.deductions[0].amount, 23.6)
|
||||
|
||||
pe.submit()
|
||||
si.load_from_db()
|
||||
|
||||
@@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount_amount(self):
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
|
||||
si.payment_terms_template = "Test Discount Amount Template"
|
||||
create_payment_terms_template_with_discount(
|
||||
name="30 Credit Days with Rs.50 Discount",
|
||||
discount_type="Amount",
|
||||
discount=50,
|
||||
template_name="Test Discount Amount Template",
|
||||
)
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Service Tax",
|
||||
"rate": 18,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Set reference date past discount cut off date
|
||||
pe_1 = get_payment_entry(
|
||||
"Sales Invoice",
|
||||
si.name,
|
||||
bank_account="_Test Cash - _TC",
|
||||
reference_date=frappe.utils.add_days(si.posting_date, 2),
|
||||
)
|
||||
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
|
||||
|
||||
# Test if tax loss is booked on enabling configuration
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe.paid_amount, 186)
|
||||
self.assertEqual(pe.deductions[0].amount, 50.0)
|
||||
|
||||
pe.submit()
|
||||
si.load_from_db()
|
||||
|
||||
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
|
||||
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
|
||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
"book_tax_discount_loss": 1,
|
||||
},
|
||||
)
|
||||
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
|
||||
self,
|
||||
):
|
||||
"""
|
||||
1. Multi-currency SI with single currency accounting (company currency)
|
||||
2. PE with early payment discount
|
||||
3. Test if Paid Amount is calculated in company currency
|
||||
4. Test if deductions are calculated in company currency
|
||||
|
||||
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_save=1,
|
||||
)
|
||||
create_payment_terms_template_with_discount()
|
||||
si.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice",
|
||||
si.name,
|
||||
bank_account="_Test Bank - _TC",
|
||||
)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
# Early payment discount loss on income
|
||||
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
|
||||
self.assertEqual(pe.received_amount, 4500.0)
|
||||
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
self.assertEqual(pe.difference_amount, 0.0)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["Debtors - _TC", 0, 5000, si.name],
|
||||
["_Test Bank - _TC", 4500, 0, None],
|
||||
["Write Off - _TC", 500.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
|
||||
"""
|
||||
1. Multi-currency SI with multi-currency accounting
|
||||
2. PE with early payment discount and also exchange loss
|
||||
3. Test if Paid Amount is calculated in transaction currency
|
||||
4. Test if deductions are calculated in base/company currency
|
||||
5. Test if exchange loss is reflected in difference
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_save=1,
|
||||
)
|
||||
create_payment_terms_template_with_discount()
|
||||
si.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||
)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
# Early payment discount loss on income
|
||||
self.assertEqual(pe.paid_amount, 90.0)
|
||||
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
|
||||
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.difference_amount, 0.0)
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||
["_Test Bank - _TC", 4200, 0, None],
|
||||
["Write Off - _TC", 500.0, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
def test_payment_against_purchase_invoice_to_check_status(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
@@ -839,24 +1038,27 @@ def create_payment_terms_template():
|
||||
).insert()
|
||||
|
||||
|
||||
def create_payment_terms_template_with_discount():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
create_payment_term("30 Credit Days with 10% Discount")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
|
||||
payment_term_template = frappe.get_doc(
|
||||
if not frappe.db.exists("Payment Terms Template", template_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "Test Discount Template",
|
||||
"template_name": template_name,
|
||||
"allocate_payment_based_on_payment_terms": 1,
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "30 Credit Days with 10% Discount",
|
||||
"payment_term": name or "30 Credit Days with 10% Discount",
|
||||
"invoice_portion": 100,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 2,
|
||||
"discount": 10,
|
||||
"discount_type": discount_type or "Percentage",
|
||||
"discount": discount or 10,
|
||||
"discount_validity_based_on": "Day(s) after invoice date",
|
||||
"discount_validity": 1,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"creation": "2016-06-15 15:56:30.815503",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"cost_center",
|
||||
@@ -17,9 +18,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@@ -28,37 +27,30 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-12 20:38:08.110674",
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
@@ -66,5 +58,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
@@ -35,7 +36,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
@@ -104,7 +106,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 17:06:55.597389",
|
||||
"modified": "2022-12-12 12:31:44.919895",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
@@ -113,5 +115,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
frappe.ui.form.on('Payment Gateway Account', {
|
||||
refresh(frm) {
|
||||
erpnext.utils.check_payments_app();
|
||||
if(!frm.doc.__islocal) {
|
||||
frm.set_df_property('payment_gateway', 'read_only', 1);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
reconcile() {
|
||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount);
|
||||
|
||||
if (show_dialog && show_dialog.length) {
|
||||
|
||||
@@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
title: __("Select Difference Account"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
|
||||
data: this.data, in_place_edit: true,
|
||||
fieldname: "allocation",
|
||||
fieldtype: "Table",
|
||||
label: __("Allocation"),
|
||||
data: this.data,
|
||||
in_place_edit: true,
|
||||
cannot_add_rows: true,
|
||||
get_data: () => {
|
||||
return this.data;
|
||||
},
|
||||
@@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
read_only: 1
|
||||
}]
|
||||
},
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
options: "<b> New Journal Entry will be posted for the difference amount </b>"
|
||||
}
|
||||
],
|
||||
primary_action: () => {
|
||||
const args = dialog.get_values()["allocation"];
|
||||
@@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
});
|
||||
|
||||
this.frm.doc.allocation.forEach(d => {
|
||||
if (d.difference_amount && !d.difference_account) {
|
||||
if (d.difference_amount) {
|
||||
dialog.fields_dict.allocation.df.data.push({
|
||||
'docname': d.name,
|
||||
'reference_name': d.reference_name,
|
||||
@@ -264,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on('Payment Reconciliation Allocation', {
|
||||
allocated_amount: function(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
// filter invoice
|
||||
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
|
||||
// filter payment
|
||||
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'calculate_difference_on_allocation_change',
|
||||
args: {
|
||||
payment_entry: payment,
|
||||
invoice: invoice,
|
||||
allocated_amount: row.allocated_amount
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
row.difference_amount = r.message;
|
||||
frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
|
||||
|
||||
@@ -14,7 +14,6 @@ from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
update_reference_in_payment_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
@@ -23,6 +22,7 @@ class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -69,6 +69,10 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -79,12 +83,13 @@ class PaymentReconciliation(Document):
|
||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||
)
|
||||
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"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,
|
||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||
t2.account_currency as currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
@@ -193,6 +198,7 @@ class PaymentReconciliation(Document):
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
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,
|
||||
)
|
||||
|
||||
if self.invoice_limit:
|
||||
@@ -213,26 +219,38 @@ class PaymentReconciliation(Document):
|
||||
inv.currency = entry.get("currency")
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, allocated_entry):
|
||||
if allocated_entry.get("reference_type") != "Payment Entry":
|
||||
return
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
difference_amount = 0
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
return difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
|
||||
row = self.get_payment_details(allocated_entry, dr_or_cr)
|
||||
|
||||
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
|
||||
update_reference_in_payment_entry(row, doc, do_not_save=True)
|
||||
|
||||
return doc.difference_amount
|
||||
return new_difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
entries = []
|
||||
for pay in args.get("payments"):
|
||||
pay.update({"unreconciled_amount": pay.get("amount")})
|
||||
@@ -246,7 +264,13 @@ class PaymentReconciliation(Document):
|
||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||
pay["amount"] = 0
|
||||
|
||||
res.difference_amount = self.get_difference_amount(res)
|
||||
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
|
||||
if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
|
||||
pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
|
||||
|
||||
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")
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
@@ -276,6 +300,7 @@ class PaymentReconciliation(Document):
|
||||
"amount": pay.get("amount"),
|
||||
"allocated_amount": allocated_amount,
|
||||
"difference_amount": pay.get("difference_amount"),
|
||||
"currency": inv.get("currency"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -298,7 +323,11 @@ class PaymentReconciliation(Document):
|
||||
else:
|
||||
reconciled_entry = entry_list
|
||||
|
||||
reconciled_entry.append(self.get_payment_details(row, dr_or_cr))
|
||||
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if payment_details.difference_amount:
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list)
|
||||
@@ -309,6 +338,57 @@ class PaymentReconciliation(Document):
|
||||
msgprint(_("Successfully Reconciled"))
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def make_difference_entry(self, row):
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
journal_entry.company = self.company
|
||||
journal_entry.posting_date = nowdate()
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
party_account_currency = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
)
|
||||
difference_account_currency = frappe.get_cached_value(
|
||||
"Account", row.difference_account, "account_currency"
|
||||
)
|
||||
|
||||
# Account Currency has balance
|
||||
dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": self.receivable_payable_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"reference_type": row.against_voucher_type,
|
||||
"reference_name": row.against_voucher,
|
||||
dr_or_cr: flt(row.difference_amount),
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
{
|
||||
"account": row.difference_account,
|
||||
"account_currency": difference_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
|
||||
reverse_dr_or_cr: flt(row.difference_amount),
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
journal_entry.submit()
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -318,6 +398,7 @@ class PaymentReconciliation(Document):
|
||||
"against_voucher_type": row.get("invoice_type"),
|
||||
"against_voucher": row.get("invoice_number"),
|
||||
"account": self.receivable_payable_account,
|
||||
"exchange_rate": row.get("exchange_rate"),
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_advance": row.get("is_advance"),
|
||||
@@ -342,6 +423,49 @@ class PaymentReconciliation(Document):
|
||||
if not self.get("payments"):
|
||||
frappe.throw(_("No records found in the Payments table"))
|
||||
|
||||
def get_invoice_exchange_map(self, invoices, payments):
|
||||
sales_invoices = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
|
||||
]
|
||||
|
||||
sales_invoices.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"]
|
||||
)
|
||||
purchase_invoices = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
|
||||
]
|
||||
purchase_invoices.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"]
|
||||
)
|
||||
|
||||
invoice_exchange_map = frappe._dict()
|
||||
|
||||
if sales_invoices:
|
||||
sales_invoice_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"name": ("in", sales_invoices)},
|
||||
fields=["name", "conversion_rate"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
invoice_exchange_map.update(sales_invoice_map)
|
||||
|
||||
if purchase_invoices:
|
||||
purchase_invoice_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Purchase Invoice",
|
||||
filters={"name": ("in", purchase_invoices)},
|
||||
fields=["name", "conversion_rate"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
unreconciled_invoices = frappe._dict()
|
||||
|
||||
@@ -375,13 +499,14 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
self.accounting_dimension_filter_conditions.clear()
|
||||
self.ple_posting_date_filter.clear()
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
self.common_filter_conditions.append(ple.company == self.company)
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_return_invoices):
|
||||
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
|
||||
self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
if self.from_invoice_date:
|
||||
|
||||
@@ -5,9 +5,11 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
@@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.create_account()
|
||||
self.create_cost_center()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
@@ -72,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_customer(self):
|
||||
if frappe.db.exists("Customer", "_Test PR Customer"):
|
||||
self.customer = "_Test PR Customer"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
if frappe.db.exists("Customer", "_Test PR Customer 2"):
|
||||
self.customer2 = "_Test PR Customer 2"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer 2"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer2 = customer.name
|
||||
|
||||
if frappe.db.exists("Customer", "_Test PR Customer 3"):
|
||||
self.customer3 = "_Test PR Customer 3"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer 3"
|
||||
customer.type = "Individual"
|
||||
customer.default_currency = "EUR"
|
||||
customer.save()
|
||||
self.customer3 = customer.name
|
||||
self.customer = make_customer("_Test PR Customer")
|
||||
self.customer2 = make_customer("_Test PR Customer 2")
|
||||
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
|
||||
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
|
||||
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
||||
|
||||
def create_account(self):
|
||||
account_name = "Debtors EUR"
|
||||
@@ -216,6 +197,22 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
return je
|
||||
|
||||
def create_cost_center(self):
|
||||
# Setup cost center
|
||||
cc_name = "Sub"
|
||||
|
||||
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
|
||||
|
||||
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
|
||||
if cc_exists:
|
||||
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
|
||||
else:
|
||||
sub_cc = frappe.new_doc("Cost Center")
|
||||
sub_cc.cost_center_name = "Sub"
|
||||
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
|
||||
sub_cc.company = self.main_cc.company
|
||||
self.sub_cc = sub_cc.save()
|
||||
|
||||
def test_filter_min_max(self):
|
||||
# check filter condition minimum and maximum amount
|
||||
self.create_sales_invoice(qty=1, rate=300)
|
||||
@@ -352,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
si.reload()
|
||||
@@ -393,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# check PR tool output
|
||||
@@ -417,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
@@ -453,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
@@ -476,6 +493,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
@@ -509,6 +531,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = allocated_amount
|
||||
|
||||
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
@@ -578,3 +605,301 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
self.assertEqual(pr.payments[0].amount, amount)
|
||||
self.assertEqual(pr.payments[0].currency, "EUR")
|
||||
|
||||
def test_difference_amount_via_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = 0
|
||||
je1.accounts[0].credit = 0
|
||||
je1.accounts[0].debit_in_account_currency = 8000
|
||||
je1.accounts[0].debit = 8000
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = 0
|
||||
je2.accounts[0].credit = 0
|
||||
je2.accounts[0].debit_in_account_currency = 16000
|
||||
je2.accounts[0].debit = 16000
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_debit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(debit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
self.assertEqual(flt(total_debit_amount, 2), -500)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer5
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Payment Entry
|
||||
pe1 = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer5,
|
||||
paid_from=self.debtors_eur,
|
||||
paid_to=self.bank,
|
||||
paid_amount=100,
|
||||
)
|
||||
|
||||
pe1.source_exchange_rate = 80
|
||||
pe1.received_amount = 8000
|
||||
pe1.save()
|
||||
pe1.submit()
|
||||
|
||||
pe2 = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer5,
|
||||
paid_from=self.debtors_eur,
|
||||
paid_to=self.bank,
|
||||
paid_amount=200,
|
||||
)
|
||||
|
||||
pe2.source_exchange_rate = 80
|
||||
pe2.received_amount = 16000
|
||||
pe2.save()
|
||||
pe2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer5
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
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 = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
def test_differing_cost_center_on_invoice_and_payment(self):
|
||||
"""
|
||||
Cost Center filter should not affect outstanding amount calculation
|
||||
"""
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
|
||||
si.cost_center = self.main_cc.name
|
||||
si.submit()
|
||||
pr = get_payment_entry(si.doctype, si.name)
|
||||
pr.cost_center = self.sub_cc.name
|
||||
pr = pr.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.cost_center = self.main_cc.name
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_cost_center_filter_on_vouchers(self):
|
||||
"""
|
||||
Test Cost Center filter is applied on Invoices, Payment Entries and Journals
|
||||
"""
|
||||
transaction_date = nowdate()
|
||||
rate = 100
|
||||
|
||||
# 'Main - PR' Cost Center
|
||||
si1 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si1.cost_center = self.main_cc.name
|
||||
si1.submit()
|
||||
|
||||
pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
|
||||
pe1.cost_center = self.main_cc.name
|
||||
pe1 = pe1.save().submit()
|
||||
|
||||
je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
|
||||
je1.accounts[0].cost_center = self.main_cc.name
|
||||
je1.accounts[1].cost_center = self.main_cc.name
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer
|
||||
je1 = je1.save().submit()
|
||||
|
||||
# 'Sub - PR' Cost Center
|
||||
si2 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si2.cost_center = self.sub_cc.name
|
||||
si2.submit()
|
||||
|
||||
pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
|
||||
pe2.cost_center = self.sub_cc.name
|
||||
pe2 = pe2.save().submit()
|
||||
|
||||
je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
|
||||
je2.accounts[0].cost_center = self.sub_cc.name
|
||||
je2.accounts[1].cost_center = self.sub_cc.name
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer
|
||||
je2 = je2.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.cost_center = self.main_cc.name
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
||||
self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
|
||||
|
||||
# Change cost center
|
||||
pr.cost_center = self.sub_cc.name
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
||||
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_no_difference_amount_for_base_currency_accounts(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debit_to
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Payment Entry
|
||||
pe1 = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=100,
|
||||
)
|
||||
|
||||
pe1.save()
|
||||
pe1.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = self.debit_to
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
customer.type = "Individual"
|
||||
|
||||
if currency:
|
||||
customer.default_currency = currency
|
||||
customer.save()
|
||||
return customer.name
|
||||
else:
|
||||
return customer_name
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"column_break_7",
|
||||
"difference_account"
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -37,7 +39,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -112,7 +114,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Unreconciled Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -120,7 +122,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -129,11 +131,24 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified": "2022-12-24 21:01:14.882747",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
@@ -141,5 +156,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
"col_break1",
|
||||
"amount",
|
||||
"outstanding_amount",
|
||||
"currency"
|
||||
"currency",
|
||||
"exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -62,11 +63,17 @@
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Exchange Rate"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-24 22:42:40.923179",
|
||||
"modified": "2022-11-08 18:18:02.502149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Invoice",
|
||||
@@ -75,5 +82,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
"difference_amount",
|
||||
"sec_break1",
|
||||
"remark",
|
||||
"currency"
|
||||
"currency",
|
||||
"exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -91,11 +92,17 @@
|
||||
"label": "Difference Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Exchange Rate"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 10:51:48.140062",
|
||||
"modified": "2022-11-08 18:18:36.268760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
@@ -103,5 +110,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
||||
});
|
||||
}
|
||||
|
||||
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") {
|
||||
if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
|
||||
frm.add_custom_button(__('Create Payment Entry'), function(){
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
"iban",
|
||||
"branch_code",
|
||||
"swift_number",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"recipient_and_message",
|
||||
"print_format",
|
||||
"email_to",
|
||||
@@ -362,13 +366,35 @@
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-30 16:19:43.680025",
|
||||
"modified": "2022-12-21 16:56:40.115737",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -9,8 +9,10 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, get_url, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from payments.utils import get_payment_gateway_controller
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_company_defaults,
|
||||
get_payment_entry,
|
||||
@@ -19,6 +21,14 @@ from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_pla
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
def _get_payment_gateway_controller(*args, **kwargs):
|
||||
with payment_app_import_guard():
|
||||
from payments.utils import get_payment_gateway_controller
|
||||
|
||||
return get_payment_gateway_controller(*args, **kwargs)
|
||||
|
||||
|
||||
class PaymentRequest(Document):
|
||||
@@ -35,21 +45,20 @@ class PaymentRequest(Document):
|
||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||
|
||||
def validate_payment_request_amount(self):
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(
|
||||
self.reference_doctype, self.reference_name
|
||||
existing_payment_request_amount = flt(
|
||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||
)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
)
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
)
|
||||
)
|
||||
|
||||
def validate_currency(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
@@ -107,7 +116,7 @@ class PaymentRequest(Document):
|
||||
self.request_phone_payment()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
request_amount = self.get_request_amount()
|
||||
|
||||
payment_record = dict(
|
||||
@@ -156,7 +165,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def payment_gateway_validation(self):
|
||||
try:
|
||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
if hasattr(controller, "on_payment_request_submission"):
|
||||
return controller.on_payment_request_submission(self)
|
||||
else:
|
||||
@@ -189,7 +198,7 @@ class PaymentRequest(Document):
|
||||
)
|
||||
data.update({"company": frappe.defaults.get_defaults().company})
|
||||
|
||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
|
||||
if hasattr(controller, "validate_minimum_transaction_amount"):
|
||||
@@ -254,6 +263,7 @@ class PaymentRequest(Document):
|
||||
|
||||
payment_entry.update(
|
||||
{
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
|
||||
@@ -262,6 +272,17 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
# Update dimensions
|
||||
payment_entry.update(
|
||||
{
|
||||
"cost_center": self.get("cost_center"),
|
||||
"project": self.get("project"),
|
||||
}
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
if payment_entry.difference_amount:
|
||||
company_details = get_company_defaults(ref_doc.company)
|
||||
|
||||
@@ -403,25 +424,22 @@ def make_payment_request(**args):
|
||||
else ""
|
||||
)
|
||||
|
||||
existing_payment_request = None
|
||||
if args.order_type == "Shopping Cart":
|
||||
existing_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
|
||||
)
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
)
|
||||
|
||||
if existing_payment_request:
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
)
|
||||
pr = frappe.get_doc("Payment Request", existing_payment_request)
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
if args.order_type != "Shopping Cart":
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
pr.update(
|
||||
{
|
||||
@@ -444,6 +462,17 @@ def make_payment_request(**args):
|
||||
}
|
||||
)
|
||||
|
||||
# Update dimensions
|
||||
pr.update(
|
||||
{
|
||||
"cost_center": ref_doc.get("cost_center"),
|
||||
"project": ref_doc.get("project"),
|
||||
}
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
pr.update({dimension: ref_doc.get(dimension)})
|
||||
|
||||
if args.order_type == "Shopping Cart" or args.mute_email:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
@@ -466,26 +495,28 @@ def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
|
||||
|
||||
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
elif dt == "POS Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
|
||||
elif dt == "Fees":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@@ -45,7 +46,10 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR")
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
so_inr.disable_rounded_total = 1
|
||||
so_inr.save()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so_inr.name,
|
||||
@@ -71,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
self.assertEqual(pr.reference_name, si_usd.name)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
|
||||
def test_payment_entry_against_purchase_invoice(self):
|
||||
si_usd = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=si_usd.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
|
||||
def test_payment_entry(self):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
@@ -20,9 +21,17 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.make_gl_entries()
|
||||
get_opening_entries = False
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
|
||||
):
|
||||
get_opening_entries = True
|
||||
|
||||
self.make_gl_entries(get_opening_entries=get_opening_entries)
|
||||
|
||||
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")
|
||||
gle_count = frappe.db.count(
|
||||
@@ -42,6 +51,25 @@ class PeriodClosingVoucher(AccountsController):
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
self.delete_closing_entries()
|
||||
|
||||
def validate_future_closing_vouchers(self):
|
||||
if frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
|
||||
)
|
||||
)
|
||||
|
||||
def delete_closing_entries(self):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == self.name
|
||||
).run()
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
@@ -56,8 +84,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def validate_posting_date(self):
|
||||
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
|
||||
|
||||
validate_fiscal_year(
|
||||
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
|
||||
)
|
||||
@@ -66,6 +92,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.posting_date, self.fiscal_year, company=self.company
|
||||
)[1]
|
||||
|
||||
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""",
|
||||
@@ -78,28 +106,64 @@ class PeriodClosingVoucher(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries(self):
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.year_start_date, -1)
|
||||
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
|
||||
):
|
||||
return
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
|
||||
):
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
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 gl_entries:
|
||||
if len(gl_entries) > 5000:
|
||||
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
queue="long",
|
||||
)
|
||||
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)
|
||||
process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
|
||||
):
|
||||
closing_entries.append(self.get_closing_entries(acc))
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
|
||||
# pl account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
# closing liability account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=False, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||
|
||||
@@ -108,6 +172,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def get_gle_for_pl_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
@@ -120,6 +186,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
@@ -129,6 +196,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
@@ -141,12 +210,36 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
def get_closing_entries(self, acc):
|
||||
closing_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"period_closing_voucher": self.name,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": flt(acc.debit_in_account_currency),
|
||||
"debit": flt(acc.debit),
|
||||
"credit_in_account_currency": flt(acc.credit_in_account_currency),
|
||||
"credit": flt(acc.credit),
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
|
||||
for dimension in self.accounting_dimensions:
|
||||
closing_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
return closing_entry
|
||||
|
||||
def update_default_dimensions(self, gl_entry, acc):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
@@ -154,47 +247,88 @@ class PeriodClosingVoucher(AccountsController):
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
|
||||
def get_balances_based_on_dimensions(
|
||||
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
|
||||
):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
||||
qb_dimension_fields = ["cost_center", "finance_book", "project"]
|
||||
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
dimension_fields.append("t1.{0}".format(dimension))
|
||||
qb_dimension_fields.append(dimension)
|
||||
|
||||
if group_by_account:
|
||||
dimension_fields.append("t1.account")
|
||||
qb_dimension_fields.append("account")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t2.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
account_filters = {
|
||||
"company": self.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
|
||||
if report_type:
|
||||
account_filters.update({"report_type": report_type})
|
||||
|
||||
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
|
||||
|
||||
if not for_aggregation:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
|
||||
"bal_in_account_currency"
|
||||
),
|
||||
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
|
||||
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
|
||||
(Sum(gl_entry.debit)).as_("debit"),
|
||||
(Sum(gl_entry.credit)).as_("credit"),
|
||||
)
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.select(gl_entry[dimension])
|
||||
|
||||
query = query.where(
|
||||
(gl_entry.company == self.company)
|
||||
& (gl_entry.is_cancelled == 0)
|
||||
& (gl_entry.account.isin(accounts))
|
||||
)
|
||||
|
||||
if get_opening_entries:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
| gl_entry.is_opening
|
||||
== "Yes"
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
& gl_entry.is_opening
|
||||
== "No"
|
||||
)
|
||||
|
||||
def process_gl_entries(gl_entries):
|
||||
if for_aggregation:
|
||||
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.groupby(gl_entry[dimension])
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
|
||||
)
|
||||
|
||||
@@ -16,16 +16,17 @@ from erpnext.accounts.utils import get_fiscal_year, now
|
||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
def test_closing_entry(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
@@ -33,18 +34,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
jv1.submit()
|
||||
|
||||
jv2 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=600,
|
||||
account1="Cost of Goods Sold - TPC",
|
||||
account2="Cash - TPC",
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
pcv = self.make_period_closing_voucher()
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
surplus_account = pcv.closing_account_head
|
||||
|
||||
expected_gle = (
|
||||
@@ -65,6 +66,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
@@ -81,6 +83,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
@@ -91,9 +94,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(submit=False)
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
|
||||
pcv.save()
|
||||
pcv.submit()
|
||||
surplus_account = pcv.closing_account_head
|
||||
@@ -128,12 +132,13 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
def test_period_closing_with_finance_book_entries(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
si = create_sales_invoice(
|
||||
create_sales_invoice(
|
||||
company=company,
|
||||
income_account="Sales - TPC",
|
||||
expense_account="Cost of Goods Sold - TPC",
|
||||
@@ -142,6 +147,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
debit_to="Debtors - TPC",
|
||||
currency="USD",
|
||||
customer="_Test Customer USD",
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
@@ -149,14 +155,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
account2="Sales - TPC",
|
||||
amount=400,
|
||||
cost_center=cost_center,
|
||||
posting_date=now(),
|
||||
posting_date="2021-03-15",
|
||||
)
|
||||
jv.company = company
|
||||
jv.finance_book = create_finance_book().name
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
pcv = self.make_period_closing_voucher()
|
||||
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
surplus_account = pcv.closing_account_head
|
||||
|
||||
expected_gle = (
|
||||
@@ -177,14 +183,130 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
|
||||
def make_period_closing_voucher(self, submit=True):
|
||||
def test_gl_entries_restrictions(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
jv1.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv1.submit)
|
||||
|
||||
def test_closing_balance_with_dimensions(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center1 = create_cost_center("Test Cost Center 1")
|
||||
cost_center2 = create_cost_center("Test Cost Center 2")
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center1,
|
||||
save=False,
|
||||
)
|
||||
jv1.company = company
|
||||
jv1.save()
|
||||
jv1.submit()
|
||||
|
||||
jv2 = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=200,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
save=False,
|
||||
)
|
||||
jv2.company = company
|
||||
jv2.save()
|
||||
jv2.submit()
|
||||
|
||||
pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
closing_balance = frappe.db.get_value(
|
||||
"Account Closing Balance",
|
||||
{
|
||||
"account": "Sales - TPC",
|
||||
"cost_center": cost_center1,
|
||||
"period_closing_voucher": pcv1.name,
|
||||
"is_period_closing_voucher_entry": 0,
|
||||
},
|
||||
["credit", "credit_in_account_currency"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(closing_balance.credit, 400)
|
||||
self.assertEqual(closing_balance.credit_in_account_currency, 400)
|
||||
|
||||
jv3 = make_journal_entry(
|
||||
posting_date="2022-03-15",
|
||||
amount=300,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center2,
|
||||
save=False,
|
||||
)
|
||||
|
||||
jv3.company = company
|
||||
jv3.save()
|
||||
jv3.submit()
|
||||
|
||||
pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31")
|
||||
|
||||
cc1_closing_balance = frappe.db.get_value(
|
||||
"Account Closing Balance",
|
||||
{
|
||||
"account": "Sales - TPC",
|
||||
"cost_center": cost_center1,
|
||||
"period_closing_voucher": pcv2.name,
|
||||
"is_period_closing_voucher_entry": 0,
|
||||
},
|
||||
["credit", "credit_in_account_currency"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
cc2_closing_balance = frappe.db.get_value(
|
||||
"Account Closing Balance",
|
||||
{
|
||||
"account": "Sales - TPC",
|
||||
"cost_center": cost_center2,
|
||||
"period_closing_voucher": pcv2.name,
|
||||
"is_period_closing_voucher_entry": 0,
|
||||
},
|
||||
["credit", "credit_in_account_currency"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(cc1_closing_balance.credit, 400)
|
||||
self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400)
|
||||
self.assertEqual(cc2_closing_balance.credit, 500)
|
||||
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
|
||||
|
||||
def make_period_closing_voucher(self, posting_date=None, submit=True):
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"company": "Test PCV Company",
|
||||
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
|
||||
"cost_center": cost_center,
|
||||
|
||||
@@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
self.validate_duplicate_pos_invoices()
|
||||
self.validate_pos_invoices()
|
||||
|
||||
def validate_duplicate_pos_invoices(self):
|
||||
pos_occurences = {}
|
||||
for idx, inv in enumerate(self.pos_transactions, 1):
|
||||
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
|
||||
|
||||
error_list = []
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
|
||||
|
||||
def validate_pos_invoices(self):
|
||||
invalid_rows = []
|
||||
for d in self.pos_transactions:
|
||||
|
||||
@@ -112,7 +112,8 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
party_type: "Customer",
|
||||
account: this.frm.doc.debit_to,
|
||||
price_list: this.frm.doc.selling_price_list,
|
||||
pos_profile: pos_profile
|
||||
pos_profile: pos_profile,
|
||||
company_address: this.frm.doc.company_address
|
||||
}, () => {
|
||||
this.apply_pricing_rule();
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
bold_extra_batch_qty_needed = frappe.bold(
|
||||
abs(available_batch_qty - reserved_batch_qty - item.qty)
|
||||
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
|
||||
)
|
||||
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||
|
||||
@@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
|
||||
).format(item.idx, bold_invalid_batch_no, bold_item_name),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
|
||||
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
|
||||
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.qty):
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
@@ -651,7 +651,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.qty
|
||||
max_available_bundles = available_qty / item.stock_qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
@@ -675,7 +675,7 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql(
|
||||
"""select sum(p_item.qty) as qty
|
||||
"""select sum(p_item.stock_qty) as qty
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||
where p.name = p_item.parent
|
||||
and ifnull(p.consolidated_invoice, '') = ''
|
||||
|
||||
@@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document):
|
||||
def validate(self):
|
||||
self.validate_customer()
|
||||
self.validate_pos_invoice_status()
|
||||
self.validate_duplicate_pos_invoices()
|
||||
|
||||
def validate_duplicate_pos_invoices(self):
|
||||
pos_occurences = {}
|
||||
for idx, inv in enumerate(self.pos_invoices, 1):
|
||||
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
|
||||
|
||||
error_list = []
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
|
||||
|
||||
def validate_customer(self):
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
@@ -425,6 +441,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if type(error_message) == list:
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@
|
||||
"description": "If rate is zero them item will be treated as \"Free Item\"",
|
||||
"fieldname": "free_item_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate"
|
||||
"label": "Free Item Rate"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -608,7 +608,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-13 19:05:35.056304",
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -10,7 +10,7 @@ import re
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
|
||||
|
||||
@@ -184,8 +184,7 @@ class PricingRule(Document):
|
||||
if self.is_cumulative and not (self.valid_from and self.valid_upto):
|
||||
frappe.throw(_("Valid from and valid upto fields are mandatory for the cumulative"))
|
||||
|
||||
if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto):
|
||||
frappe.throw(_("Valid from date must be less than valid upto date"))
|
||||
self.validate_from_to_dates("valid_from", "valid_upto")
|
||||
|
||||
def validate_condition(self):
|
||||
if (
|
||||
@@ -256,7 +255,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
for item in item_list:
|
||||
args_copy = copy.deepcopy(args)
|
||||
args_copy.update(item)
|
||||
data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
|
||||
data = get_pricing_rule_for_item(args_copy, doc=doc)
|
||||
out.append(data)
|
||||
|
||||
if (
|
||||
@@ -293,7 +292,7 @@ def update_pricing_rule_uom(pricing_rule, args):
|
||||
pricing_rule.uom = row.uom
|
||||
|
||||
|
||||
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
|
||||
def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
get_pricing_rule_items,
|
||||
|
||||
@@ -1123,7 +1123,7 @@ def make_pricing_rule(**args):
|
||||
"apply_on": args.apply_on or "Item Code",
|
||||
"applicable_for": args.applicable_for,
|
||||
"selling": args.selling or 0,
|
||||
"currency": "USD",
|
||||
"currency": "INR",
|
||||
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
|
||||
"buying": args.buying or 0,
|
||||
"min_qty": args.min_qty or 0.0,
|
||||
|
||||
@@ -250,6 +250,22 @@ def get_other_conditions(conditions, values, args):
|
||||
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
|
||||
values["transaction_date"] = args.get("transaction_date")
|
||||
|
||||
if args.get("doctype") in [
|
||||
"Quotation",
|
||||
"Quotation Item",
|
||||
"Sales Order",
|
||||
"Sales Order Item",
|
||||
"Delivery Note",
|
||||
"Delivery Note Item",
|
||||
"Sales Invoice",
|
||||
"Sales Invoice Item",
|
||||
"POS Invoice",
|
||||
"POS Invoice Item",
|
||||
]:
|
||||
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
|
||||
else:
|
||||
conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
@@ -669,13 +685,23 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
item_details.free_item_data.append(free_item_data_args)
|
||||
|
||||
|
||||
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
|
||||
def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
|
||||
if pricing_rule_args:
|
||||
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
|
||||
args = {(d["item_code"], d["pricing_rules"]): d for d in pricing_rule_args}
|
||||
|
||||
for args in pricing_rule_args:
|
||||
if not items or (args.get("item_code"), args.get("pricing_rules")) not in items:
|
||||
doc.append("items", args)
|
||||
for item in doc.items:
|
||||
if not item.is_free_item:
|
||||
continue
|
||||
|
||||
free_item_data = args.get((item.item_code, item.pricing_rules))
|
||||
if free_item_data:
|
||||
free_item_data.pop("item_name")
|
||||
free_item_data.pop("description")
|
||||
item.update(free_item_data)
|
||||
args.pop((item.item_code, item.pricing_rules))
|
||||
|
||||
for free_item in args.values():
|
||||
doc.append("items", free_item)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
@@ -49,7 +49,6 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{{ _("Against") }}: {{ row.against }}
|
||||
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
|
||||
@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
|
||||
class ProcessStatementOfAccounts(Document):
|
||||
def validate(self):
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.name }}"
|
||||
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 }}."
|
||||
|
||||
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"currency": doc.currency,
|
||||
@@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
||||
]
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[[fields_dict[customer_collection], "IN", selected]],
|
||||
)
|
||||
|
||||
@@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person):
|
||||
if sales_person_records.get("Customer"):
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["name", "in", list(sales_person_records["Customer"])]],
|
||||
)
|
||||
else:
|
||||
@@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if customer_collection == "Sales Partner":
|
||||
customers = frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "email_id"],
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[["default_sales_partner", "=", collection_name]],
|
||||
)
|
||||
else:
|
||||
@@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
continue
|
||||
|
||||
customer_list.append(
|
||||
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
|
||||
{
|
||||
"name": customer.name,
|
||||
"customer_name": customer.customer_name,
|
||||
"primary_email": primary_email,
|
||||
"billing_email": billing_email,
|
||||
}
|
||||
)
|
||||
return customer_list
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_workflow": 1,
|
||||
"creation": "2020-08-03 16:35:21.852178",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"customer_name",
|
||||
"billing_email",
|
||||
"primary_email"
|
||||
],
|
||||
@@ -30,11 +30,18 @@
|
||||
"fieldtype": "Read Only",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 22:55:38.875601",
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
@@ -43,5 +50,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -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'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,13 @@
|
||||
"tax_withholding_net_total",
|
||||
"base_tax_withholding_net_total",
|
||||
"taxes_section",
|
||||
"tax_category",
|
||||
"taxes_and_charges",
|
||||
"column_break_58",
|
||||
"tax_category",
|
||||
"column_break_49",
|
||||
"shipping_rule",
|
||||
"column_break_49",
|
||||
"incoterm",
|
||||
"named_place",
|
||||
"section_break_51",
|
||||
"taxes",
|
||||
"totals",
|
||||
@@ -117,6 +118,7 @@
|
||||
"paid_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
@@ -1425,6 +1427,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "apply_tds",
|
||||
"fieldname": "tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
@@ -1434,12 +1437,13 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "apply_tds",
|
||||
"fieldname": "base_tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Tax Withholding Net Total",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1541,17 +1545,30 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Incoterm",
|
||||
"options": "Incoterm"
|
||||
},
|
||||
{
|
||||
"depends_on": "incoterm",
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "allocate_advances_automatically",
|
||||
"description": "Advance payments allocated against orders will only be fetched",
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-25 12:44:29.935567",
|
||||
"modified": "2023-04-03 22:57:14.074982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
@@ -116,7 +117,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.validate_expense_account()
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.create_remarks()
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
@@ -231,7 +232,9 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if (
|
||||
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return
|
||||
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
|
||||
and not self.is_return
|
||||
and not self.is_internal_supplier
|
||||
):
|
||||
self.validate_rate_with_reference_doc(
|
||||
[
|
||||
@@ -578,6 +581,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
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)
|
||||
@@ -972,6 +976,28 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -1414,6 +1440,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
@@ -1461,19 +1489,16 @@ class PurchaseInvoice(BuyingController):
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
updated_pr = []
|
||||
po_details = []
|
||||
|
||||
pr_details_billed_amt = self.get_pr_details_billed_amt()
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.pr_detail:
|
||||
billed_amt = frappe.db.sql(
|
||||
"""select sum(amount) from `tabPurchase Invoice Item`
|
||||
where pr_detail=%s and docstatus=1""",
|
||||
d.pr_detail,
|
||||
)
|
||||
billed_amt = billed_amt and billed_amt[0][0] or 0
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
d.pr_detail,
|
||||
"billed_amt",
|
||||
billed_amt,
|
||||
flt(pr_details_billed_amt.get(d.pr_detail)),
|
||||
update_modified=update_modified,
|
||||
)
|
||||
updated_pr.append(d.purchase_receipt)
|
||||
@@ -1483,11 +1508,35 @@ class PurchaseInvoice(BuyingController):
|
||||
if po_details:
|
||||
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
|
||||
|
||||
adjust_incoming_rate = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
for pr in set(updated_pr):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
|
||||
|
||||
pr_doc = frappe.get_doc("Purchase Receipt", pr)
|
||||
update_billing_percentage(pr_doc, update_modified=update_modified)
|
||||
update_billing_percentage(
|
||||
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
|
||||
)
|
||||
|
||||
def get_pr_details_billed_amt(self):
|
||||
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
|
||||
|
||||
pr_details_billed_amt = {}
|
||||
pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")]
|
||||
if pr_details:
|
||||
doctype = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(doctype.pr_detail, Sum(doctype.amount))
|
||||
.where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
|
||||
.groupby(doctype.pr_detail)
|
||||
)
|
||||
|
||||
pr_details_billed_amt = frappe._dict(query.run(as_list=1))
|
||||
|
||||
return pr_details_billed_amt
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.due_date = None
|
||||
|
||||
@@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def test_adjust_incoming_rate(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
|
||||
)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Reduce the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 50
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 50)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
|
||||
)
|
||||
|
||||
# Don't adjust incoming rate
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 50
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 100)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
|
||||
@@ -7,7 +7,6 @@ import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils.background_jobs import is_job_queued
|
||||
|
||||
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
|
||||
|
||||
@@ -27,7 +26,7 @@ def start_payment_ledger_repost(docname=None):
|
||||
"""
|
||||
if docname:
|
||||
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
|
||||
if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
|
||||
if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
|
||||
try:
|
||||
for entry in repost_doc.repost_vouchers:
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
@@ -102,10 +101,9 @@ def execute_repost_payment_ledger(docname):
|
||||
|
||||
job_name = "payment_ledger_repost_" + docname
|
||||
|
||||
if not is_job_queued(job_name):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
|
||||
docname=docname,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
|
||||
docname=docname,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
|
||||
@@ -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'];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||
cur_frm.add_custom_button(__('Payment'),
|
||||
this.make_payment_entry, __('Create'));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
if(doc.docstatus==1 && !doc.is_return) {
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"column_break_27",
|
||||
"campaign",
|
||||
"source",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -61,12 +58,13 @@
|
||||
"total",
|
||||
"net_total",
|
||||
"taxes_section",
|
||||
"tax_category",
|
||||
"taxes_and_charges",
|
||||
"column_break_38",
|
||||
"shipping_rule",
|
||||
"incoterm",
|
||||
"column_break_55",
|
||||
"tax_category",
|
||||
"incoterm",
|
||||
"named_place",
|
||||
"section_break_40",
|
||||
"taxes",
|
||||
"section_break_43",
|
||||
@@ -122,6 +120,7 @@
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"write_off_section",
|
||||
@@ -202,7 +201,9 @@
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"campaign",
|
||||
"represents_company",
|
||||
"source",
|
||||
"customer_group",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
@@ -921,6 +922,7 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Sales Taxes and Charges",
|
||||
"oldfieldname": "other_charges",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Taxes and Charges"
|
||||
@@ -1774,6 +1776,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"hide_days": 1,
|
||||
@@ -2079,10 +2083,6 @@
|
||||
"fieldname": "company_addr_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_52",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -2121,6 +2121,20 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Incoterm",
|
||||
"options": "Incoterm"
|
||||
},
|
||||
{
|
||||
"depends_on": "incoterm",
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "allocate_advances_automatically",
|
||||
"description": "Advance payments allocated against orders will only be fetched",
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2133,11 +2147,10 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-11-17 17:17:10.883487",
|
||||
"modified": "2023-04-03 22:55:14.206473",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if not self.is_return:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
@@ -397,6 +397,8 @@ class SalesInvoice(SellingController):
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
@@ -1185,11 +1187,24 @@ class SalesInvoice(SellingController):
|
||||
if asset.calculate_depreciation:
|
||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
reset_depreciation_schedule(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
|
||||
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
|
||||
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
@@ -263,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36],
|
||||
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
|
||||
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
|
||||
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17],
|
||||
"_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55],
|
||||
"_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
|
||||
"_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1627.5)
|
||||
self.assertEqual(si.grand_total, 32.55)
|
||||
self.assertEqual(si.base_grand_total, 1627.0)
|
||||
self.assertEqual(si.grand_total, 32.54)
|
||||
|
||||
def test_sales_invoice_with_discount_and_inclusive_tax(self):
|
||||
si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
|
||||
@@ -398,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67],
|
||||
"_Test Account CST - _TC": [27.88, 25.95, 1323.62],
|
||||
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
|
||||
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40],
|
||||
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40],
|
||||
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86],
|
||||
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01],
|
||||
"_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
|
||||
"_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
|
||||
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
|
||||
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@@ -410,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
@@ -451,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
[test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
|
||||
[test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
|
||||
[test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
|
||||
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.35],
|
||||
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.34],
|
||||
[test_records[3]["taxes"][6]["account_head"], 0.0, 100],
|
||||
[test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
|
||||
["_Test Account Service Tax - _TC", 16.85, 0.0],
|
||||
@@ -1166,6 +1169,46 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
update_stock=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si.transaction_date = nowdate()
|
||||
si.save()
|
||||
|
||||
packed_item = si.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_pos_si_without_payment(self):
|
||||
make_pos_profile()
|
||||
|
||||
@@ -1571,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
|
||||
"_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
|
||||
"_Test Account CST - _TC": [17.19, 17.19, 17.19],
|
||||
"_Test Account VAT - _TC": [78.13, 78.13, 78.13],
|
||||
"_Test Account VAT - _TC": [78.12, 78.12, 78.12],
|
||||
"_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
|
||||
}
|
||||
|
||||
@@ -1580,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
if expected_values.get(d.account_head):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.total_taxes_and_charges, 234.43)
|
||||
self.assertEqual(si.base_grand_total, 859.43)
|
||||
self.assertEqual(si.grand_total, 859.43)
|
||||
self.assertEqual(si.total_taxes_and_charges, 234.42)
|
||||
self.assertEqual(si.base_grand_total, 859.42)
|
||||
self.assertEqual(si.grand_total, 859.42)
|
||||
|
||||
def test_multi_currency_gle(self):
|
||||
si = create_sales_invoice(
|
||||
@@ -1942,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 19453.13)
|
||||
self.assertEqual(si.net_total, 19453.12)
|
||||
self.assertEqual(si.grand_total, 24900)
|
||||
self.assertEqual(si.total_taxes_and_charges, 5446.88)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
[si.debit_to, 24900, 0.0],
|
||||
["_Test Account Service Tax - _TC", 0.0, 5446.88],
|
||||
["Sales - _TC", 0.0, 19453.13],
|
||||
["Sales - _TC", 0.0, 19453.12],
|
||||
["Round Off - _TC", 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
@@ -2774,7 +2817,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
["2021-09-30", 5041.1, 26407.22],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
@@ -2805,7 +2848,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
@@ -2834,7 +2877,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
["2025-06-06", 18633.88, 100000.0, False],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
|
||||
@@ -890,7 +890,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-02 12:53:12.693217",
|
||||
"modified": "2022-12-28 16:17:33.484531",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -280,7 +280,8 @@ class Subscription(Document):
|
||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||
self.validate_end_date()
|
||||
self.validate_to_follow_calendar_months()
|
||||
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
|
||||
if not self.cost_center:
|
||||
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
|
||||
|
||||
def validate_trial_period(self):
|
||||
"""
|
||||
|
||||
@@ -5,5 +5,9 @@ frappe.ui.form.on('Subscription Plan', {
|
||||
price_determination: function(frm) {
|
||||
frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate');
|
||||
frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list');
|
||||
}
|
||||
},
|
||||
|
||||
subscription_plan: function (frm) {
|
||||
erpnext.utils.check_payments_app();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ class TaxRule(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_tax_template()
|
||||
self.validate_date()
|
||||
self.validate_from_to_dates("from_date", "to_date")
|
||||
self.validate_filters()
|
||||
self.validate_use_for_shopping_cart()
|
||||
|
||||
@@ -51,10 +51,6 @@ class TaxRule(Document):
|
||||
if not (self.sales_tax_template or self.purchase_tax_template):
|
||||
frappe.throw(_("Tax Template is mandatory."))
|
||||
|
||||
def validate_date(self):
|
||||
if self.from_date and self.to_date and self.from_date > self.to_date:
|
||||
frappe.throw(_("From Date cannot be greater than To Date"))
|
||||
|
||||
def validate_filters(self):
|
||||
filters = {
|
||||
"tax_type": self.tax_type,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"autoname": "hash",
|
||||
"creation": "2022-09-13 16:18:59.404842",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -36,11 +36,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 23:40:41.479208",
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
"naming_rule": "Autoincrement",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
else:
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
|
||||
cost_center = get_cost_center(inv)
|
||||
tax_row.update({"cost_center": cost_center})
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
return tax_row
|
||||
|
||||
|
||||
def get_cost_center(inv):
|
||||
cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
|
||||
|
||||
if len(inv.get("taxes", [])) > 0:
|
||||
cost_center = inv.get("taxes")[0].cost_center
|
||||
|
||||
return cost_center
|
||||
|
||||
|
||||
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
|
||||
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
|
||||
|
||||
@@ -247,9 +259,7 @@ 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, pan_no, tax_details, posting_date, net_total
|
||||
)
|
||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
|
||||
@@ -268,7 +278,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
tax_amount = normal_round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
@@ -400,12 +410,26 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
## for TDS to be deducted on advances
|
||||
payment_entry_filters = {
|
||||
"party_type": "Supplier",
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
|
||||
if cint(tax_details.consider_party_ledger_amount):
|
||||
invoice_filters.pop("apply_tds", None)
|
||||
field = "sum(grand_total)"
|
||||
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
@@ -417,14 +441,28 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
"party": ("in", parties),
|
||||
"reference_type": ("!=", "Purchase Invoice"),
|
||||
},
|
||||
"sum(credit_in_account_currency)",
|
||||
"sum(credit_in_account_currency - debit_in_account_currency)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
# Get Amount via payment entry
|
||||
payment_entry_amounts = frappe.db.get_all(
|
||||
"Payment Entry",
|
||||
filters=payment_entry_filters,
|
||||
fields=["sum(unallocated_amount) as amount", "payment_type"],
|
||||
group_by="payment_type",
|
||||
)
|
||||
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
supp_credit_amt += type.amount
|
||||
else:
|
||||
supp_credit_amt -= type.amount
|
||||
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
@@ -526,7 +564,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, pan_no, tax_details, posting_date, net_total):
|
||||
def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
@@ -565,3 +603,20 @@ def is_valid_certificate(
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def normal_round(number):
|
||||
"""
|
||||
Rounds a number to the nearest integer.
|
||||
:param number: The number to round.
|
||||
"""
|
||||
decimal_part = number - int(number)
|
||||
|
||||
if decimal_part >= 0.5:
|
||||
decimal_part = 1
|
||||
else:
|
||||
decimal_part = 0
|
||||
|
||||
number = int(number) + decimal_part
|
||||
|
||||
return number
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
def setUpClass(self):
|
||||
# create relevant supplier, etc
|
||||
create_records()
|
||||
create_tax_with_holding_category()
|
||||
create_tax_withholding_category_records()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
@@ -38,7 +38,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier")
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount uptil now
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 3000)
|
||||
self.assertEqual(pi.grand_total, 7000)
|
||||
invoices.append(pi)
|
||||
@@ -47,7 +47,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount uptil now
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 500)
|
||||
invoices.append(pi)
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
invoices.append(si)
|
||||
|
||||
# create another invoice whose total when added to previously created invoice,
|
||||
# surpasses cumulative threshhold
|
||||
# surpasses cumulative threshold
|
||||
si = create_sales_invoice(customer="Test TCS Customer", rate=12000)
|
||||
si.submit()
|
||||
|
||||
@@ -329,6 +329,38 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_via_payment_entry_for_advances(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier7", "tax_withholding_category", "Advance TDS Category"
|
||||
)
|
||||
|
||||
# create payment entry
|
||||
pe1 = create_payment_entry(
|
||||
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
|
||||
)
|
||||
pe1.submit()
|
||||
|
||||
self.assertFalse(pe1.get("taxes"))
|
||||
|
||||
pe2 = create_payment_entry(
|
||||
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
|
||||
)
|
||||
pe2.submit()
|
||||
|
||||
self.assertFalse(pe2.get("taxes"))
|
||||
|
||||
pe3 = create_payment_entry(
|
||||
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
|
||||
)
|
||||
pe3.apply_tax_withholding_amount = 1
|
||||
pe3.save()
|
||||
pe3.submit()
|
||||
|
||||
self.assertEquals(pe3.get("taxes")[0].tax_amount, 1200)
|
||||
pe1.cancel()
|
||||
pe2.cancel()
|
||||
pe3.cancel()
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
@@ -450,6 +482,32 @@ def create_sales_invoice(**args):
|
||||
return si
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
# return payment entry doc object
|
||||
args = frappe._dict(args)
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"posting_date": today(),
|
||||
"payment_type": args.payment_type,
|
||||
"party_type": args.party_type,
|
||||
"party": args.party,
|
||||
"company": "_Test Company",
|
||||
"paid_from": "Cash - _TC",
|
||||
"paid_to": "Creditors - _TC",
|
||||
"paid_amount": args.paid_amount or 10000,
|
||||
"received_amount": args.paid_amount or 10000,
|
||||
"reference_no": args.reference_no or "12345",
|
||||
"reference_date": today(),
|
||||
"paid_from_account_currency": "INR",
|
||||
"paid_to_account_currency": "INR",
|
||||
}
|
||||
)
|
||||
|
||||
pe.save()
|
||||
return pe
|
||||
|
||||
|
||||
def create_records():
|
||||
# create a new suppliers
|
||||
for name in [
|
||||
@@ -460,6 +518,7 @@ def create_records():
|
||||
"Test TDS Supplier4",
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
@@ -530,142 +589,129 @@ def create_records():
|
||||
).insert()
|
||||
|
||||
|
||||
def create_tax_with_holding_category():
|
||||
def create_tax_withholding_category_records():
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
from_date = fiscal_year[1]
|
||||
to_date = fiscal_year[2]
|
||||
|
||||
# Cumulative threshold
|
||||
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Cumulative Threshold TDS",
|
||||
"category_name": "10% TDS",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000.00,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
create_tax_withholding_category(
|
||||
category_name="Cumulative Threshold TDS",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=0,
|
||||
cumulative_threshold=30000.00,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Cumulative Threshold TCS",
|
||||
"category_name": "10% TCS",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000.00,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TCS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
# Category for TCS
|
||||
create_tax_withholding_category(
|
||||
category_name="Cumulative Threshold TCS",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TCS - _TC",
|
||||
single_threshold=0,
|
||||
cumulative_threshold=30000.00,
|
||||
)
|
||||
|
||||
# Single thresold
|
||||
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Single Threshold TDS",
|
||||
"category_name": "10% TDS",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 20000.00,
|
||||
"cumulative_threshold": 0,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
# Single threshold
|
||||
create_tax_withholding_category(
|
||||
category_name="Single Threshold TDS",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=20000,
|
||||
cumulative_threshold=0,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "New TDS Category",
|
||||
"category_name": "New TDS Category",
|
||||
"round_off_tax_amount": 1,
|
||||
"consider_party_ledger_amount": 1,
|
||||
"tax_on_excess_amount": 1,
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
create_tax_withholding_category(
|
||||
category_name="New TDS Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=0,
|
||||
cumulative_threshold=30000,
|
||||
round_off_tax_amount=1,
|
||||
consider_party_ledger_amount=1,
|
||||
tax_on_excess_amount=1,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Service Category",
|
||||
"category_name": "Test Service Category",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 2000,
|
||||
"cumulative_threshold": 2000,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
create_tax_withholding_category(
|
||||
category_name="Test Service Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=2000,
|
||||
cumulative_threshold=2000,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Goods Category",
|
||||
"category_name": "Test Goods Category",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 2000,
|
||||
"cumulative_threshold": 2000,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
create_tax_withholding_category(
|
||||
category_name="Test Goods Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=2000,
|
||||
cumulative_threshold=2000,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"):
|
||||
create_tax_withholding_category(
|
||||
category_name="Test Multi Invoice Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=5000,
|
||||
cumulative_threshold=10000,
|
||||
)
|
||||
|
||||
create_tax_withholding_category(
|
||||
category_name="Advance TDS Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=5000,
|
||||
cumulative_threshold=10000,
|
||||
consider_party_ledger_amount=1,
|
||||
)
|
||||
|
||||
|
||||
def create_tax_withholding_category(
|
||||
category_name,
|
||||
rate,
|
||||
from_date,
|
||||
to_date,
|
||||
account,
|
||||
single_threshold=0,
|
||||
cumulative_threshold=0,
|
||||
round_off_tax_amount=0,
|
||||
consider_party_ledger_amount=0,
|
||||
tax_on_excess_amount=0,
|
||||
):
|
||||
if not frappe.db.exists("Tax Withholding Category", category_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Multi Invoice Category",
|
||||
"category_name": "Test Multi Invoice Category",
|
||||
"name": category_name,
|
||||
"category_name": category_name,
|
||||
"round_off_tax_amount": round_off_tax_amount,
|
||||
"consider_party_ledger_amount": consider_party_ledger_amount,
|
||||
"tax_on_excess_amount": tax_on_excess_amount,
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 5000,
|
||||
"cumulative_threshold": 10000,
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"tax_withholding_rate": rate,
|
||||
"single_threshold": single_threshold,
|
||||
"cumulative_threshold": cumulative_threshold,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
"accounts": [{"company": "_Test Company", "account": account}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -199,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
# filter zero debit and credit entries
|
||||
merged_gl_map = filter(
|
||||
lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map
|
||||
lambda x: flt(x.debit, precision) != 0
|
||||
or flt(x.credit, precision) != 0
|
||||
or (
|
||||
x.voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
),
|
||||
merged_gl_map,
|
||||
)
|
||||
merged_gl_map = list(merged_gl_map)
|
||||
|
||||
@@ -293,6 +300,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
|
||||
if gl_map:
|
||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
||||
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
|
||||
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||
|
||||
for entry in gl_map:
|
||||
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
||||
@@ -350,15 +360,26 @@ def process_debit_credit_difference(gl_map):
|
||||
allowance = get_debit_credit_allowance(voucher_type, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
if not (
|
||||
voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
):
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
|
||||
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
|
||||
make_round_off_gle(gl_map, debit_credit_diff, precision)
|
||||
|
||||
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
if not (
|
||||
voucher_type == "Journal Entry"
|
||||
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
|
||||
== "Exchange Gain Or Loss"
|
||||
):
|
||||
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
|
||||
|
||||
|
||||
def get_debit_credit_difference(gl_map, precision):
|
||||
@@ -501,6 +522,9 @@ def make_reverse_gl_entries(
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
||||
for entry in gl_entries:
|
||||
@@ -548,6 +572,28 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
)
|
||||
|
||||
|
||||
def validate_against_pcv(is_opening, posting_date, company):
|
||||
if is_opening and frappe.db.exists(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}
|
||||
):
|
||||
frappe.throw(
|
||||
_("Opening Entry can not be created after Period Closing Voucher is created."),
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
message = _("Books have been closed till the period ending on {0}").format(
|
||||
formatdate(last_pcv_date)
|
||||
)
|
||||
message += "</br >"
|
||||
message += _("You cannot create/amend any accounting entries till this date.")
|
||||
frappe.throw(message, title=_("Period Closed"))
|
||||
|
||||
|
||||
def set_as_cancel(voucher_type, voucher_no):
|
||||
"""
|
||||
Set is_cancelled=1 in all original gl entries for the voucher
|
||||
|
||||
@@ -32,6 +32,16 @@ from erpnext import get_company_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
|
||||
SALES_TRANSACTION_TYPES = {
|
||||
"Quotation",
|
||||
"Sales Order",
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"POS Invoice",
|
||||
}
|
||||
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
|
||||
|
||||
|
||||
class DuplicatePartyAccountError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -124,12 +134,6 @@ def _get_party_details(
|
||||
set_other_values(party_details, party, party_type)
|
||||
set_price_list(party_details, party, party_type, price_list, pos_profile)
|
||||
|
||||
party_details["tax_category"] = get_address_tax_category(
|
||||
party.get("tax_category"),
|
||||
party_address,
|
||||
shipping_address if party_type != "Supplier" else party_address,
|
||||
)
|
||||
|
||||
tax_template = set_taxes(
|
||||
party.name,
|
||||
party_type,
|
||||
@@ -170,6 +174,9 @@ def _get_party_details(
|
||||
party_type, party.name, "tax_withholding_category"
|
||||
)
|
||||
|
||||
if not party_details.get("tax_category") and pos_profile:
|
||||
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
|
||||
|
||||
return party_details
|
||||
|
||||
|
||||
@@ -211,14 +218,10 @@ def set_address_details(
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
|
||||
if party_details.company_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "company_address", party_details.company_address)
|
||||
)
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
|
||||
party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if doctype in PURCHASE_TRANSACTION_TYPES:
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
@@ -244,9 +247,21 @@ def set_address_details(
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
|
||||
)
|
||||
|
||||
party_address, shipping_address = (
|
||||
party_details.get(billing_address_field),
|
||||
party_details.shipping_address_name,
|
||||
)
|
||||
|
||||
party_details["tax_category"] = get_address_tax_category(
|
||||
party.get("tax_category"),
|
||||
party_address,
|
||||
shipping_address if party_type != "Supplier" else party_address,
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
return party_address, shipping_address
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
@@ -544,7 +559,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
|
||||
else:
|
||||
due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months))
|
||||
due_date = max(due_date, get_last_day(add_months(due_date, term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,9 @@ class ReceivablePayableReport(object):
|
||||
# Get return entries
|
||||
self.get_return_entries()
|
||||
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.data = []
|
||||
|
||||
for ple in self.ple_entries:
|
||||
@@ -251,7 +254,8 @@ class ReceivablePayableReport(object):
|
||||
row.invoice_grand_total = row.invoiced
|
||||
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
|
||||
# non-zero oustanding, we must consider this row
|
||||
@@ -794,19 +798,19 @@ class ReceivablePayableReport(object):
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party_isin(
|
||||
qb.from_(self.customer).where(
|
||||
self.customer.payment_terms == self.filters.get("payment_terms_template")
|
||||
)
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party_isin(
|
||||
qb.from_(self.customer).where(
|
||||
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
|
||||
)
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.default_sales_partner == self.filters.get("sales_partner"))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -855,7 +859,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
else:
|
||||
self.qb_selection_filter.append(
|
||||
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
|
||||
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
|
||||
)
|
||||
|
||||
def is_invoice(self, ple):
|
||||
@@ -865,10 +869,15 @@ class ReceivablePayableReport(object):
|
||||
def get_party_details(self, party):
|
||||
if not party in self.party_details:
|
||||
if self.party_type == "Customer":
|
||||
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
fields.append("default_sales_partner")
|
||||
|
||||
self.party_details[party] = frappe.db.get_value(
|
||||
"Customer",
|
||||
party,
|
||||
["customer_name", "territory", "customer_group", "customer_primary_contact"],
|
||||
fields,
|
||||
as_dict=True,
|
||||
)
|
||||
else:
|
||||
@@ -969,6 +978,9 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.show_sales_person:
|
||||
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
if self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
label=_("Supplier Group"),
|
||||
@@ -1028,3 +1040,17 @@ class ReceivablePayableReport(object):
|
||||
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
||||
"type": "percentage",
|
||||
}
|
||||
|
||||
def get_exchange_rate_revaluations(self):
|
||||
je = qb.DocType("Journal Entry")
|
||||
results = (
|
||||
qb.from_(je)
|
||||
.select(je.name)
|
||||
.where(
|
||||
(je.company == self.filters.company)
|
||||
& (je.posting_date.lte(self.filters.report_date))
|
||||
& (je.voucher_type == "Exchange Rate Revaluation")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.err_journals = [x[0] for x in results] if results else []
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate, today
|
||||
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.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
@@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
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()
|
||||
|
||||
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"}
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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 test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
@@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
name = make_sales_invoice()
|
||||
name = make_sales_invoice().name
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
@@ -118,8 +146,72 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||
)
|
||||
def test_exchange_revaluation_for_party(self):
|
||||
"""
|
||||
Exchange Revaluation for party on Receivable/Payable shoule be included
|
||||
"""
|
||||
|
||||
def make_sales_invoice():
|
||||
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.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.currency = "USD"
|
||||
si.conversion_rate = 0.90
|
||||
si.debit_to = self.debtors_usd
|
||||
si = si.save().submit()
|
||||
|
||||
# Exchange Revaluation
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company
|
||||
err.posting_date = today()
|
||||
accounts = err.get_accounts_data()
|
||||
err.extend("accounts", accounts)
|
||||
err.accounts[0].new_exchange_rate = 0.95
|
||||
row = err.accounts[0]
|
||||
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()
|
||||
|
||||
# Submit JV for ERR
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||
je = je.submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_for_err = [0, -5, 0, 5]
|
||||
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,
|
||||
[
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
si = create_sales_invoice(
|
||||
@@ -134,22 +226,26 @@ def make_sales_invoice():
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
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),
|
||||
)
|
||||
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.submit()
|
||||
si = si.save()
|
||||
|
||||
return si.name
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
|
||||
return si
|
||||
|
||||
|
||||
def make_payment(docname):
|
||||
|
||||
@@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
if row.sales_person:
|
||||
self.party_total[row.party].sales_person.append(row.sales_person)
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column(
|
||||
@@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
)
|
||||
if self.filters.show_sales_person:
|
||||
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
else:
|
||||
self.add_column(
|
||||
label=_("Supplier Group"),
|
||||
|
||||
@@ -25,6 +25,7 @@ def get_data(filters):
|
||||
["posting_date", "<=", filters.get("to_date")],
|
||||
["against_voucher_type", "=", "Asset"],
|
||||
["account", "in", depreciation_accounts],
|
||||
["is_cancelled", "=", 0],
|
||||
]
|
||||
|
||||
if filters.get("asset"):
|
||||
|
||||
@@ -131,8 +131,36 @@ def get_assets(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabAsset` a, `tabDepreciation Schedule` ds
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
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.calculate_depreciation=0 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)
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
|
||||
@@ -25,6 +25,8 @@ def execute(filters=None):
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
filters.period_start_date = period_list[0]["year_start_date"]
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
@@ -96,7 +98,7 @@ def execute(filters=None):
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
|
||||
report_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
return columns, data, message, chart, report_summary
|
||||
@@ -174,7 +176,6 @@ def get_report_summary(
|
||||
liability,
|
||||
equity,
|
||||
provisional_profit_loss,
|
||||
total_credit,
|
||||
currency,
|
||||
filters,
|
||||
consolidated=False,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user