mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 12:39:18 +00:00
Compare commits
1152 Commits
accounts-f
...
v16.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b1c0c36b5 | ||
|
|
d316ef2306 | ||
|
|
af3a7903b3 | ||
|
|
a66e114a71 | ||
|
|
631b9d3bb0 | ||
|
|
eb03781718 | ||
|
|
eb7cebac91 | ||
|
|
3420e21d45 | ||
|
|
211832104c | ||
|
|
4b85d51257 | ||
|
|
0363b01ab7 | ||
|
|
fc517f7fa2 | ||
|
|
18451b69e6 | ||
|
|
50ce61ae02 | ||
|
|
294fb27dc8 | ||
|
|
c3fdb191b9 | ||
|
|
ccb4b1fbc4 | ||
|
|
adec530792 | ||
|
|
b5e0b543f7 | ||
|
|
e87857cee0 | ||
|
|
93db2ebd6f | ||
|
|
2925f9a04e | ||
|
|
02a9c54b74 | ||
|
|
c7bf103c0c | ||
|
|
8434efd11b | ||
|
|
671610db1e | ||
|
|
1c6bfc9af7 | ||
|
|
b9659a8741 | ||
|
|
6dead8fd85 | ||
|
|
e3beca400f | ||
|
|
d8ff1595a7 | ||
|
|
528de7fbd4 | ||
|
|
cecd07bbf4 | ||
|
|
e66b1a06f4 | ||
|
|
c15e96c460 | ||
|
|
78ac8232d8 | ||
|
|
8d4a179a8f | ||
|
|
058500e011 | ||
|
|
bf2ab32abf | ||
|
|
7e99148357 | ||
|
|
4e6d86d6f0 | ||
|
|
584c40a1b5 | ||
|
|
1d6d9c2040 | ||
|
|
4cb4b34683 | ||
|
|
7662616721 | ||
|
|
9f4bf65768 | ||
|
|
7c96a08054 | ||
|
|
46e7fedff8 | ||
|
|
e8665864a4 | ||
|
|
b6e5b67676 | ||
|
|
e9c009b564 | ||
|
|
17955337cc | ||
|
|
34fd4e7043 | ||
|
|
bf199cc2e0 | ||
|
|
e4741072a6 | ||
|
|
f23c11256c | ||
|
|
8e143d68b4 | ||
|
|
79a14f49e1 | ||
|
|
d3a480200e | ||
|
|
c1d50c492b | ||
|
|
d595a974ee | ||
|
|
b11f20596e | ||
|
|
c06a8568bd | ||
|
|
fc9bc36110 | ||
|
|
67def7dc13 | ||
|
|
cdd7b12279 | ||
|
|
3a995ba260 | ||
|
|
4d8d29b0df | ||
|
|
02cefa8bdb | ||
|
|
aef2e2794b | ||
|
|
069f28feeb | ||
|
|
62270af65b | ||
|
|
5139442205 | ||
|
|
5eb062c065 | ||
|
|
20f6e37b65 | ||
|
|
c5ce14dc14 | ||
|
|
b5d0e85b59 | ||
|
|
7db70742e8 | ||
|
|
b0a04e202a | ||
|
|
721b29c8df | ||
|
|
f4f02458ef | ||
|
|
e5b93d85e6 | ||
|
|
e4a0bc2d5f | ||
|
|
8acf373e68 | ||
|
|
10a3f61689 | ||
|
|
3c13543c24 | ||
|
|
3906bf450e | ||
|
|
69597329e9 | ||
|
|
87be020c78 | ||
|
|
d0ba365aaa | ||
|
|
d82bf43684 | ||
|
|
cfa00829a8 | ||
|
|
190204a939 | ||
|
|
bc63c85daf | ||
|
|
dbab929016 | ||
|
|
1bfb62465f | ||
|
|
dd94e51d66 | ||
|
|
fd0ed04979 | ||
|
|
f111d97444 | ||
|
|
ae7aa19afc | ||
|
|
772847c0d3 | ||
|
|
ed1f1110c7 | ||
|
|
24c8cfe128 | ||
|
|
5fa9e0421c | ||
|
|
103e4aaa93 | ||
|
|
6729d3d1ca | ||
|
|
66425462ba | ||
|
|
e675c76628 | ||
|
|
57624e1e33 | ||
|
|
3acc3e6ad1 | ||
|
|
2dcba90afc | ||
|
|
ede0dcd58a | ||
|
|
a7be255261 | ||
|
|
015529a321 | ||
|
|
f5aaa1139c | ||
|
|
d420ec0b22 | ||
|
|
20320c4a6c | ||
|
|
e0f6f2a600 | ||
|
|
6cd4ef694e | ||
|
|
e432a88ec1 | ||
|
|
8fdf6a9ac6 | ||
|
|
e5b02e81a9 | ||
|
|
a36331c393 | ||
|
|
b63d069635 | ||
|
|
feb4bf394a | ||
|
|
49f1688a51 | ||
|
|
fd6683e196 | ||
|
|
5a47503611 | ||
|
|
07a69a073d | ||
|
|
f8f82ccf31 | ||
|
|
54cbe4222d | ||
|
|
f222d3a37b | ||
|
|
8ba71300db | ||
|
|
7a4cd3ac33 | ||
|
|
7f6e509e20 | ||
|
|
f786c16a7d | ||
|
|
53b13501a9 | ||
|
|
244319bf1d | ||
|
|
f6a4f696a1 | ||
|
|
afc5dda372 | ||
|
|
140d13cfb3 | ||
|
|
0950e67eea | ||
|
|
31a147126e | ||
|
|
72aa27a87f | ||
|
|
9cb5768fea | ||
|
|
01c560eb99 | ||
|
|
fa0ac8db4d | ||
|
|
57c6f4ffb6 | ||
|
|
84612d676b | ||
|
|
c7a81161c7 | ||
|
|
aac39b2671 | ||
|
|
028cd97ee1 | ||
|
|
c297282fa5 | ||
|
|
522e4887ca | ||
|
|
bd94deee62 | ||
|
|
344572cf87 | ||
|
|
fc9496a36b | ||
|
|
b204853193 | ||
|
|
2d5d03e63a | ||
|
|
cccd34b06a | ||
|
|
7ab1e1f677 | ||
|
|
daabb42ad7 | ||
|
|
3d4c8d6d35 | ||
|
|
247cc1d53e | ||
|
|
2eb448d4b4 | ||
|
|
7308021aa8 | ||
|
|
82b49f5d9d | ||
|
|
b9e8b2808a | ||
|
|
d4702ac232 | ||
|
|
f622996c48 | ||
|
|
b8b55754c8 | ||
|
|
7baa75faa5 | ||
|
|
6147f9c6a3 | ||
|
|
7c16db567b | ||
|
|
83bc8744bb | ||
|
|
fd5b84fe1a | ||
|
|
59f5ee7b63 | ||
|
|
4632ddc497 | ||
|
|
7bb0ec836f | ||
|
|
ca568a01f5 | ||
|
|
06fd0f8084 | ||
|
|
cc6cb5d1af | ||
|
|
6c6bf306d3 | ||
|
|
43e859b093 | ||
|
|
855dd4162f | ||
|
|
28a49a21ee | ||
|
|
50b30fce4a | ||
|
|
debd3c6886 | ||
|
|
83ddaf1696 | ||
|
|
f79e7dd806 | ||
|
|
9854b146a5 | ||
|
|
b93f920592 | ||
|
|
45fe7f2875 | ||
|
|
1fba6fc2f4 | ||
|
|
7e81be7b8c | ||
|
|
767bb534c0 | ||
|
|
349db6cfaf | ||
|
|
73e7c38bc7 | ||
|
|
7811c08e2b | ||
|
|
aefd36e050 | ||
|
|
87d00a373f | ||
|
|
410a4b20af | ||
|
|
f56a673baa | ||
|
|
24757465ce | ||
|
|
c57a43b3f4 | ||
|
|
e3fb7f4c47 | ||
|
|
8108fe4ca5 | ||
|
|
8ebd1fd029 | ||
|
|
a450f7a00d | ||
|
|
cb5926f59b | ||
|
|
0c08fb9303 | ||
|
|
732b04f37c | ||
|
|
016170b08c | ||
|
|
566d1a1ee2 | ||
|
|
e1f9adf4e9 | ||
|
|
ae9c9f7570 | ||
|
|
1416ae3fb2 | ||
|
|
c249638ebb | ||
|
|
d24a68b206 | ||
|
|
1097d6eef1 | ||
|
|
a9b3839c3c | ||
|
|
2860d5a406 | ||
|
|
b8ecefa06c | ||
|
|
292a51c160 | ||
|
|
469a1ade79 | ||
|
|
acb30dc6ac | ||
|
|
43cdca950f | ||
|
|
e2e2e78b65 | ||
|
|
0c43c07cf6 | ||
|
|
875bf5c8a9 | ||
|
|
b30ffc561b | ||
|
|
8fa047f96d | ||
|
|
4a30634942 | ||
|
|
69e94248c1 | ||
|
|
8fb8f32ad6 | ||
|
|
da899913b8 | ||
|
|
41a7e64772 | ||
|
|
aae0448e1f | ||
|
|
9f0f4d6709 | ||
|
|
7b4cb629de | ||
|
|
3ef66f65b9 | ||
|
|
70f1d3ca47 | ||
|
|
68ccb961f1 | ||
|
|
20dc93a4b7 | ||
|
|
bf791ae716 | ||
|
|
44d85b0764 | ||
|
|
b3526599dd | ||
|
|
bce4ec20e5 | ||
|
|
30d4d53171 | ||
|
|
9dfb0fdcbb | ||
|
|
6fc9636642 | ||
|
|
db9ce998e1 | ||
|
|
11db07c42e | ||
|
|
5f4185ede9 | ||
|
|
b6cb9d4799 | ||
|
|
7df349844a | ||
|
|
73daad0fc9 | ||
|
|
fa8e80c6a0 | ||
|
|
aebcd010ba | ||
|
|
8dacfdf287 | ||
|
|
0050898762 | ||
|
|
734d553338 | ||
|
|
ca31a94c76 | ||
|
|
666c77dd91 | ||
|
|
20654acdd7 | ||
|
|
e8777d4f72 | ||
|
|
3ebff7e236 | ||
|
|
f44a181c81 | ||
|
|
4a8ed972d0 | ||
|
|
ca2ddd5d92 | ||
|
|
201dbad545 | ||
|
|
c44adc6d98 | ||
|
|
9580a3c9f7 | ||
|
|
941b70a185 | ||
|
|
232cc16bbb | ||
|
|
96829787b9 | ||
|
|
b55afe36c8 | ||
|
|
c7069df4b4 | ||
|
|
46f8cfd228 | ||
|
|
dd18d78c10 | ||
|
|
6cb6cbb00b | ||
|
|
23c70332df | ||
|
|
278fd072c3 | ||
|
|
fc92458378 | ||
|
|
a35216a668 | ||
|
|
175d262b94 | ||
|
|
bd9f5fca08 | ||
|
|
f523c7889e | ||
|
|
ca00b46507 | ||
|
|
88dd869a11 | ||
|
|
4e04d5d1bc | ||
|
|
f2160a0629 | ||
|
|
9ca3d00eb7 | ||
|
|
6ade609dd6 | ||
|
|
e84c84975c | ||
|
|
c66f78c784 | ||
|
|
56192afe5f | ||
|
|
8af3dd0ba0 | ||
|
|
655b35fcdd | ||
|
|
489a035637 | ||
|
|
2073cb0106 | ||
|
|
eda8a621c6 | ||
|
|
83ab480211 | ||
|
|
f122509816 | ||
|
|
50bb1ce31d | ||
|
|
2873dab98e | ||
|
|
9bbcbe0ac3 | ||
|
|
8310b5ca36 | ||
|
|
09bcadfbb9 | ||
|
|
250b9755f2 | ||
|
|
6ac88f9bbf | ||
|
|
4b07d4a1b9 | ||
|
|
1b530f7f92 | ||
|
|
8a8e1a2c4d | ||
|
|
352c585208 | ||
|
|
e98d8bf638 | ||
|
|
bb21415a72 | ||
|
|
bc784a0e73 | ||
|
|
48c53c06ce | ||
|
|
5e8d7a949c | ||
|
|
1d0aa4d3ec | ||
|
|
f1b79e6687 | ||
|
|
da4b78491d | ||
|
|
16c6b2c39f | ||
|
|
9e28703ad9 | ||
|
|
e9c37642c8 | ||
|
|
d191b80587 | ||
|
|
280558efa2 | ||
|
|
cfe2181bf8 | ||
|
|
fbb67265b2 | ||
|
|
58e4f5690e | ||
|
|
a441b1c53a | ||
|
|
0e1b45ca26 | ||
|
|
a5e5365ba9 | ||
|
|
dfe57b9fd3 | ||
|
|
19bfa9225d | ||
|
|
6e8a6582a0 | ||
|
|
5ae7805429 | ||
|
|
586431ca1c | ||
|
|
61c31f0cd0 | ||
|
|
6c65c8955c | ||
|
|
9a1f551e53 | ||
|
|
b8434ecfb0 | ||
|
|
c0ac5f94b5 | ||
|
|
820ccba9a4 | ||
|
|
c62b66038b | ||
|
|
a55092d8da | ||
|
|
15d9d8b719 | ||
|
|
cf03d03033 | ||
|
|
9cc77934a6 | ||
|
|
f40c492a05 | ||
|
|
5b3d2c0d02 | ||
|
|
ce123f1a89 | ||
|
|
18ac589796 | ||
|
|
df13308663 | ||
|
|
32b3a13591 | ||
|
|
83522bf079 | ||
|
|
7c6f0e4463 | ||
|
|
4d8ec5f54c | ||
|
|
68fabb72fa | ||
|
|
b23c6e2687 | ||
|
|
8c0dbb5bf6 | ||
|
|
3b221b0078 | ||
|
|
fb0bd44a85 | ||
|
|
cb951bbb18 | ||
|
|
0942845af7 | ||
|
|
b567eac01d | ||
|
|
278963be69 | ||
|
|
c55c97a9fe | ||
|
|
a091e47bd7 | ||
|
|
358083d759 | ||
|
|
fdebf25ca7 | ||
|
|
6710a8b032 | ||
|
|
d78a626661 | ||
|
|
48ea75c452 | ||
|
|
e04353fc31 | ||
|
|
e2ac7646e6 | ||
|
|
7a3206f0bf | ||
|
|
482efee6c0 | ||
|
|
41e1f92612 | ||
|
|
39031ae8a2 | ||
|
|
e53a74f2be | ||
|
|
3a604dd249 | ||
|
|
11ce4ad102 | ||
|
|
adabb6acb3 | ||
|
|
00c9e20df3 | ||
|
|
d1e0992253 | ||
|
|
260e6fb979 | ||
|
|
a0af90f755 | ||
|
|
b0d3681593 | ||
|
|
8b1fd7469c | ||
|
|
f991420152 | ||
|
|
31374e1134 | ||
|
|
c186b1d9c8 | ||
|
|
178bc759c7 | ||
|
|
aa8e8da29f | ||
|
|
028b6790cf | ||
|
|
333169e52b | ||
|
|
17320d1062 | ||
|
|
34cee8fa9c | ||
|
|
fe66bd4dc2 | ||
|
|
9638e7ea83 | ||
|
|
80933ec66b | ||
|
|
abef84358c | ||
|
|
f48b90c600 | ||
|
|
64e50f2e55 | ||
|
|
848f8d6b1f | ||
|
|
1562e9b828 | ||
|
|
1778e2c564 | ||
|
|
62acc4aeb5 | ||
|
|
da4d6cb567 | ||
|
|
5f80857bc9 | ||
|
|
a412e0b357 | ||
|
|
f4c0611cc5 | ||
|
|
dec474ef3a | ||
|
|
f3142c4af6 | ||
|
|
0b3e40b155 | ||
|
|
e32f898dd7 | ||
|
|
5f6ed62c03 | ||
|
|
382e4fe843 | ||
|
|
f07f2cfe88 | ||
|
|
978aa9ac23 | ||
|
|
af4707d1ee | ||
|
|
76da5efaa9 | ||
|
|
5986d79643 | ||
|
|
6cc2290f6e | ||
|
|
ba9bbed038 | ||
|
|
273ff33ebf | ||
|
|
38affb0562 | ||
|
|
44840b8b80 | ||
|
|
2f19244660 | ||
|
|
890316a793 | ||
|
|
b6aec1d31a | ||
|
|
c2cd09cda7 | ||
|
|
c4136870cf | ||
|
|
c055e86e51 | ||
|
|
3bef6bf5ef | ||
|
|
9ff5425859 | ||
|
|
d0bdbfecef | ||
|
|
c287665033 | ||
|
|
ac6583574e | ||
|
|
9a4619a289 | ||
|
|
0563faca54 | ||
|
|
bc213c45aa | ||
|
|
ad2c58cf2a | ||
|
|
8ea4e2139b | ||
|
|
9331f8ce7e | ||
|
|
41cf546946 | ||
|
|
6adbe7a3fd | ||
|
|
af9742a3ee | ||
|
|
b923a6b72c | ||
|
|
09a8905b2d | ||
|
|
766d157320 | ||
|
|
9486d0d462 | ||
|
|
d7fea5d4e1 | ||
|
|
ae339f69f4 | ||
|
|
af4d14e820 | ||
|
|
db7456eea2 | ||
|
|
88acf05ba2 | ||
|
|
bf3b3d4c02 | ||
|
|
9b794d5a42 | ||
|
|
d811a368a9 | ||
|
|
719cb42c1e | ||
|
|
8d523d2cd3 | ||
|
|
3de80ec640 | ||
|
|
c9eda3c279 | ||
|
|
10a66b8e73 | ||
|
|
0f7d89f4d1 | ||
|
|
aa8a0d0a9f | ||
|
|
2fa948b070 | ||
|
|
cb84ffd972 | ||
|
|
c28f6f1856 | ||
|
|
25bf2d94e7 | ||
|
|
2db85ad883 | ||
|
|
c3723e798d | ||
|
|
f6a96e5563 | ||
|
|
2ec119e561 | ||
|
|
a8b94f2090 | ||
|
|
96cdb7d54f | ||
|
|
f9be8a46fb | ||
|
|
3c032e1f35 | ||
|
|
97a7c0162d | ||
|
|
45a7195abe | ||
|
|
c7a7cb2b90 | ||
|
|
7ad123592c | ||
|
|
09c9ac1b66 | ||
|
|
9cc48f3686 | ||
|
|
5ed48a8539 | ||
|
|
bd595602de | ||
|
|
b3fdef8d19 | ||
|
|
99148a2aba | ||
|
|
86d6facab3 | ||
|
|
8e54be7808 | ||
|
|
f15efc543b | ||
|
|
d4ef2ebd3c | ||
|
|
767e4762bb | ||
|
|
5c2bb66028 | ||
|
|
a8af04f6fc | ||
|
|
605b7c5efe | ||
|
|
a34dd9d247 | ||
|
|
6bb0bdcdca | ||
|
|
a20fd7428c | ||
|
|
8d9b83e410 | ||
|
|
e57d2b4811 | ||
|
|
f3ba59480b | ||
|
|
4e4452b73e | ||
|
|
dbbad3e6ee | ||
|
|
d16c50486a | ||
|
|
254eb5e7d6 | ||
|
|
074bdd119b | ||
|
|
3787b9fbe5 | ||
|
|
77045e45c5 | ||
|
|
a52f42c7e4 | ||
|
|
8e0227ea68 | ||
|
|
c02fb7a619 | ||
|
|
0bec419301 | ||
|
|
4f99045ca0 | ||
|
|
eb8ea4a02d | ||
|
|
74f748e1f5 | ||
|
|
7f1c11c92e | ||
|
|
8cb0632081 | ||
|
|
e8a8d3947c | ||
|
|
4568114ba8 | ||
|
|
88d8310a47 | ||
|
|
85114eda7c | ||
|
|
5757feb45a | ||
|
|
f301b36b5e | ||
|
|
198eb372e3 | ||
|
|
60470e2a5b | ||
|
|
dc718f7acb | ||
|
|
2fe5fad884 | ||
|
|
ed4c17d3a2 | ||
|
|
f194ac093c | ||
|
|
1c82f42fa8 | ||
|
|
d42aad18a7 | ||
|
|
6a03fc6ede | ||
|
|
75999a7ae4 | ||
|
|
b53b6cac52 | ||
|
|
ca12d88514 | ||
|
|
68703d6e5b | ||
|
|
8f2b3f8703 | ||
|
|
997401a16a | ||
|
|
a62b0ca196 | ||
|
|
2e5241a211 | ||
|
|
4edcc12edc | ||
|
|
cbc0f327d6 | ||
|
|
30ae3e0b59 | ||
|
|
a28d799c09 | ||
|
|
ec3cd7e09c | ||
|
|
13d4f5ec31 | ||
|
|
baf884f95c | ||
|
|
1bcfad8eb1 | ||
|
|
2767cb04fb | ||
|
|
5401843315 | ||
|
|
5e2d246931 | ||
|
|
e8f87927e1 | ||
|
|
107fa1d605 | ||
|
|
264baf34f6 | ||
|
|
66407d22fc | ||
|
|
f26ee9e546 | ||
|
|
5154fa8259 | ||
|
|
b2feb5abbe | ||
|
|
b58c4edd5d | ||
|
|
c5bbabac4b | ||
|
|
6ac76bac6e | ||
|
|
c5fdc256fa | ||
|
|
0376bba345 | ||
|
|
2e803e6494 | ||
|
|
f13022835a | ||
|
|
6e44951a96 | ||
|
|
80730908c9 | ||
|
|
66f56eea33 | ||
|
|
31d55248e4 | ||
|
|
994dd425d4 | ||
|
|
fab1ef5d76 | ||
|
|
97711b7e83 | ||
|
|
7012345968 | ||
|
|
7bfcdb13b1 | ||
|
|
d6bdbfe266 | ||
|
|
2db09b3840 | ||
|
|
1e2c56874f | ||
|
|
17ff48ab92 | ||
|
|
c807a7be7b | ||
|
|
44919be5a6 | ||
|
|
8f86c1b3e9 | ||
|
|
4ad8e55d06 | ||
|
|
b5ad4acc41 | ||
|
|
931f0663b1 | ||
|
|
2bdcec0a7e | ||
|
|
eb38accb33 | ||
|
|
c5b1af84eb | ||
|
|
01c14b5ce4 | ||
|
|
a7155c1fdb | ||
|
|
b75e7a1188 | ||
|
|
d6087e5d92 | ||
|
|
70521fb9bf | ||
|
|
b5ee193566 | ||
|
|
a0256bd798 | ||
|
|
445a255a7f | ||
|
|
af212f520d | ||
|
|
dec67eecad | ||
|
|
6515bb04bf | ||
|
|
b96b7bd046 | ||
|
|
758553b9fc | ||
|
|
f5b75b27d7 | ||
|
|
7c1a947cd8 | ||
|
|
ccbbc60585 | ||
|
|
47af1cec1b | ||
|
|
ec06f4a71b | ||
|
|
d9a377108c | ||
|
|
ed73bd6626 | ||
|
|
3f78d6afed | ||
|
|
4e578c4f83 | ||
|
|
404e68bdc2 | ||
|
|
4f33ee01cf | ||
|
|
c7e7e02b5b | ||
|
|
4df20a3122 | ||
|
|
29048c3364 | ||
|
|
09cdb943ec | ||
|
|
0373f7f33f | ||
|
|
95877e73f0 | ||
|
|
16f4e12854 | ||
|
|
9cc8a42074 | ||
|
|
28febc69e8 | ||
|
|
eee78766cd | ||
|
|
826c74eb71 | ||
|
|
6da10b9f97 | ||
|
|
cd540ab4cc | ||
|
|
b2e4e76b97 | ||
|
|
479e412a44 | ||
|
|
d330700f39 | ||
|
|
dc85babb4d | ||
|
|
17a6392407 | ||
|
|
58db596027 | ||
|
|
7bca3bbcd8 | ||
|
|
b12b40f373 | ||
|
|
72edd86561 | ||
|
|
29f2ecbd6f | ||
|
|
8053303378 | ||
|
|
1a920c035b | ||
|
|
f106d0e762 | ||
|
|
5daa625fe8 | ||
|
|
9f599ee52d | ||
|
|
d82464b2f9 | ||
|
|
88e94aa53a | ||
|
|
95ff1d48ab | ||
|
|
3c6369d396 | ||
|
|
bee0e5d8d4 | ||
|
|
1e60076ade | ||
|
|
4ad624be9c | ||
|
|
026487dce7 | ||
|
|
9b5d215a7a | ||
|
|
2612152456 | ||
|
|
75839f36ba | ||
|
|
e08805128b | ||
|
|
09325aad3d | ||
|
|
ba94d02cb4 | ||
|
|
c404e3b093 | ||
|
|
cd5b913750 | ||
|
|
dcdafc79ee | ||
|
|
91e285efd7 | ||
|
|
e9e498cbd0 | ||
|
|
b24c38f332 | ||
|
|
b4fbda4da3 | ||
|
|
21ec4ed911 | ||
|
|
7deb407206 | ||
|
|
8c35a6ecdd | ||
|
|
92ec633a5c | ||
|
|
bcf6deec9a | ||
|
|
35379294c3 | ||
|
|
1252fed642 | ||
|
|
699e9b4452 | ||
|
|
38b4536300 | ||
|
|
cfdbeb6a1a | ||
|
|
e1fd90f731 | ||
|
|
8ee2cbf259 | ||
|
|
25458d6ba6 | ||
|
|
1966584804 | ||
|
|
2a0ba84f69 | ||
|
|
0c1df30771 | ||
|
|
5f986e4032 | ||
|
|
b2ddef8340 | ||
|
|
150c764205 | ||
|
|
b855eb54b3 | ||
|
|
7e8d19b0c8 | ||
|
|
48783d136f | ||
|
|
8b8f569da2 | ||
|
|
d050cd221d | ||
|
|
5cfd7ec32a | ||
|
|
b808a51d8f | ||
|
|
1125f96316 | ||
|
|
725a4fcf2d | ||
|
|
32cf6148aa | ||
|
|
ba2411b4ee | ||
|
|
ce1312764f | ||
|
|
3a4c1a9f9a | ||
|
|
945390502e | ||
|
|
ae7db7ea5a | ||
|
|
8a7e5d0626 | ||
|
|
ff9b392024 | ||
|
|
0b84d11600 | ||
|
|
c2810ea799 | ||
|
|
00ffdee928 | ||
|
|
b9aaae6343 | ||
|
|
e6160d1b63 | ||
|
|
82b6326e0b | ||
|
|
e42b751dce | ||
|
|
71e46b3ef5 | ||
|
|
765f9a9bbf | ||
|
|
cdbe8b909b | ||
|
|
956e5b1b68 | ||
|
|
92fdec9b92 | ||
|
|
147a5ee953 | ||
|
|
a2fadd9347 | ||
|
|
da7f28a3c3 | ||
|
|
a7e43eddad | ||
|
|
0520ab3c66 | ||
|
|
d344be32a0 | ||
|
|
9145bf5563 | ||
|
|
b7c7e0746e | ||
|
|
355aa52cb8 | ||
|
|
cf449d8dcb | ||
|
|
98e864bea0 | ||
|
|
aab7cd1ae6 | ||
|
|
31142b2f47 | ||
|
|
c5d92d7999 | ||
|
|
d8fc369e38 | ||
|
|
145d40dec8 | ||
|
|
6079bee3a3 | ||
|
|
6a66ce5a97 | ||
|
|
8a5fd5fe89 | ||
|
|
f68515210b | ||
|
|
c2358c6b3f | ||
|
|
8ed9ee9213 | ||
|
|
95e6c72539 | ||
|
|
2b3bdfe387 | ||
|
|
5391ca2a55 | ||
|
|
0e7f75f5c0 | ||
|
|
d17120909c | ||
|
|
74a5325a95 | ||
|
|
0ca6e19a85 | ||
|
|
383c33b02a | ||
|
|
079218ffbf | ||
|
|
563c2998ca | ||
|
|
d52d98666f | ||
|
|
e830cca886 | ||
|
|
7b592d8737 | ||
|
|
88b262abc7 | ||
|
|
a8d3e9bacc | ||
|
|
264dcf8539 | ||
|
|
f1f68ead7d | ||
|
|
5a17dd8d6d | ||
|
|
c93dba2895 | ||
|
|
a436c6a503 | ||
|
|
8235a551f0 | ||
|
|
ca47ae6fd8 | ||
|
|
3d0b28a198 | ||
|
|
58e217be6a | ||
|
|
5e58e344b2 | ||
|
|
c2b8b97d7d | ||
|
|
fa8007f949 | ||
|
|
40b787827a | ||
|
|
33135899ab | ||
|
|
50cf814b43 | ||
|
|
5806bcbb17 | ||
|
|
56def01240 | ||
|
|
d01c4b68fe | ||
|
|
aeece36d93 | ||
|
|
49c866db74 | ||
|
|
e2a01773a5 | ||
|
|
903e97af5f | ||
|
|
7b1d860c33 | ||
|
|
5659538e67 | ||
|
|
3b7d7aed4c | ||
|
|
3404419a1f | ||
|
|
47a6d34224 | ||
|
|
d3c33d16ad | ||
|
|
fc098a732b | ||
|
|
e92c46ba19 | ||
|
|
5ded5e54f1 | ||
|
|
b01f872f7d | ||
|
|
d5120efa25 | ||
|
|
f644c19760 | ||
|
|
42f4e7ebde | ||
|
|
d5025b2af8 | ||
|
|
5fe3fcf174 | ||
|
|
47bb2544b9 | ||
|
|
cd145f4141 | ||
|
|
9194e6350a | ||
|
|
ffae7c4175 | ||
|
|
d26f8aa629 | ||
|
|
5fd7d46986 | ||
|
|
866f1e695b | ||
|
|
310099f4cd | ||
|
|
7422464e75 | ||
|
|
02941afd6a | ||
|
|
13f8bcd289 | ||
|
|
ef37e6aa16 | ||
|
|
f7b3253683 | ||
|
|
8f01e89d76 | ||
|
|
8a40eac45a | ||
|
|
7172f30455 | ||
|
|
ff2d9bf4cb | ||
|
|
20e0313a8c | ||
|
|
7faee7edc2 | ||
|
|
21361ebb2f | ||
|
|
aa6f09e9a9 | ||
|
|
5e47b0dadb | ||
|
|
8a57090aa2 | ||
|
|
dd4bef0706 | ||
|
|
4b612c64a8 | ||
|
|
c3e735ae96 | ||
|
|
c3ff5e3748 | ||
|
|
ee69a6b8ab | ||
|
|
4e6d288056 | ||
|
|
cb737f31fc | ||
|
|
1d850bf3be | ||
|
|
fdb790b00f | ||
|
|
29f9e423b2 | ||
|
|
6141071a18 | ||
|
|
0cb734d6a0 | ||
|
|
1cf9f903e5 | ||
|
|
d40e660a52 | ||
|
|
57f9353d90 | ||
|
|
acec1a7a9d | ||
|
|
ec4d4a0d6c | ||
|
|
1ee700fff3 | ||
|
|
197f00f211 | ||
|
|
5fdf8058df | ||
|
|
124293bd63 | ||
|
|
38cf0d9b5f | ||
|
|
3271eaaf0e | ||
|
|
5ba4c1ea22 | ||
|
|
3ca3a6d9bb | ||
|
|
3327799524 | ||
|
|
0973dbac65 | ||
|
|
8fd5d7187a | ||
|
|
9ebf546e1f | ||
|
|
4a03462890 | ||
|
|
4576ccbbdc | ||
|
|
22ec48159e | ||
|
|
e08793cb8f | ||
|
|
4abe2e82a0 | ||
|
|
57f9faa15a | ||
|
|
09ed3066d8 | ||
|
|
04a44e7e14 | ||
|
|
e4bae76580 | ||
|
|
e40fe9919c | ||
|
|
1cb03db43b | ||
|
|
bd88356a8a | ||
|
|
b5d892c802 | ||
|
|
1f832ca23e | ||
|
|
64456af654 | ||
|
|
af9dc8e406 | ||
|
|
077692b57b | ||
|
|
882b6c2950 | ||
|
|
d8deb33c8c | ||
|
|
8857037971 | ||
|
|
ccb89fee75 | ||
|
|
e23d229e7b | ||
|
|
906a4bd398 | ||
|
|
b6e452a695 | ||
|
|
37b120bf69 | ||
|
|
be40b5bbff | ||
|
|
2a1eb08b08 | ||
|
|
ae0d9d1134 | ||
|
|
59c3eef7db | ||
|
|
4efe681a5c | ||
|
|
83dab5db60 | ||
|
|
d1595a2549 | ||
|
|
074f07694f | ||
|
|
dea734cd4c | ||
|
|
cf73de9533 | ||
|
|
f13e540e74 | ||
|
|
1512c94e79 | ||
|
|
d2c19007cc | ||
|
|
9b374d605a | ||
|
|
113ff17c71 | ||
|
|
9670edb521 | ||
|
|
4d9473f844 | ||
|
|
68d6fc142b | ||
|
|
ca37f0371b | ||
|
|
73eaddcd67 | ||
|
|
592ec1c5a5 | ||
|
|
b1be525032 | ||
|
|
842546d917 | ||
|
|
8f91919933 | ||
|
|
67d471598d | ||
|
|
154350b733 | ||
|
|
9ea3e1e848 | ||
|
|
b672744543 | ||
|
|
4e1e2ee756 | ||
|
|
6277dac209 | ||
|
|
9349dcc907 | ||
|
|
2ea6921e20 | ||
|
|
12a2e14dae | ||
|
|
12008b775f | ||
|
|
5662801a9c | ||
|
|
91f3c82bdf | ||
|
|
47b7214580 | ||
|
|
768afd7968 | ||
|
|
ec5849d8d2 | ||
|
|
fdbe7bc988 | ||
|
|
8ea5170fb8 | ||
|
|
350539f5e6 | ||
|
|
4cde0bfddd | ||
|
|
05ee4c0b0c | ||
|
|
56e44cfccb | ||
|
|
231479a6e2 | ||
|
|
054e7adeac | ||
|
|
9c2525a8f4 | ||
|
|
bacef2f135 | ||
|
|
78da4c38fa | ||
|
|
334deccd2d | ||
|
|
b5df39f47d | ||
|
|
8ca02e7fbb | ||
|
|
7362d783b1 | ||
|
|
60f8654ad6 | ||
|
|
b2a0cdf4bc | ||
|
|
c4b9268f9d | ||
|
|
316b6d6867 | ||
|
|
5dcb766b9f | ||
|
|
03bfbeb1cb | ||
|
|
14c5245037 | ||
|
|
42002d0aa1 | ||
|
|
84af60da7f | ||
|
|
c4882f6f26 | ||
|
|
e5e26cd92a | ||
|
|
9b303a2272 | ||
|
|
80b6c226b2 | ||
|
|
8b38578914 | ||
|
|
4bd3b00e5f | ||
|
|
38287afc05 | ||
|
|
f57d7f39bc | ||
|
|
9a989a84fb | ||
|
|
3ce6da3b71 | ||
|
|
0af4515afd | ||
|
|
68cdadf11a | ||
|
|
f420371a7e | ||
|
|
9118f08e7b | ||
|
|
b98f4611e6 | ||
|
|
4f720b3969 | ||
|
|
3a0e1e8ef9 | ||
|
|
75533ea7d8 | ||
|
|
019f8103f3 | ||
|
|
b79f88a0a6 | ||
|
|
ef7a3419fa | ||
|
|
58c92ea10d | ||
|
|
510f50077b | ||
|
|
eafd2d4b5f | ||
|
|
2ea9ffea3c | ||
|
|
37aa24141b | ||
|
|
926c670c91 | ||
|
|
6c1620ab8c | ||
|
|
31ae91f313 | ||
|
|
e056c0327d | ||
|
|
7c5f5405cc | ||
|
|
d3751d9bb4 | ||
|
|
5cfa71fa47 | ||
|
|
4ba4aef151 | ||
|
|
58315bc963 | ||
|
|
3ca1940881 | ||
|
|
b9affe0cd8 | ||
|
|
51d583b6a7 | ||
|
|
42751fec6d | ||
|
|
385229b81a | ||
|
|
1fe7ef5f1a | ||
|
|
a211db592d | ||
|
|
80d13d6629 | ||
|
|
e64b6db2eb | ||
|
|
7fddbb6dc4 | ||
|
|
5a3fcbedb5 | ||
|
|
be826dba3b | ||
|
|
7fb4d67662 | ||
|
|
6f20ceba81 | ||
|
|
3d0a668c50 | ||
|
|
827f9cc6ef | ||
|
|
d171dc7328 | ||
|
|
6474435ede | ||
|
|
da04225c7d | ||
|
|
c7b8461d43 | ||
|
|
3f7a60d56c | ||
|
|
6db605c443 | ||
|
|
bd9e240ca5 | ||
|
|
2b051ebb87 | ||
|
|
d0ebe5d675 | ||
|
|
e148a38353 | ||
|
|
f7585f40ac | ||
|
|
584d81cfbb | ||
|
|
722581cf05 | ||
|
|
f7c9dc20a8 | ||
|
|
834221b297 | ||
|
|
1d0cd68d2c | ||
|
|
7baf6ec3d6 | ||
|
|
2374cf8bfd | ||
|
|
607cf47312 | ||
|
|
b03cbdb83a | ||
|
|
0b614007bb | ||
|
|
4bd476293b | ||
|
|
cad5cbb5ed | ||
|
|
462deb3755 | ||
|
|
bc351feb3e | ||
|
|
87b4f872f8 | ||
|
|
0b91338771 | ||
|
|
19a9497273 | ||
|
|
80219724f0 | ||
|
|
46ed52a329 | ||
|
|
91fcac5785 | ||
|
|
a66d643638 | ||
|
|
f2ad27eb06 | ||
|
|
88823e51c7 | ||
|
|
ab9241bc11 | ||
|
|
bdabcb081a | ||
|
|
dbfb77f768 | ||
|
|
ba29f5b858 | ||
|
|
e35e8968f0 | ||
|
|
69016a284f | ||
|
|
1b2e5c9706 | ||
|
|
a8f3864905 | ||
|
|
4c5ddf03e2 | ||
|
|
2c4654ab30 | ||
|
|
c5219278fb | ||
|
|
10131333b2 | ||
|
|
07f3f420af | ||
|
|
b5e6c3e703 | ||
|
|
4846dfd3ca | ||
|
|
6d4afc85e6 | ||
|
|
4c8226eb18 | ||
|
|
9eabaf02c6 | ||
|
|
f7d09f8760 | ||
|
|
55f531bad6 | ||
|
|
8d7e31e3f2 | ||
|
|
4cf02b4d78 | ||
|
|
a5830c8247 | ||
|
|
ed40b3232e | ||
|
|
8ef18754a0 | ||
|
|
70483cffa3 | ||
|
|
63fb9f55e7 | ||
|
|
67e7a09e08 | ||
|
|
5cc2cf530a | ||
|
|
a0b9b5cd71 | ||
|
|
3820be8a64 | ||
|
|
405d901514 | ||
|
|
e10007c646 | ||
|
|
800a44a65f | ||
|
|
cc7810998c | ||
|
|
ad42eae2d6 | ||
|
|
8c49c9e500 | ||
|
|
b10e7bf7b5 | ||
|
|
e213c64f9e | ||
|
|
ef38b26a73 | ||
|
|
fca7abf4d6 | ||
|
|
9c13edc0b9 | ||
|
|
fe39ce03bb | ||
|
|
0b88f98a86 | ||
|
|
191c0e65a1 | ||
|
|
090e155fd0 | ||
|
|
cae1237859 | ||
|
|
fa3bd6f5a7 | ||
|
|
7406d83260 | ||
|
|
5b464ae4c1 | ||
|
|
653ae84b3e | ||
|
|
6e32769e37 | ||
|
|
09e37bc98c | ||
|
|
643e1fdce8 | ||
|
|
86edacb781 | ||
|
|
cef879bb3b | ||
|
|
324bebfd44 | ||
|
|
186d540502 | ||
|
|
9e93298f12 | ||
|
|
c738b6d356 | ||
|
|
8ba199016a | ||
|
|
f25ee3c53f | ||
|
|
e88074ddec | ||
|
|
1846de0d49 | ||
|
|
1a31825409 | ||
|
|
c839ebf593 | ||
|
|
1c92b01542 | ||
|
|
f44c908a8d | ||
|
|
0d09d21d2e | ||
|
|
a15578f8f4 | ||
|
|
4888461be2 | ||
|
|
7a93630629 | ||
|
|
48b537dc8c | ||
|
|
fb23719b62 | ||
|
|
0510f7e13f | ||
|
|
4d26a796ef | ||
|
|
eca71dce54 | ||
|
|
1928a394c9 | ||
|
|
f3eda02972 | ||
|
|
fff6f1fb23 | ||
|
|
dcdc1c6a89 | ||
|
|
4680295303 | ||
|
|
60537eeb48 | ||
|
|
7285eaf633 | ||
|
|
4901dc2531 | ||
|
|
9c0ff14060 | ||
|
|
52cf9d4950 | ||
|
|
eaf37c606e | ||
|
|
dd68578252 | ||
|
|
8854db51dd | ||
|
|
291f0c7161 | ||
|
|
ed1a1099cb | ||
|
|
34c190a76e | ||
|
|
7b8bb4f959 | ||
|
|
d26c598daa | ||
|
|
5bac896329 | ||
|
|
689eee767d | ||
|
|
553ec40d4a | ||
|
|
34e13ee745 | ||
|
|
f43444dd28 | ||
|
|
807d344ee1 | ||
|
|
4e9732ab96 | ||
|
|
b0cd4bc9e7 | ||
|
|
10094829e8 | ||
|
|
44363c069e | ||
|
|
5b11710a28 | ||
|
|
d464c731bb | ||
|
|
f1682ea90e | ||
|
|
3e80e99fa6 | ||
|
|
e41a7b8cd7 | ||
|
|
edf8f83a2a | ||
|
|
fdd0e39c91 | ||
|
|
4d4086854a | ||
|
|
a3058bf8cc | ||
|
|
fcfcaa76c6 | ||
|
|
3fcd8d84ac | ||
|
|
33690975f6 | ||
|
|
71db348330 | ||
|
|
72fff5d9ee | ||
|
|
cf1d892d60 | ||
|
|
81ab15351e | ||
|
|
95f604457d | ||
|
|
0e026b9ccd | ||
|
|
0968f435d2 | ||
|
|
f4f79d99e4 | ||
|
|
feb62102d9 | ||
|
|
0f89e750a7 | ||
|
|
2de9f8f2e2 | ||
|
|
3e4846ea3d | ||
|
|
752ea7ee7d | ||
|
|
88f6d783b4 | ||
|
|
2112f36577 | ||
|
|
610877fb17 | ||
|
|
004bd59245 | ||
|
|
84e6d278c3 | ||
|
|
2b4f621c8e | ||
|
|
31434630b5 | ||
|
|
47aa006ea9 | ||
|
|
1319b28b1f | ||
|
|
cbfb14a654 | ||
|
|
608d38a172 |
@@ -45,3 +45,9 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||
|
||||
# these commits actually changed something valuable
|
||||
# but they have a lot of whitespace changes that make blame noisy
|
||||
# PR: https://github.com/frappe/erpnext/pull/49816
|
||||
3ffd50c772735877b330d010c1058f623da8721d
|
||||
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
||||
4
.github/workflows/docs-checker.yml
vendored
4
.github/workflows/docs-checker.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Validate Docs
|
||||
env:
|
||||
|
||||
6
.github/workflows/generate-pot-file.yml
vendored
6
.github/workflows/generate-pot-file.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
|
||||
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15"]
|
||||
version: ["14", "15", "16"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: '3.14'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: '3.14'
|
||||
cache: pip
|
||||
|
||||
- name: Download Semgrep rules
|
||||
|
||||
24
.github/workflows/patch.yml
vendored
24
.github/workflows/patch.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.6
|
||||
image: mariadb:11.8
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -49,14 +49,17 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: |
|
||||
3.11
|
||||
3.13
|
||||
3.14
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -110,8 +113,8 @@ jobs:
|
||||
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
|
||||
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
wget https://erpnext.com/files/v13-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
|
||||
wget https://frappe.io/files/erpnext-v14.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
@@ -132,15 +135,14 @@ jobs:
|
||||
# Resetup env and install apps
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env
|
||||
bench -v setup env --python python$2
|
||||
bench pip install -e ./apps/erpnext
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 14
|
||||
update_to_version 15
|
||||
update_to_version 15 3.13
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-16
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup dependencies
|
||||
|
||||
12
.github/workflows/run-indinvidual-tests.yml
vendored
12
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- id: set-matrix
|
||||
run: |
|
||||
# Use grep and find to get the list of test files
|
||||
@@ -72,17 +72,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
4
.github/workflows/semantic-commits.yml
vendored
4
.github/workflows/semantic-commits.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
name: Check Commit Titles
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 200
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
18
.github/workflows/server-tests-mariadb.yml
vendored
18
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.svg'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
@@ -62,12 +63,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -78,9 +79,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -128,10 +129,9 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
env:
|
||||
TYPE: server
|
||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
|
||||
- name: Show bench output
|
||||
@@ -140,7 +140,6 @@ jobs:
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
@@ -149,10 +148,9 @@ jobs:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
10
.github/workflows/server-tests-postgres.yml
vendored
10
.github/workflows/server-tests-postgres.yml
vendored
@@ -47,12 +47,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
@@ -50,6 +50,15 @@ pull_request_rules:
|
||||
- version-15-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: backport to version-16-beta
|
||||
conditions:
|
||||
- label="backport version-16-beta"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-16-beta
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": ["version-16"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
@@ -21,4 +21,4 @@
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0-dev"
|
||||
__version__ = "16.0.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -9,18 +9,20 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2020-07-22 12:19:59.879476",
|
||||
"modified": "2020-07-22 12:21:48.780513",
|
||||
"last_synced_on": "2026-01-02 13:01:24.037552",
|
||||
"modified": "2026-01-02 13:04:57.850305",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Balance",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"source": "Account Balance Timeline",
|
||||
"time_interval": "Quarterly",
|
||||
"timeseries": 0,
|
||||
"time_interval": "Monthly",
|
||||
"timeseries": 1,
|
||||
"timespan": "Last Year",
|
||||
"type": "Line",
|
||||
"use_report_chart": 0,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"chart_type": "Report",
|
||||
"creation": "2020-07-17 11:25:34.448572",
|
||||
"creation": "2025-04-01 20:38:16.986176",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
|
||||
@@ -9,7 +9,7 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2023-07-19 13:08:56.470390",
|
||||
"modified": "2025-12-19 12:37:31.673782",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss",
|
||||
@@ -17,8 +17,9 @@
|
||||
"owner": "Administrator",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
"type": "Line",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
@@ -318,7 +319,7 @@ def get_already_booked_amount(doc, item):
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
@@ -449,14 +450,12 @@ def process_deferred_accounting(posting_date=None):
|
||||
for company in companies:
|
||||
for record_type in ("Income", "Expense"):
|
||||
doc = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
)
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
)
|
||||
|
||||
doc.insert()
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"account_currency",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_category",
|
||||
"account_type",
|
||||
"tax_rate",
|
||||
"freeze_account",
|
||||
@@ -189,13 +190,20 @@
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable"
|
||||
},
|
||||
{
|
||||
"description": "Used with Financial Report Template",
|
||||
"fieldname": "account_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Category",
|
||||
"options": "Account Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-22 10:40:35.766017",
|
||||
"modified": "2025-08-02 06:26:44.657146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -250,6 +258,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_number",
|
||||
"show_name_in_global_search": 1,
|
||||
"show_preview_popup": 1,
|
||||
@@ -257,4 +266,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class Account(NestedSet):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
account_name: DF.Data
|
||||
account_number: DF.Data | None
|
||||
@@ -92,8 +93,10 @@ class Account(NestedSet):
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -302,10 +305,10 @@ class Account(NestedSet):
|
||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
throw(_("You are not authorized to set Frozen value"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
|
||||
@@ -70,6 +70,7 @@ frappe.treeview_settings["Account"] = {
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,6 +161,14 @@ frappe.treeview_settings["Account"] = {
|
||||
.options,
|
||||
description: __("Optional. This setting will be used to filter in various transactions."),
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "account_category",
|
||||
label: __("Account Category"),
|
||||
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
|
||||
.options,
|
||||
description: __("Optional. Used with Financial Report Template"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "tax_rate",
|
||||
|
||||
@@ -23,15 +23,7 @@ def create_charts(
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
|
||||
if account_name not in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
@@ -55,6 +47,7 @@ def create_charts(
|
||||
"report_type": report_type,
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_category": child.get("account_category"),
|
||||
"account_currency": child.get("account_currency")
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
@@ -97,20 +90,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(
|
||||
set(child.keys())
|
||||
- set(
|
||||
[
|
||||
"account_name",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_number",
|
||||
"account_currency",
|
||||
]
|
||||
)
|
||||
):
|
||||
elif len(set(child.keys()) - set(get_chart_metadata_fields())):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
@@ -253,13 +233,7 @@ def validate_bank_account(coa, bank_account):
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in [
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
if account_name not in get_chart_metadata_fields():
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
@@ -284,15 +258,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
"""recursively called to form a parent-child based list of dict from chart template"""
|
||||
for account_name, child in children.items():
|
||||
account = {}
|
||||
if account_name in [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
if account_name in get_chart_metadata_fields():
|
||||
continue
|
||||
|
||||
if from_coa_importer:
|
||||
@@ -310,3 +276,16 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
|
||||
|
||||
_import_accounts(chart, None)
|
||||
return accounts
|
||||
|
||||
|
||||
def get_chart_metadata_fields():
|
||||
return [
|
||||
"account_name",
|
||||
"account_number",
|
||||
"account_type",
|
||||
"account_category",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]
|
||||
|
||||
@@ -9,103 +9,192 @@ def get():
|
||||
return {
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"}
|
||||
},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {"account_type": "Cash", "account_category": "Cash and Cash Equivalents"},
|
||||
"account_type": "Cash",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_type": "Payable"},
|
||||
_("Employee Advances"): {
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {"account_category": "Other Current Assets"}
|
||||
},
|
||||
_("Prepaid Expenses"): {"account_category": "Other Current Assets"},
|
||||
_("Short-term Investments"): {"account_category": "Short-term Investments"},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock"},
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1},
|
||||
_("Tax Assets"): {"is_group": 1, "account_category": "Other Current Assets"},
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset"},
|
||||
_("Software"): {"account_type": "Fixed Asset"},
|
||||
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_category": "Intangible Assets"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
},
|
||||
_("Investments"): {"is_group": 1},
|
||||
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
|
||||
_("Investments"): {"is_group": 1, "account_category": "Long-term Investments"},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_category": "Other Non-current Assets",
|
||||
}
|
||||
},
|
||||
"root_type": "Asset",
|
||||
},
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
|
||||
},
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {},
|
||||
_("Commission on Sales"): {},
|
||||
_("Depreciation"): {"account_type": "Depreciation"},
|
||||
_("Entertainment Expenses"): {},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
|
||||
_("Legal Expenses"): {},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable"},
|
||||
_("Office Maintenance Expenses"): {},
|
||||
_("Office Rent"): {},
|
||||
_("Postal Expenses"): {},
|
||||
_("Print and Stationery"): {},
|
||||
_("Round Off"): {"account_type": "Round Off"},
|
||||
_("Salary"): {},
|
||||
_("Sales Expenses"): {},
|
||||
_("Telephone Expenses"): {},
|
||||
_("Travel Expenses"): {},
|
||||
_("Utility Expenses"): {},
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
_("Impairment"): {},
|
||||
_("Administrative Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Commission on Sales"): {"account_category": "Operating Expenses"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"},
|
||||
_("Entertainment Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Office Rent"): {"account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {"account_category": "Operating Expenses"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"},
|
||||
_("Salary"): {"account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
|
||||
_("Impairment"): {"account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_category": "Tax Expense"},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {_("Sales"): {}, _("Service"): {}},
|
||||
_("Indirect Income"): {"is_group": 1},
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_category": "Revenue from Operations"},
|
||||
},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
|
||||
"is_group": 1,
|
||||
},
|
||||
"root_type": "Income",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable"},
|
||||
_("Payroll Payable"): {},
|
||||
_("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"},
|
||||
_("Payroll Payable"): {"account_category": "Other Payables"},
|
||||
},
|
||||
_("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
|
||||
_("Customer Advances"): {"account_category": "Other Current Liabilities"},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
|
||||
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
},
|
||||
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
|
||||
_("Duties and Taxes"): {
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {"account_category": "Short-term Provisions"},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {},
|
||||
_("Unsecured Loans"): {},
|
||||
_("Bank Overdraft Account"): {},
|
||||
_("Secured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Unsecured Loans"): {"account_category": "Long-term Borrowings"},
|
||||
_("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"},
|
||||
},
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {"account_category": "Long-term Provisions"},
|
||||
_("Employee Benefits Obligation"): {"account_category": "Other Non-current Liabilities"},
|
||||
"is_group": 1,
|
||||
},
|
||||
"root_type": "Liability",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {"account_type": "Equity"},
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,49 +10,128 @@ def get():
|
||||
_("Application of Funds (Assets)"): {
|
||||
_("Current Assets"): {
|
||||
_("Accounts Receivable"): {
|
||||
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
|
||||
_("Debtors"): {
|
||||
"account_type": "Receivable",
|
||||
"account_number": "1310",
|
||||
"account_category": "Trade Receivables",
|
||||
},
|
||||
"account_number": "1300",
|
||||
},
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
|
||||
_("Bank Accounts"): {
|
||||
"account_type": "Bank",
|
||||
"is_group": 1,
|
||||
"account_number": "1200",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Cash In Hand"): {
|
||||
_("Cash"): {"account_type": "Cash", "account_number": "1110"},
|
||||
_("Cash"): {
|
||||
"account_type": "Cash",
|
||||
"account_number": "1110",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
"account_type": "Cash",
|
||||
"account_number": "1100",
|
||||
"account_category": "Cash and Cash Equivalents",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
|
||||
_("Employee Advances"): {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable",
|
||||
"account_category": "Other Receivables",
|
||||
},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
_("Earnest Money"): {"account_number": "1651"},
|
||||
_("Earnest Money"): {
|
||||
"account_number": "1651",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
"account_number": "1650",
|
||||
},
|
||||
_("Prepaid Expenses"): {
|
||||
"account_number": "1660",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Short-term Investments"): {
|
||||
"account_number": "1670",
|
||||
"account_category": "Short-term Investments",
|
||||
},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
|
||||
_("Stock In Hand"): {
|
||||
"account_type": "Stock",
|
||||
"account_number": "1410",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Tax Assets"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1500",
|
||||
"account_category": "Other Current Assets",
|
||||
},
|
||||
_("Tax Assets"): {"is_group": 1, "account_number": "1500"},
|
||||
"account_number": "1100-1600",
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Capital Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1710",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Electronic Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1720",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Furniture and Fixtures"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1730",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Office Equipment"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1740",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Plants and Machineries"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1750",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Buildings"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1760",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("Software"): {
|
||||
"account_type": "Fixed Asset",
|
||||
"account_number": "1770",
|
||||
"account_category": "Intangible Assets",
|
||||
},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
"account_number": "1790",
|
||||
"account_category": "Tangible Assets",
|
||||
},
|
||||
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
|
||||
"account_number": "1700",
|
||||
},
|
||||
_("Investments"): {"is_group": 1, "account_number": "1800"},
|
||||
_("Investments"): {
|
||||
"is_group": 1,
|
||||
"account_number": "1800",
|
||||
"account_category": "Long-term Investments",
|
||||
},
|
||||
_("Temporary Accounts"): {
|
||||
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
|
||||
_("Temporary Opening"): {
|
||||
"account_type": "Temporary",
|
||||
"account_number": "1910",
|
||||
"account_category": "Other Non-current Assets",
|
||||
},
|
||||
"account_number": "1900",
|
||||
},
|
||||
"root_type": "Asset",
|
||||
@@ -61,42 +140,94 @@ def get():
|
||||
_("Expenses"): {
|
||||
_("Direct Expenses"): {
|
||||
_("Stock Expenses"): {
|
||||
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
|
||||
_("Cost of Goods Sold"): {
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"account_number": "5111",
|
||||
"account_category": "Cost of Goods Sold",
|
||||
},
|
||||
_("Expenses Included In Asset Valuation"): {
|
||||
"account_type": "Expenses Included In Asset Valuation",
|
||||
"account_number": "5112",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Expenses Included In Valuation"): {
|
||||
"account_type": "Expenses Included In Valuation",
|
||||
"account_number": "5118",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {
|
||||
"account_type": "Stock Adjustment",
|
||||
"account_number": "5119",
|
||||
"account_category": "Other Direct Costs",
|
||||
},
|
||||
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
|
||||
"account_number": "5110",
|
||||
},
|
||||
"account_number": "5100",
|
||||
},
|
||||
_("Indirect Expenses"): {
|
||||
_("Administrative Expenses"): {"account_number": "5201"},
|
||||
_("Commission on Sales"): {"account_number": "5202"},
|
||||
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
|
||||
_("Entertainment Expenses"): {"account_number": "5204"},
|
||||
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
|
||||
_("Legal Expenses"): {"account_number": "5206"},
|
||||
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
|
||||
_("Office Maintenance Expenses"): {"account_number": "5208"},
|
||||
_("Office Rent"): {"account_number": "5209"},
|
||||
_("Postal Expenses"): {"account_number": "5210"},
|
||||
_("Print and Stationery"): {"account_number": "5211"},
|
||||
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
|
||||
_("Salary"): {"account_number": "5213"},
|
||||
_("Sales Expenses"): {"account_number": "5214"},
|
||||
_("Telephone Expenses"): {"account_number": "5215"},
|
||||
_("Travel Expenses"): {"account_number": "5216"},
|
||||
_("Utility Expenses"): {"account_number": "5217"},
|
||||
_("Write Off"): {"account_number": "5218"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219"},
|
||||
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
|
||||
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
|
||||
_("Administrative Expenses"): {
|
||||
"account_number": "5201",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Commission on Sales"): {
|
||||
"account_number": "5202",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Depreciation"): {
|
||||
"account_type": "Depreciation",
|
||||
"account_number": "5203",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Entertainment Expenses"): {
|
||||
"account_number": "5204",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Freight and Forwarding Charges"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5205",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Legal Expenses"): {"account_number": "5206", "account_category": "Operating Expenses"},
|
||||
_("Marketing Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5207",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Maintenance Expenses"): {
|
||||
"account_number": "5208",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Office Rent"): {"account_number": "5209", "account_category": "Operating Expenses"},
|
||||
_("Postal Expenses"): {"account_number": "5210", "account_category": "Operating Expenses"},
|
||||
_("Print and Stationery"): {
|
||||
"account_number": "5211",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Round Off"): {
|
||||
"account_type": "Round Off",
|
||||
"account_number": "5212",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Salary"): {"account_number": "5213", "account_category": "Operating Expenses"},
|
||||
_("Sales Expenses"): {"account_number": "5214", "account_category": "Operating Expenses"},
|
||||
_("Telephone Expenses"): {"account_number": "5215", "account_category": "Operating Expenses"},
|
||||
_("Travel Expenses"): {"account_number": "5216", "account_category": "Operating Expenses"},
|
||||
_("Utility Expenses"): {"account_number": "5217", "account_category": "Operating Expenses"},
|
||||
_("Write Off"): {"account_number": "5218", "account_category": "Operating Expenses"},
|
||||
_("Exchange Gain/Loss"): {"account_number": "5219", "account_category": "Operating Expenses"},
|
||||
_("Interest Expense"): {"account_number": "5220", "account_category": "Finance Costs"},
|
||||
_("Bank Charges"): {"account_number": "5221", "account_category": "Finance Costs"},
|
||||
_("Gain/Loss on Asset Disposal"): {
|
||||
"account_number": "5222",
|
||||
"account_category": "Other Operating Income",
|
||||
},
|
||||
_("Miscellaneous Expenses"): {
|
||||
"account_type": "Chargeable",
|
||||
"account_number": "5223",
|
||||
"account_category": "Operating Expenses",
|
||||
},
|
||||
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
@@ -104,54 +235,126 @@ def get():
|
||||
},
|
||||
_("Income"): {
|
||||
_("Direct Income"): {
|
||||
_("Sales"): {"account_number": "4110"},
|
||||
_("Service"): {"account_number": "4120"},
|
||||
_("Sales"): {"account_number": "4110", "account_category": "Revenue from Operations"},
|
||||
_("Service"): {"account_number": "4120", "account_category": "Revenue from Operations"},
|
||||
"account_number": "4100",
|
||||
},
|
||||
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
|
||||
_("Indirect Income"): {
|
||||
_("Interest Income"): {"account_number": "4210", "account_category": "Investment Income"},
|
||||
_("Interest on Fixed Deposits"): {
|
||||
"account_number": "4220",
|
||||
"account_category": "Investment Income",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "4200",
|
||||
},
|
||||
"root_type": "Income",
|
||||
"account_number": "4000",
|
||||
},
|
||||
_("Source of Funds (Liabilities)"): {
|
||||
_("Current Liabilities"): {
|
||||
_("Accounts Payable"): {
|
||||
_("Creditors"): {"account_type": "Payable", "account_number": "2110"},
|
||||
_("Payroll Payable"): {"account_number": "2120"},
|
||||
_("Creditors"): {
|
||||
"account_type": "Payable",
|
||||
"account_number": "2110",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Payroll Payable"): {"account_number": "2120", "account_category": "Other Payables"},
|
||||
"account_number": "2100",
|
||||
},
|
||||
_("Accrued Expenses"): {
|
||||
"account_number": "2150",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Customer Advances"): {
|
||||
"account_number": "2160",
|
||||
"account_category": "Other Current Liabilities",
|
||||
},
|
||||
_("Stock Liabilities"): {
|
||||
_("Stock Received But Not Billed"): {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "2210",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
_("Asset Received But Not Billed"): {
|
||||
"account_type": "Asset Received But Not Billed",
|
||||
"account_number": "2211",
|
||||
"account_category": "Trade Payables",
|
||||
},
|
||||
"account_number": "2200",
|
||||
},
|
||||
_("Duties and Taxes"): {
|
||||
_("TDS Payable"): {"account_number": "2310"},
|
||||
_("TDS Payable"): {
|
||||
"account_number": "2310",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
"account_type": "Tax",
|
||||
"is_group": 1,
|
||||
"account_number": "2300",
|
||||
"account_category": "Current Tax Liabilities",
|
||||
},
|
||||
_("Short-term Provisions"): {
|
||||
"account_number": "2350",
|
||||
"account_category": "Short-term Provisions",
|
||||
},
|
||||
_("Loans (Liabilities)"): {
|
||||
_("Secured Loans"): {"account_number": "2410"},
|
||||
_("Unsecured Loans"): {"account_number": "2420"},
|
||||
_("Bank Overdraft Account"): {"account_number": "2430"},
|
||||
_("Secured Loans"): {
|
||||
"account_number": "2410",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Unsecured Loans"): {
|
||||
"account_number": "2420",
|
||||
"account_category": "Long-term Borrowings",
|
||||
},
|
||||
_("Bank Overdraft Account"): {
|
||||
"account_number": "2430",
|
||||
"account_category": "Short-term Borrowings",
|
||||
},
|
||||
"account_number": "2400",
|
||||
},
|
||||
"account_number": "2100-2400",
|
||||
},
|
||||
_("Non-Current Liabilities"): {
|
||||
_("Long-term Provisions"): {
|
||||
"account_number": "2510",
|
||||
"account_category": "Long-term Provisions",
|
||||
},
|
||||
_("Employee Benefits Obligation"): {
|
||||
"account_number": "2520",
|
||||
"account_category": "Other Non-current Liabilities",
|
||||
},
|
||||
"is_group": 1,
|
||||
"account_number": "2500",
|
||||
},
|
||||
"root_type": "Liability",
|
||||
"account_number": "2000",
|
||||
},
|
||||
_("Equity"): {
|
||||
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
|
||||
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
|
||||
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
|
||||
_("Capital Stock"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3100",
|
||||
"account_category": "Share Capital",
|
||||
},
|
||||
_("Dividends Paid"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3200",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Opening Balance Equity"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3300",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Retained Earnings"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3400",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
_("Revaluation Surplus"): {
|
||||
"account_type": "Equity",
|
||||
"account_number": "3500",
|
||||
"account_category": "Reserves and Surplus",
|
||||
},
|
||||
"root_type": "Equity",
|
||||
"account_number": "3000",
|
||||
},
|
||||
|
||||
@@ -415,15 +415,13 @@ def create_account(**kwargs):
|
||||
return account.name
|
||||
else:
|
||||
account = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
|
||||
account.save()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Account Category", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:account_category_name",
|
||||
"creation": "2025-08-02 06:22:31.835063",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_category_name",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account_category_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Account Category Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-15 03:19:47.171349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_category_name, description",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document, bulk_insert
|
||||
|
||||
DOCTYPE = "Account Category"
|
||||
|
||||
|
||||
class AccountCategory(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_category_name: DF.Data
|
||||
description: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FormulaFieldUpdater,
|
||||
)
|
||||
|
||||
# get all template rows with this account category being used
|
||||
row = frappe.qb.DocType("Financial Report Row")
|
||||
rows = frappe._dict(
|
||||
frappe.qb.from_(row)
|
||||
.select(row.name, row.calculation_formula)
|
||||
.where(row.calculation_formula.like(f"%{old_name}%"))
|
||||
.run()
|
||||
)
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# Update formulas with new name
|
||||
updater = FormulaFieldUpdater(
|
||||
field_name="account_category",
|
||||
value_mapping={old_name: new_name},
|
||||
exclude_operators=["like", "not like"],
|
||||
)
|
||||
|
||||
updated_formulas = updater.update_in_rows(rows)
|
||||
|
||||
if updated_formulas:
|
||||
frappe.msgprint(
|
||||
_("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
|
||||
)
|
||||
|
||||
|
||||
def import_account_categories(template_path: str):
|
||||
categories_file = os.path.join(template_path, "account_categories.json")
|
||||
|
||||
if not os.path.exists(categories_file):
|
||||
return
|
||||
|
||||
with open(categories_file) as f:
|
||||
categories = json.load(f, object_hook=frappe._dict)
|
||||
|
||||
create_account_categories(categories)
|
||||
|
||||
|
||||
def create_account_categories(categories: list[dict]):
|
||||
if not categories:
|
||||
return
|
||||
|
||||
existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
|
||||
new_categories = []
|
||||
|
||||
for category_data in categories:
|
||||
category_name = category_data.get("account_category_name")
|
||||
if not category_name or category_name in existing_categories:
|
||||
continue
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
**category_data,
|
||||
"doctype": DOCTYPE,
|
||||
"name": category_name,
|
||||
}
|
||||
)
|
||||
|
||||
new_categories.append(doc)
|
||||
existing_categories.add(category_name)
|
||||
|
||||
if new_categories:
|
||||
bulk_insert(DOCTYPE, new_categories)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestAccountCategory(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for AccountCategory.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"exempted_role",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -67,10 +68,18 @@
|
||||
"label": "Closed Documents",
|
||||
"options": "Closed Document",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Role allowed to bypass period restrictions.",
|
||||
"fieldname": "exempted_role",
|
||||
"fieldtype": "Link",
|
||||
"label": "Exempted Role",
|
||||
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified": "2025-12-01 16:53:44.631299",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
|
||||
@@ -30,6 +30,7 @@ class AccountingPeriod(Document):
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
exempted_role: DF.Link | None
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
# end: auto-generated types
|
||||
@@ -113,7 +114,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
accounting_period = (
|
||||
frappe.qb.from_(ap)
|
||||
.from_(cd)
|
||||
.select(ap.name)
|
||||
.select(ap.name, ap.exempted_role)
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
@@ -126,6 +127,11 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
).run(as_dict=1)
|
||||
|
||||
if accounting_period:
|
||||
if (
|
||||
accounting_period[0].get("exempted_role")
|
||||
and accounting_period[0].get("exempted_role") in frappe.get_roles()
|
||||
):
|
||||
return
|
||||
frappe.throw(
|
||||
_("You cannot create a {0} within the closed Accounting Period {1}").format(
|
||||
doc.doctype, frappe.bold(accounting_period[0]["name"])
|
||||
|
||||
@@ -37,6 +37,59 @@ class TestAccountingPeriod(IntegrationTestCase):
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def test_accounting_period_exempted_role(self):
|
||||
# Create Accounting Period with exempted role
|
||||
ap = create_accounting_period(
|
||||
period_name="Test Accounting Period Exempted",
|
||||
exempted_role="Accounts Manager",
|
||||
start_date="2025-12-01",
|
||||
end_date="2025-12-31",
|
||||
)
|
||||
ap.save()
|
||||
|
||||
# Create users
|
||||
users = frappe.get_all("User", filters={"email": ["like", "test%"]}, limit=1)
|
||||
user = None
|
||||
|
||||
if users[0].name:
|
||||
user = frappe.get_doc("User", users[0].name)
|
||||
else:
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": "test1@example.com",
|
||||
"first_name": "Test1",
|
||||
}
|
||||
)
|
||||
user.insert()
|
||||
|
||||
user.roles = []
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
|
||||
# ---- Non-exempted user should FAIL ----
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
frappe.set_user(user.name)
|
||||
posting_date = "2025-12-11"
|
||||
doc = create_sales_invoice(
|
||||
do_not_save=1,
|
||||
posting_date=posting_date,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
doc.submit()
|
||||
|
||||
# ---- Exempted role should PASS ----
|
||||
user.append("roles", {"role": "Accounts Manager"})
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
doc = create_sales_invoice(do_not_save=1, posting_date=posting_date)
|
||||
|
||||
doc.submit() # Should not raise
|
||||
self.assertEqual(doc.docstatus, 1)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
@@ -51,5 +104,6 @@ def create_accounting_period(**args):
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
accounting_period.exempted_role = args.exempted_role or ""
|
||||
|
||||
return accounting_period
|
||||
|
||||
@@ -64,26 +64,17 @@
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"role_to_notify_on_depreciation_failure",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
@@ -91,31 +82,23 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"tab_break_dpet",
|
||||
"chart_of_accounts_section",
|
||||
"show_balance_in_coa",
|
||||
"banking_section",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"payment_request_section",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"budget_section",
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -296,13 +279,6 @@
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
@@ -343,11 +319,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -362,11 +333,6 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
@@ -377,7 +343,7 @@
|
||||
{
|
||||
"fieldname": "tab_break_dpet",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Chart Of Accounts"
|
||||
"label": "Others"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -421,11 +387,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
@@ -501,14 +462,9 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
@@ -550,10 +506,6 @@
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
@@ -593,11 +545,6 @@
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -651,15 +598,55 @@
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "30, 60, 90, 120",
|
||||
"fieldname": "default_ageing_range",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Ageing Range"
|
||||
},
|
||||
{
|
||||
"fieldname": "chart_of_accounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Budget"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -23,7 +22,6 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
@@ -42,6 +40,7 @@ class AccountsSettings(Document):
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
@@ -50,7 +49,6 @@ class AccountsSettings(Document):
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
@@ -59,11 +57,11 @@ class AccountsSettings(Document):
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
@@ -75,6 +73,7 @@ class AccountsSettings(Document):
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
use_legacy_controller_for_pcv: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -99,9 +98,6 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -128,10 +124,6 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
@@ -160,6 +152,5 @@ class AccountsSettings(Document):
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
refresh(frm) {
|
||||
frm.set_currency_labels(["amount"], frm.doc.currency);
|
||||
frm.set_currency_labels(["base_amount"], erpnext.get_currency(frm.doc.company));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"amount",
|
||||
"base_amount",
|
||||
"event",
|
||||
"delinked"
|
||||
],
|
||||
@@ -76,13 +78,29 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "base_amount",
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_rate",
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"precision": "9",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-13 15:11:58.300836",
|
||||
"modified": "2025-11-13 12:45:03.014555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
|
||||
@@ -19,10 +19,12 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
base_amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
exchange_rate: DF.Float
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-25 10:24:39.836195",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_detail",
|
||||
"account_head",
|
||||
"allocated_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Name",
|
||||
"options": "reference_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_detail",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Head",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "party_account_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:58.308002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Tax",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"is_tax_withholding_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -25,7 +26,6 @@
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"allocated_amount",
|
||||
"column_break_13",
|
||||
"base_tax_amount",
|
||||
"base_net_amount",
|
||||
@@ -97,11 +97,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -172,12 +172,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
@@ -213,18 +207,26 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-22 19:16:22.346267",
|
||||
"modified": "2025-12-15 06:42:18.707671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ class AdvanceTaxesandCharges(Document):
|
||||
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
allocated_amount: DF.Currency
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -28,10 +27,12 @@ class AdvanceTaxesandCharges(Document):
|
||||
currency: DF.Link | None
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
|
||||
@@ -125,7 +125,7 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
msg += "</ul>"
|
||||
frappe.throw(_(msg))
|
||||
msgprint(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
@@ -134,16 +134,44 @@ class BankClearance(Document):
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
old_clearance_date = frappe.db.get_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
{
|
||||
"parent": d.payment_entry,
|
||||
"account": self.account,
|
||||
"amount": [">", 0],
|
||||
},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
if d.clearance_date or old_clearance_date:
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
sales_invoice = frappe.get_lazy_doc("Sales Invoice", d.payment_entry)
|
||||
sales_invoice.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
old_clearance_date = payment_entry.clearance_date
|
||||
|
||||
if d.clearance_date or old_clearance_date:
|
||||
# using db_set to trigger notification
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
payment_entry.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
"label": "Payment Entry",
|
||||
"oldfieldname": "voucher_id",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "payment_document",
|
||||
"width": "50"
|
||||
"options": "payment_document"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
@@ -69,7 +68,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"columns": 1,
|
||||
"fieldname": "cheque_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@@ -79,8 +78,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "cheque_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Cheque Date",
|
||||
"oldfieldname": "cheque_date",
|
||||
"oldfieldtype": "Date",
|
||||
@@ -96,17 +97,19 @@
|
||||
"oldfieldtype": "Date"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.609319",
|
||||
"modified": "2025-12-17 14:33:45.913311",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Clearance Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("bank", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -304,6 +304,7 @@ def create_payment_entry_bts(
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
company_bank_account=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
@@ -345,6 +346,9 @@ def create_payment_entry_bts(
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
if company_bank_account:
|
||||
pe.bank_account = company_bank_account
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
|
||||
@@ -14,6 +14,7 @@ import openpyxl
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.data_import import DataImport
|
||||
from frappe.core.doctype.data_import.importer import Importer, ImportFile
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.file_manager import get_file, save_file
|
||||
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
@@ -371,7 +372,7 @@ def get_import_status(docname):
|
||||
|
||||
logs = frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["count(*) as count", "success"],
|
||||
fields=[{"COUNT": "*", "as": "count"}, "success"],
|
||||
filters={"data_import": docname},
|
||||
group_by="success",
|
||||
)
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban"
|
||||
"bank_party_iban",
|
||||
"extended_bank_statement_section",
|
||||
"included_fee",
|
||||
"excluded_fee"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -233,12 +236,32 @@
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "extended_bank_statement_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extended Bank Statement"
|
||||
},
|
||||
{
|
||||
"fieldname": "included_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Included Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||
"fieldname": "excluded_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Excluded Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified": "2025-12-07 20:49:18.600757",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,6 +32,8 @@ class BankTransaction(Document):
|
||||
date: DF.Date | None
|
||||
deposit: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
excluded_fee: DF.Currency
|
||||
included_fee: DF.Currency
|
||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
@@ -45,9 +47,14 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -307,6 +314,40 @@ class BankTransaction(Document):
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
def validate_included_fee(self):
|
||||
"""
|
||||
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||
"""
|
||||
|
||||
if self.included_fee and self.withdrawal:
|
||||
if self.included_fee > self.withdrawal:
|
||||
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||
|
||||
def handle_excluded_fee(self):
|
||||
# Include the excluded fee on validate to handle all further processing the same
|
||||
excluded_fee = flt(self.excluded_fee)
|
||||
if excluded_fee <= 0:
|
||||
return
|
||||
|
||||
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||
|
||||
# Enforce directionality
|
||||
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||
frappe.throw(
|
||||
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||
)
|
||||
|
||||
if flt(self.deposit) > 0:
|
||||
self.deposit = flt(self.deposit) - excluded_fee
|
||||
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||
elif flt(self.withdrawal) >= 0:
|
||||
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||
self.excluded_fee = 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(UnitTestCase):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 101
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||
|
||||
def test_included_fee_allows_equal(self):
|
||||
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||
amount (only the fee was deducted from the account)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 100
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_included_fee_allows_for_deposit(self):
|
||||
"""For deposits, a fee may be recorded separately without limiting the
|
||||
received amount."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.included_fee = 999
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_excluded_fee_noop_when_zero(self):
|
||||
"""When there is no excluded fee to apply, the amounts should remain
|
||||
unchanged."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 5
|
||||
bt.excluded_fee = 0
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 100)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.excluded_fee = 11
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||
"""A transaction must be either incoming or outgoing when applying a
|
||||
fee, not both."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.withdrawal = 10
|
||||
bt.excluded_fee = 1
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_deducts_from_deposit(self):
|
||||
"""When a fee is deducted from an incoming payment, the net received
|
||||
amount decreases and the fee is tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 95)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||
while still tracking the fee."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 5
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_increases_outgoing_payment(self):
|
||||
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||
the total money leaving increases and the fee is tracked."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 105)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||
"""If only an excluded fee is provided, it should be treated as an
|
||||
outgoing payment and the fee is then tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 5)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
@@ -4,16 +4,6 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
frappe.ui.form.on("Budget", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("account", "accounts", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("monthly_distribution", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -22,6 +12,15 @@ frappe.ui.form.on("Budget", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
@@ -30,8 +29,20 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
refresh: async function (frm) {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
budget_against: function (frm) {
|
||||
@@ -39,6 +50,20 @@ frappe.ui.form.on("Budget", {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
},
|
||||
|
||||
budget_amount(frm) {
|
||||
if (frm.doc.budget_distribution?.length) {
|
||||
frm.doc.budget_distribution.forEach((row) => {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
});
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
|
||||
distribute_equally: function (frm) {
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
set_null_value: function (frm) {
|
||||
if (frm.doc.budget_against == "Cost Center") {
|
||||
frm.set_value("project", null);
|
||||
@@ -51,4 +76,68 @@ frappe.ui.form.on("Budget", {
|
||||
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
|
||||
frm.toggle_reqd("project", frm.doc.budget_against == "Project");
|
||||
},
|
||||
|
||||
revise_budget_action: function (frm) {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created."
|
||||
),
|
||||
function () {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.budget.budget.revise_budget",
|
||||
args: { budget_name: frm.doc.name },
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__("New revised budget created successfully"));
|
||||
frappe.set_route("Form", "Budget", r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
function () {
|
||||
frappe.msgprint(__("Revision cancelled"));
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Budget Distribution", {
|
||||
amount(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
percent(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function set_total_budget_amount(frm) {
|
||||
let total = 0;
|
||||
|
||||
(frm.doc.budget_distribution || []).forEach((row) => {
|
||||
total += flt(row.amount);
|
||||
});
|
||||
|
||||
frm.set_value("budget_distribution_total", total);
|
||||
}
|
||||
|
||||
function toggle_distribution_fields(frm) {
|
||||
const grid = frm.fields_dict.budget_distribution.grid;
|
||||
|
||||
["amount", "percent"].forEach((field) => {
|
||||
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
|
||||
});
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
|
||||
@@ -12,10 +12,23 @@
|
||||
"company",
|
||||
"cost_center",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"account",
|
||||
"column_break_3",
|
||||
"monthly_distribution",
|
||||
"amended_from",
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_start_date",
|
||||
"budget_end_date",
|
||||
"distribution_frequency",
|
||||
"budget_amount",
|
||||
"section_break_nwug",
|
||||
"distribute_equally",
|
||||
"section_break_fpdt",
|
||||
"budget_distribution",
|
||||
"section_break_wkqb",
|
||||
"column_break_paum",
|
||||
"column_break_nwor",
|
||||
"budget_distribution_total",
|
||||
"section_break_6",
|
||||
"applicable_on_material_request",
|
||||
"action_if_annual_budget_exceeded_on_mr",
|
||||
@@ -32,8 +45,8 @@
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
"section_break_kkan",
|
||||
"revision_of"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -44,6 +57,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Budget Against",
|
||||
"options": "\nCost Center\nProject",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -53,6 +67,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -62,7 +77,8 @@
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.budget_against == 'Project'",
|
||||
@@ -70,28 +86,13 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"reqd": 1
|
||||
"options": "Project",
|
||||
"read_only_depends_on": "eval: doc.revision_of"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
|
||||
"fieldname": "monthly_distribution",
|
||||
"fieldtype": "Link",
|
||||
"label": "Monthly Distribution",
|
||||
"options": "Monthly Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@@ -187,22 +188,12 @@
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Accounts",
|
||||
"options": "Budget Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "BUDGET-.########",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"options": "BUDGET-.########",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
@@ -232,13 +223,117 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fpdt",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution",
|
||||
"fieldtype": "Table",
|
||||
"label": "Budget Distribution",
|
||||
"options": "Budget Distribution"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_kkan",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "revision_of",
|
||||
"fieldtype": "Data",
|
||||
"label": "Revision Of",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "distribute_equally",
|
||||
"fieldtype": "Check",
|
||||
"label": "Distribute Equally"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_nwug",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "To Fiscal Year",
|
||||
"options": "Fiscal Year",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_start_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_end_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Budget End Date"
|
||||
},
|
||||
{
|
||||
"default": "Monthly",
|
||||
"fieldname": "distribution_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Distribution Frequency",
|
||||
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wkqb",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_paum",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nwor",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Distribution Total",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified": "2025-12-10 02:35:01.197613",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
|
||||
from frappe.utils.data import get_first_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -30,9 +34,9 @@ class Budget(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount
|
||||
from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
|
||||
|
||||
accounts: DF.Table[BudgetAccount]
|
||||
account: DF.Link
|
||||
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
@@ -47,73 +51,118 @@ class Budget(Document):
|
||||
applicable_on_material_request: DF.Check
|
||||
applicable_on_purchase_order: DF.Check
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
budget_amount: DF.Currency
|
||||
budget_distribution: DF.Table[BudgetDistribution]
|
||||
budget_distribution_total: DF.Currency
|
||||
budget_end_date: DF.Date | None
|
||||
budget_start_date: DF.Date | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
distribute_equally: DF.Check
|
||||
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
|
||||
from_fiscal_year: DF.Link
|
||||
naming_series: DF.Literal["BUDGET-.########"]
|
||||
project: DF.Link | None
|
||||
revision_of: DF.Data | None
|
||||
to_fiscal_year: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
if not self.get(frappe.scrub(self.budget_against)):
|
||||
frappe.throw(_("{0} is mandatory").format(self.budget_against))
|
||||
self.validate_budget_amount()
|
||||
self.validate_fiscal_year()
|
||||
self.set_fiscal_year_dates()
|
||||
self.validate_duplicate()
|
||||
self.validate_accounts()
|
||||
self.validate_account()
|
||||
self.set_null_value()
|
||||
self.validate_applicable_for()
|
||||
self.validate_existing_expenses()
|
||||
|
||||
def validate_budget_amount(self):
|
||||
if self.budget_amount <= 0:
|
||||
frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount))
|
||||
|
||||
def validate_fiscal_year(self):
|
||||
if self.from_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.from_fiscal_year, self.company)
|
||||
if self.to_fiscal_year:
|
||||
self.validate_fiscal_year_company(self.to_fiscal_year, self.company)
|
||||
|
||||
def validate_fiscal_year_company(self, fiscal_year, company):
|
||||
linked_companies = frappe.get_all(
|
||||
"Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company"
|
||||
)
|
||||
if linked_companies and company not in linked_companies:
|
||||
frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company))
|
||||
|
||||
def set_fiscal_year_dates(self):
|
||||
if self.from_fiscal_year:
|
||||
self.budget_start_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.from_fiscal_year, "year_start_date"
|
||||
)
|
||||
if self.to_fiscal_year:
|
||||
self.budget_end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", self.to_fiscal_year, "year_end_date"
|
||||
)
|
||||
|
||||
if self.budget_start_date > self.budget_end_date:
|
||||
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
|
||||
|
||||
def validate_duplicate(self):
|
||||
budget_against_field = frappe.scrub(self.budget_against)
|
||||
budget_against = self.get(budget_against_field)
|
||||
account = self.account
|
||||
|
||||
if not account:
|
||||
return
|
||||
|
||||
accounts = [d.account for d in self.accounts] or []
|
||||
existing_budget = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
|
||||
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
|
||||
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
|
||||
),
|
||||
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
|
||||
as_dict=1,
|
||||
f"""
|
||||
SELECT name, account
|
||||
FROM `tabBudget`
|
||||
WHERE
|
||||
docstatus < 2
|
||||
AND company = %s
|
||||
AND {budget_against_field} = %s
|
||||
AND account = %s
|
||||
AND name != %s
|
||||
AND (
|
||||
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
)
|
||||
""",
|
||||
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
for d in existing_budget:
|
||||
if existing_budget:
|
||||
d = existing_budget[0]
|
||||
frappe.throw(
|
||||
_(
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
|
||||
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
|
||||
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years."
|
||||
).format(d.name, self.budget_against, budget_against, d.account),
|
||||
DuplicateBudgetError,
|
||||
)
|
||||
|
||||
def validate_accounts(self):
|
||||
account_list = []
|
||||
for d in self.get("accounts"):
|
||||
if d.account:
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
frappe.throw(_("Account is mandatory"))
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
self.account
|
||||
)
|
||||
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
|
||||
else:
|
||||
account_list.append(d.account)
|
||||
)
|
||||
|
||||
def set_null_value(self):
|
||||
if self.budget_against == "Cost Center":
|
||||
@@ -139,30 +188,232 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def validate_existing_expenses(self):
|
||||
if self.is_new() and self.revision_of:
|
||||
return
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
params = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"account": self.account,
|
||||
"budget_start_date": self.budget_start_date,
|
||||
"budget_end_date": self.budget_end_date,
|
||||
"budget_against_field": frappe.scrub(self.budget_against),
|
||||
"budget_against_doctype": frappe.unscrub(self.budget_against),
|
||||
}
|
||||
)
|
||||
|
||||
params[params.budget_against_field] = self.get(params.budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"):
|
||||
params.is_tree = True
|
||||
else:
|
||||
params.is_tree = False
|
||||
|
||||
actual_spent = get_actual_expense(params)
|
||||
|
||||
if actual_spent > self.budget_amount:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Spending for Account {0} ({1}) between {2} and {3} "
|
||||
"has already exceeded the new allocated budget. "
|
||||
"Spent: {4}, Budget: {5}"
|
||||
).format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
frappe.bold(self.budget_start_date),
|
||||
frappe.bold(self.budget_end_date),
|
||||
frappe.bold(frappe.utils.fmt_money(actual_spent)),
|
||||
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
|
||||
),
|
||||
title=_("Budget Limit Exceeded"),
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.allocate_budget()
|
||||
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
|
||||
|
||||
def on_update(self):
|
||||
self.validate_distribution_totals()
|
||||
|
||||
def allocate_budget(self):
|
||||
if self._should_skip_allocation():
|
||||
return
|
||||
|
||||
if self._should_recalculate_manual_distribution():
|
||||
self._recalculate_manual_distribution()
|
||||
return
|
||||
|
||||
if not self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
self._regenerate_distribution()
|
||||
|
||||
def _should_skip_allocation(self):
|
||||
return self.revision_of and not self.distribute_equally
|
||||
|
||||
def _should_recalculate_manual_distribution(self):
|
||||
return (
|
||||
not self.distribute_equally
|
||||
and bool(self.budget_distribution)
|
||||
and self._is_only_budget_amount_changed()
|
||||
)
|
||||
|
||||
def _is_only_budget_amount_changed(self):
|
||||
old = self.get_doc_before_save()
|
||||
if not old:
|
||||
return False
|
||||
|
||||
return (
|
||||
old.budget_amount != self.budget_amount
|
||||
and old.distribution_frequency == self.distribution_frequency
|
||||
and old.budget_start_date == self.budget_start_date
|
||||
and old.budget_end_date == self.budget_end_date
|
||||
)
|
||||
|
||||
def _recalculate_manual_distribution(self):
|
||||
for row in self.budget_distribution:
|
||||
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
|
||||
|
||||
def should_regenerate_budget_distribution(self):
|
||||
"""Check whether budget distribution should be recalculated."""
|
||||
old_doc = self.get_doc_before_save() if not self.is_new() else None
|
||||
if not old_doc or not self.budget_distribution:
|
||||
return True
|
||||
|
||||
if old_doc:
|
||||
changed_fields = [
|
||||
"from_fiscal_year",
|
||||
"to_fiscal_year",
|
||||
"budget_amount",
|
||||
"distribution_frequency",
|
||||
]
|
||||
for field in changed_fields:
|
||||
if old_doc.get(field) != self.get(field):
|
||||
return True
|
||||
|
||||
return bool(self.distribute_equally)
|
||||
|
||||
def _regenerate_distribution(self):
|
||||
self.set("budget_distribution", [])
|
||||
|
||||
periods = self.get_budget_periods()
|
||||
total_periods = len(periods)
|
||||
row_percent = 100 / total_periods if total_periods else 0
|
||||
|
||||
for start_date, end_date in periods:
|
||||
row = self.append("budget_distribution", {})
|
||||
row.start_date = start_date
|
||||
row.end_date = end_date
|
||||
self.add_allocated_amount(row, row_percent)
|
||||
|
||||
self.budget_distribution_total = self.budget_amount
|
||||
|
||||
def get_budget_periods(self):
|
||||
"""Return list of (start_date, end_date) tuples based on frequency."""
|
||||
frequency = self.distribution_frequency
|
||||
periods = []
|
||||
|
||||
start_date = getdate(self.budget_start_date)
|
||||
end_date = getdate(self.budget_end_date)
|
||||
|
||||
while start_date <= end_date:
|
||||
period_start = get_first_day(start_date)
|
||||
period_end = self.get_period_end(period_start, frequency)
|
||||
period_end = min(period_end, end_date)
|
||||
|
||||
periods.append((period_start, period_end))
|
||||
start_date = add_months(period_start, self.get_month_increment(frequency))
|
||||
|
||||
return periods
|
||||
|
||||
def get_period_end(self, start_date, frequency):
|
||||
"""Return the correct end date for a given frequency."""
|
||||
if frequency == "Monthly":
|
||||
return get_last_day(start_date)
|
||||
elif frequency == "Quarterly":
|
||||
return get_last_day(add_months(start_date, 2))
|
||||
elif frequency == "Half-Yearly":
|
||||
return get_last_day(add_months(start_date, 5))
|
||||
else: # Yearly
|
||||
return get_last_day(add_months(start_date, 11))
|
||||
|
||||
def get_month_increment(self, frequency):
|
||||
"""Return how many months to move forward for the next period."""
|
||||
return {
|
||||
"Monthly": 1,
|
||||
"Quarterly": 3,
|
||||
"Half-Yearly": 6,
|
||||
"Yearly": 12,
|
||||
}.get(frequency, 1)
|
||||
|
||||
def add_allocated_amount(self, row, row_percent):
|
||||
row.amount = flt(self.budget_amount * row_percent / 100, 3)
|
||||
row.percent = flt(row_percent, 3)
|
||||
|
||||
def validate_distribution_totals(self):
|
||||
if self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
total_amount = sum(d.amount for d in self.budget_distribution)
|
||||
total_percent = sum(d.percent for d in self.budget_distribution)
|
||||
|
||||
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
|
||||
frappe.throw(
|
||||
_("Total distributed amount {0} must be equal to Budget Amount {1}").format(
|
||||
flt(total_amount, 2), self.budget_amount
|
||||
)
|
||||
)
|
||||
|
||||
if flt(abs(total_percent - 100), 2) > 0.10:
|
||||
frappe.throw(
|
||||
_("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2))
|
||||
)
|
||||
|
||||
|
||||
def validate_expense_against_budget(params, expense_amount=0):
|
||||
params = frappe._dict(params)
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
return
|
||||
|
||||
if not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
if not params.fiscal_year:
|
||||
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
|
||||
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
posting_date = getdate(params.get("posting_date"))
|
||||
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
|
||||
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
|
||||
|
||||
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
|
||||
budget_exists = frappe.db.sql(
|
||||
"""
|
||||
select name
|
||||
from `tabBudget`
|
||||
where company = %s
|
||||
and docstatus = 1
|
||||
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
|
||||
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
|
||||
limit 1
|
||||
""",
|
||||
(params.company, year_end_date, year_start_date),
|
||||
)
|
||||
|
||||
if not budget_exists:
|
||||
return
|
||||
|
||||
if not args.account:
|
||||
args.account = args.get("expense_account")
|
||||
if params.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", params.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not (args.get("account") and args.get("cost_center")) and args.item_code:
|
||||
args.cost_center, args.account = get_item_details(args)
|
||||
if not params.account:
|
||||
params.account = params.get("expense_account")
|
||||
|
||||
if not args.account:
|
||||
if not params.get("expense_account") and params.get("account"):
|
||||
params.expense_account = params.account
|
||||
|
||||
if not (params.get("account") and params.get("cost_center")) and params.item_code:
|
||||
params.cost_center, params.account = get_item_details(params)
|
||||
|
||||
if not params.account:
|
||||
return
|
||||
|
||||
default_dimensions = [
|
||||
@@ -180,59 +431,78 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
budget_against = dimension.get("fieldname")
|
||||
|
||||
if (
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||
params.get(budget_against)
|
||||
and params.account
|
||||
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
|
||||
):
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
args.is_tree = True
|
||||
params.is_tree = True
|
||||
else:
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
|
||||
args.is_tree = False
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
|
||||
params.is_tree = False
|
||||
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
params.budget_against_field = budget_against
|
||||
params.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
from
|
||||
`tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
SELECT
|
||||
b.name,
|
||||
b.{budget_against} AS budget_against,
|
||||
b.budget_amount,
|
||||
b.from_fiscal_year,
|
||||
b.to_fiscal_year,
|
||||
b.budget_start_date,
|
||||
b.budget_end_date,
|
||||
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
|
||||
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
|
||||
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
|
||||
b.action_if_annual_budget_exceeded,
|
||||
b.action_if_accumulated_monthly_budget_exceeded,
|
||||
b.action_if_annual_budget_exceeded_on_mr,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
||||
b.action_if_annual_budget_exceeded_on_po,
|
||||
b.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
FROM
|
||||
`tabBudget` b
|
||||
WHERE
|
||||
b.company = %s
|
||||
AND b.docstatus = 1
|
||||
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
|
||||
AND b.account = %s
|
||||
{condition}
|
||||
""",
|
||||
(args.fiscal_year, args.account),
|
||||
""",
|
||||
(params.company, params.posting_date, params.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
|
||||
if budget_records:
|
||||
validate_budget_records(args, budget_records, expense_amount)
|
||||
validate_budget_records(params, budget_records, expense_amount)
|
||||
|
||||
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
def validate_budget_records(params, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
args["for_material_request"] = budget.for_material_request
|
||||
args["for_purchase_order"] = budget.for_purchase_order
|
||||
yearly_action, monthly_action = get_actions(params, budget)
|
||||
params["for_material_request"] = budget.for_material_request
|
||||
params["for_purchase_order"] = budget.for_purchase_order
|
||||
params["from_fiscal_year"], params["to_fiscal_year"] = (
|
||||
budget.from_fiscal_year,
|
||||
budget.to_fiscal_year,
|
||||
)
|
||||
params["budget_start_date"], params["budget_end_date"] = (
|
||||
budget.budget_start_date,
|
||||
budget.budget_end_date,
|
||||
)
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
args,
|
||||
params,
|
||||
flt(budget.budget_amount),
|
||||
_("Annual"),
|
||||
yearly_action,
|
||||
@@ -241,14 +511,12 @@ def validate_budget_records(args, budget_records, expense_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
|
||||
)
|
||||
budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
|
||||
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
params["month_end_date"] = get_last_day(params.posting_date)
|
||||
|
||||
compare_expense_with_budget(
|
||||
args,
|
||||
params,
|
||||
budget_amount,
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
@@ -257,40 +525,41 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
)
|
||||
|
||||
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||
def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0):
|
||||
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
|
||||
if not amount:
|
||||
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||
params.requested_amount, params.ordered_amount = (
|
||||
get_requested_amount(params),
|
||||
get_ordered_amount(params),
|
||||
)
|
||||
|
||||
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||
amount = args.requested_amount + args.ordered_amount
|
||||
if params.get("doctype") == "Material Request" and params.for_material_request:
|
||||
amount = params.requested_amount + params.ordered_amount
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||
amount = args.ordered_amount
|
||||
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
|
||||
amount = params.ordered_amount
|
||||
|
||||
total_expense = args.actual_expense + amount
|
||||
total_expense = params.actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if args.actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = args.actual_expense - budget_amount
|
||||
if params.actual_expense > budget_amount:
|
||||
diff = params.actual_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
|
||||
|
||||
currency = frappe.get_cached_value("Company", args.company, "default_currency")
|
||||
|
||||
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
|
||||
currency = frappe.get_cached_value("Company", params.company, "default_currency")
|
||||
msg = _msg.format(
|
||||
_(action_for),
|
||||
frappe.bold(args.account),
|
||||
frappe.unscrub(args.budget_against_field),
|
||||
frappe.bold(params.account),
|
||||
frappe.unscrub(params.budget_against_field),
|
||||
frappe.bold(budget_against),
|
||||
frappe.bold(fmt_money(budget_amount, currency=currency)),
|
||||
error_tense,
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
msg += get_expense_breakup(args, currency, budget_against)
|
||||
msg += get_expense_breakup(params, currency, budget_against)
|
||||
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
@@ -303,14 +572,25 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
def get_expense_breakup(params, currency, budget_against):
|
||||
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
params.budget_against_field: budget_against,
|
||||
"account": params.account,
|
||||
"company": params.company,
|
||||
}
|
||||
)
|
||||
|
||||
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
gl_filters = common_filters.copy()
|
||||
gl_filters.update(
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -319,18 +599,23 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label=_("Actual Expenses"),
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
),
|
||||
filters=gl_filters,
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||
+ frappe.bold(fmt_money(params.actual_expense, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
mr_filters = common_filters.copy()
|
||||
mr_filters.update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
@@ -339,22 +624,24 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
label=_("Material Requests"),
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
),
|
||||
filters=mr_filters,
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(params.requested_amount, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
po_filters = common_filters.copy()
|
||||
po_filters.update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["between", [from_date, to_date]]],
|
||||
"item_code": params.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
@@ -362,42 +649,34 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
label=_("Unbilled Orders"),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
),
|
||||
filters=po_filters,
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||
+ frappe.bold(fmt_money(params.ordered_amount, currency=currency))
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def get_actions(args, budget):
|
||||
def get_actions(params, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
if params.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
elif params.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
yearly_action = budget.action_if_annual_budget_exceeded_on_po
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
|
||||
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_requested_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Material Request")
|
||||
def get_requested_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
@@ -411,9 +690,9 @@ def get_requested_amount(args):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
def get_ordered_amount(params):
|
||||
item_code = params.get("item_code")
|
||||
condition = get_other_condition(params, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
@@ -427,111 +706,102 @@ def get_ordered_amount(args):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
def get_other_condition(params, for_doc):
|
||||
condition = f"expense_account = '{params.expense_account}'"
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
if budget_against_field and params.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
start_date, end_date = frappe.get_cached_value(
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
|
||||
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
def get_actual_expense(args):
|
||||
if not args.budget_against_doctype:
|
||||
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
|
||||
def get_actual_expense(params):
|
||||
if not params.budget_against_doctype:
|
||||
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
|
||||
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
|
||||
|
||||
if args.is_tree:
|
||||
date_condition = (
|
||||
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
|
||||
)
|
||||
|
||||
if params.is_tree:
|
||||
lft_rgt = frappe.db.get_value(
|
||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
params.update(lft_rgt)
|
||||
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})"""
|
||||
condition2 = f"""
|
||||
and exists(
|
||||
select name from `tab{params.budget_against_doctype}`
|
||||
where lft >= %(lft)s and rgt <= %(rgt)s
|
||||
and name = gle.{budget_against_field}
|
||||
)
|
||||
"""
|
||||
else:
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
condition2 = f"""
|
||||
and gle.{budget_against_field} = %({budget_against_field})s
|
||||
"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account=%(account)s
|
||||
{condition1}
|
||||
and gle.fiscal_year=%(fiscal_year)s
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""",
|
||||
(args),
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
is_cancelled = 0
|
||||
and gle.account = %(account)s
|
||||
{condition1}
|
||||
{date_condition}
|
||||
and gle.company = %(company)s
|
||||
and gle.docstatus = 1
|
||||
{condition2}
|
||||
""",
|
||||
params,
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
def get_accumulated_monthly_budget(budget_name, posting_date):
|
||||
posting_date = getdate(posting_date)
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(mdp)
|
||||
.join(md)
|
||||
.on(mdp.parent == md.name)
|
||||
.select(mdp.month, mdp.percentage_allocation)
|
||||
.where(md.fiscal_year == fiscal_year)
|
||||
.where(md.name == monthly_distribution)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
bd = frappe.qb.DocType("Budget Distribution")
|
||||
b = frappe.qb.DocType("Budget")
|
||||
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
result = (
|
||||
frappe.qb.from_(bd)
|
||||
.join(b)
|
||||
.on(bd.parent == b.name)
|
||||
.select(Sum(bd.amount).as_("accumulated_amount"))
|
||||
.where(b.name == budget_name)
|
||||
.where(bd.start_date <= posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution and distribution:
|
||||
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
|
||||
else:
|
||||
accumulated_percentage += 100.0 / 12
|
||||
|
||||
dt = add_months(dt, 1)
|
||||
|
||||
return annual_budget * accumulated_percentage / 100
|
||||
return flt(result[0]["accumulated_amount"]) if result else 0.0
|
||||
|
||||
|
||||
def get_item_details(args):
|
||||
def get_item_details(params):
|
||||
cost_center, expense_account = None, None
|
||||
|
||||
if not args.get("company"):
|
||||
if not params.get("company"):
|
||||
return cost_center, expense_account
|
||||
|
||||
if args.item_code:
|
||||
if params.item_code:
|
||||
item_defaults = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": args.item_code, "company": args.get("company")},
|
||||
{"parent": params.item_code, "company": params.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
if item_defaults:
|
||||
@@ -539,7 +809,7 @@ def get_item_details(args):
|
||||
|
||||
if not (cost_center and expense_account):
|
||||
for doctype in ["Item Group", "Company"]:
|
||||
data = get_expense_cost_center(doctype, args)
|
||||
data = get_expense_cost_center(doctype, params)
|
||||
|
||||
if not cost_center and data:
|
||||
cost_center = data[0]
|
||||
@@ -553,14 +823,39 @@ def get_item_details(args):
|
||||
return cost_center, expense_account
|
||||
|
||||
|
||||
def get_expense_cost_center(doctype, args):
|
||||
def get_expense_cost_center(doctype, params):
|
||||
if doctype == "Item Group":
|
||||
return frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
|
||||
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
|
||||
["buying_cost_center", "expense_account"],
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_value(
|
||||
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
|
||||
)
|
||||
|
||||
|
||||
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||
from_year = frappe.get_cached_value(
|
||||
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
to_year = frappe.get_cached_value(
|
||||
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||
)
|
||||
return from_year.year_start_date, to_year.year_end_date
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def revise_budget(budget_name):
|
||||
old_budget = frappe.get_doc("Budget", budget_name)
|
||||
|
||||
if old_budget.docstatus == 1:
|
||||
old_budget.cancel()
|
||||
|
||||
new_budget = frappe.copy_doc(old_budget)
|
||||
new_budget.docstatus = 0
|
||||
new_budget.revision_of = old_budget.name
|
||||
new_budget.insert()
|
||||
|
||||
return new_budget.name
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
from frappe.client import submit
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
revise_budget,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -25,11 +27,15 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
self.company = "_Test Company"
|
||||
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
|
||||
self.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
self.cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -50,12 +56,13 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -73,13 +80,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_exception_approver_role(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -107,16 +112,16 @@ class TestBudget(ERPNextTestSuite):
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -151,14 +156,15 @@ class TestBudget(ERPNextTestSuite):
|
||||
applicable_on_purchase_order=1,
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
||||
budget_against="Cost Center",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
@@ -175,13 +181,14 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -200,7 +207,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -217,7 +224,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_yearly_budget_crossed_stop2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
@@ -237,7 +244,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation1(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -266,7 +273,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_monthly_budget_on_cancellation2(self):
|
||||
set_total_expense_zero(nowdate(), "project")
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||
month = now_datetime().month
|
||||
if month > 9:
|
||||
month = 9
|
||||
@@ -298,11 +305,17 @@ class TestBudget(ERPNextTestSuite):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Company - _TC",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -331,11 +344,14 @@ class TestBudget(ERPNextTestSuite):
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget.name,
|
||||
nowdate(),
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -372,7 +388,12 @@ class TestBudget(ERPNextTestSuite):
|
||||
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
|
||||
make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="Main Budget Cost Center 1 - _TC",
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -387,12 +408,15 @@ class TestBudget(ERPNextTestSuite):
|
||||
def test_action_for_cumulative_limit(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -422,6 +446,165 @@ class TestBudget(ERPNextTestSuite):
|
||||
po.cancel()
|
||||
jv.cancel()
|
||||
|
||||
def test_fiscal_year_validation(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2100",
|
||||
"year_start_date": "2100-04-01",
|
||||
"year_end_date": "2101-03-31",
|
||||
"companies": [{"company": "_Test Company"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
from_fiscal_year="2100",
|
||||
to_fiscal_year="2099",
|
||||
do_not_save=True,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_total_distribution_equals_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
applicable_on_cumulative_expense=True,
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for row in budget.budget_distribution:
|
||||
row.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_evenly_distribute_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
total = sum([d.amount for d in budget.budget_distribution])
|
||||
self.assertEqual(flt(total), 120000)
|
||||
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
|
||||
|
||||
def test_create_revised_budget(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
self.assertNotEqual(budget.name, revised_budget.name)
|
||||
self.assertEqual(revised_budget.budget_against, budget.budget_against)
|
||||
self.assertEqual(revised_budget.budget_amount, budget.budget_amount)
|
||||
|
||||
old_budget = frappe.get_doc("Budget", budget.name)
|
||||
self.assertEqual(old_budget.docstatus, 2)
|
||||
|
||||
def test_revision_preserves_distribution(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||
)
|
||||
|
||||
revised_name = revise_budget(budget.name)
|
||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||
|
||||
self.assertGreater(len(revised_budget.budget_distribution), 0)
|
||||
|
||||
total = sum(row.amount for row in revised_budget.budget_distribution)
|
||||
self.assertEqual(total, revised_budget.budget_amount)
|
||||
|
||||
def test_manual_budget_amount_total(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=0,
|
||||
budget_amount=30000,
|
||||
budget_start_date="2025-04-01",
|
||||
budget_end_date="2025-06-30",
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
budget.budget_distribution = []
|
||||
|
||||
for row in [
|
||||
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
|
||||
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
|
||||
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
|
||||
]:
|
||||
budget.append("budget_distribution", row)
|
||||
|
||||
budget.save()
|
||||
|
||||
total_child_amount = sum(row.amount for row in budget.budget_distribution)
|
||||
|
||||
self.assertEqual(total_child_amount, budget.budget_amount)
|
||||
|
||||
def test_fiscal_year_company_mismatch(self):
|
||||
budget = make_budget(budget_against="Cost Center", do_not_save=True, submit_budget=False)
|
||||
|
||||
fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": "2099",
|
||||
"year_start_date": "2099-04-01",
|
||||
"year_end_date": "2100-03-31",
|
||||
"companies": [{"company": "_Test Company 2"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget.from_fiscal_year = fy.name
|
||||
budget.to_fiscal_year = fy.name
|
||||
budget.company = "_Test Company"
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_manual_distribution_total_equals_budget_amount(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
cost_center="_Test Cost Center - _TC",
|
||||
distribute_equally=0,
|
||||
budget_amount=12000,
|
||||
do_not_save=False,
|
||||
submit_budget=False,
|
||||
)
|
||||
|
||||
for d in budget.budget_distribution:
|
||||
d.amount = 2000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
budget.save()
|
||||
|
||||
def test_duplicate_budget_validation(self):
|
||||
budget = make_budget(
|
||||
budget_against="Cost Center",
|
||||
distribute_equally=1,
|
||||
budget_amount=15000,
|
||||
do_not_save=False,
|
||||
submit_budget=True,
|
||||
)
|
||||
|
||||
new_budget = frappe.new_doc("Budget")
|
||||
new_budget.company = "_Test Company"
|
||||
new_budget.from_fiscal_year = budget.from_fiscal_year
|
||||
new_budget.to_fiscal_year = new_budget.from_fiscal_year
|
||||
new_budget.budget_against = "Cost Center"
|
||||
new_budget.cost_center = "_Test Cost Center - _TC"
|
||||
new_budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
new_budget.budget_amount = 10000
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
new_budget.insert()
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
@@ -430,21 +613,32 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
||||
budget_against = budget_against_CC or "_Test Cost Center - _TC"
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
|
||||
|
||||
args = frappe._dict(
|
||||
{
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"monthly_end_date": posting_date,
|
||||
"month_end_date": posting_date,
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": fiscal_year,
|
||||
"from_fiscal_year": fiscal_year,
|
||||
"to_fiscal_year": fiscal_year,
|
||||
"budget_against_field": budget_against_field,
|
||||
"budget_start_date": fiscal_year_start_date,
|
||||
"budget_end_date": fiscal_year_end_date,
|
||||
}
|
||||
)
|
||||
|
||||
if not args.get(budget_against_field):
|
||||
args[budget_against_field] = budget_against
|
||||
|
||||
args.budget_against_doctype = frappe.unscrub(budget_against_field)
|
||||
|
||||
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
|
||||
args.is_tree = True
|
||||
else:
|
||||
args.is_tree = False
|
||||
|
||||
existing_expense = get_actual_expense(args)
|
||||
|
||||
if existing_expense:
|
||||
@@ -474,18 +668,33 @@ def make_budget(**args):
|
||||
|
||||
budget_against = args.budget_against
|
||||
cost_center = args.cost_center
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project_name = "{}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"project": project,
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
else:
|
||||
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
budget_list = frappe.get_all(
|
||||
"Budget",
|
||||
filters={
|
||||
"cost_center": cost_center or "_Test Cost Center - _TC",
|
||||
"account": "_Test Account Cost for Goods Sold - _TC",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for name in budget_list:
|
||||
doc = frappe.get_doc("Budget", name)
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
frappe.delete_doc("Budget", name, force=True, ignore_missing=True)
|
||||
|
||||
budget = frappe.new_doc("Budget")
|
||||
|
||||
@@ -494,18 +703,18 @@ def make_budget(**args):
|
||||
else:
|
||||
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
||||
|
||||
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
|
||||
monthly_distribution.fiscal_year = fiscal_year
|
||||
monthly_distribution.save()
|
||||
|
||||
budget.fiscal_year = fiscal_year
|
||||
budget.monthly_distribution = "_Test Distribution"
|
||||
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
|
||||
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
|
||||
budget.company = "_Test Company"
|
||||
budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||
budget.budget_amount = args.budget_amount or 200000
|
||||
budget.applicable_on_booking_actual_expenses = 1
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
|
||||
|
||||
budget.distribution_frequency = "Monthly"
|
||||
budget.distribute_equally = args.get("distribute_equally", 1)
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
@@ -530,7 +739,13 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
budget.insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
if args.submit_budget:
|
||||
budget.submit()
|
||||
|
||||
return budget
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-12 23:31:03.841996",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"start_date",
|
||||
"end_date",
|
||||
"amount",
|
||||
"percent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "percent",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percent"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 13:18:28.398198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Distribution",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AdvanceTax(Document):
|
||||
class BudgetDistribution(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -14,14 +14,13 @@ class AdvanceTax(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_head: DF.Link | None
|
||||
allocated_amount: DF.Currency
|
||||
amount: DF.Currency
|
||||
end_date: DF.Date | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_detail: DF.Data | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_type: DF.Link | None
|
||||
percent: DF.Percent
|
||||
start_date: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -101,10 +101,11 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:47.653110",
|
||||
"modified": "2026-01-02 18:19:02.873815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -141,8 +142,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,11 +105,13 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
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,
|
||||
currency=company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import functions
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
@@ -81,10 +83,11 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 8500.0)
|
||||
self.assertEqual(je.total_credit, 8500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=["sum(debit)-sum(credit) as balance"],
|
||||
fields=[(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance")],
|
||||
)[0]
|
||||
self.assertEqual(acc_balance.balance, 8500.0)
|
||||
|
||||
@@ -146,12 +149,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(je.total_debit, 500.0)
|
||||
self.assertEqual(je.total_credit, 500.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency
|
||||
@@ -193,12 +199,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
],
|
||||
)[0]
|
||||
# account should have balance only in account currency
|
||||
@@ -235,12 +244,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||
|
||||
gl = DocType("GL Entry")
|
||||
acc_balance = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||
fields=[
|
||||
"sum(debit)-sum(credit) as balance",
|
||||
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
|
||||
(
|
||||
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
|
||||
).as_("balance_in_account_currency"),
|
||||
],
|
||||
)[0]
|
||||
# account shouldn't have balance in base and account currency post revaluation
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-09-06 09:39:46.503678",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_code",
|
||||
"display_name",
|
||||
"indentation_level",
|
||||
"data_source",
|
||||
"balance_type",
|
||||
"column_break_hxqu",
|
||||
"fieldtype",
|
||||
"color",
|
||||
"bold_text",
|
||||
"italic_text",
|
||||
"hidden_calculation",
|
||||
"hide_when_empty",
|
||||
"reverse_sign",
|
||||
"include_in_charts",
|
||||
"section_break_ornw",
|
||||
"column_break_asfe",
|
||||
"advanced_filtering",
|
||||
"filters_editor",
|
||||
"calculation_formula",
|
||||
"section_break_pvro",
|
||||
"formula_description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "Code to reference this line in formulas (e.g., REV100, EXP200, ASSET100)",
|
||||
"fieldname": "reference_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Line Reference"
|
||||
},
|
||||
{
|
||||
"description": "Text displayed on the financial statement (e.g., 'Total Revenue', 'Cash and Cash Equivalents')",
|
||||
"fieldname": "display_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Display Name"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "Indentation level: 0 = Main heading, 1 = Sub-category, 2 = Individual accounts, etc.",
|
||||
"fieldname": "indentation_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Indent Level"
|
||||
},
|
||||
{
|
||||
"description": "How this line gets its data",
|
||||
"fieldname": "data_source",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Data Source",
|
||||
"options": "\nAccount Data\nCalculated Amount\nCustom API\nBlank Line\nColumn Break\nSection Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.data_source == 'Account Data'",
|
||||
"description": "Opening Balance = Start of period, Closing Balance = End of period, Period Movement = Net change during period",
|
||||
"fieldname": "balance_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Balance Type",
|
||||
"mandatory_depends_on": "eval:doc.data_source == 'Account Data'",
|
||||
"options": "\nOpening Balance\nClosing Balance\nPeriod Movement (Debits - Credits)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hxqu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Bold text for emphasis (totals, major headings)",
|
||||
"fieldname": "bold_text",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bold Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Italic text for subtotals or notes",
|
||||
"fieldname": "italic_text",
|
||||
"fieldtype": "Check",
|
||||
"label": "Italic Text"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Calculate but don't show on final report",
|
||||
"fieldname": "hidden_calculation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hidden Line (Internal Use Only)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Hide this line if amount is zero",
|
||||
"fieldname": "hide_when_empty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide If Zero"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "0",
|
||||
"description": "Show negative values as positive (for expenses in P&L)",
|
||||
"fieldname": "reverse_sign",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reverse Sign"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ornw",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.data_source === \"Account Data\" && doc.advanced_filtering) || [\"Calculated Amount\", \"Custom API\"].includes(doc.data_source);\n",
|
||||
"fieldname": "calculation_formula",
|
||||
"fieldtype": "Code",
|
||||
"label": "Formula or Account Filter",
|
||||
"mandatory_depends_on": "eval:doc.data_source != 'Blank Line' && doc.data_source != 'Column Break' && doc.data_source != 'Section Break'"
|
||||
},
|
||||
{
|
||||
"fieldname": "formula_description",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, this row's values will be displayed on financial charts",
|
||||
"fieldname": "include_in_charts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include in Charts"
|
||||
},
|
||||
{
|
||||
"description": "Color to highlight values (e.g., red for exceptions)",
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"label": "Color"
|
||||
},
|
||||
{
|
||||
"description": "How to format and present values in the financial report (only if different from column fieldtype)",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"label": "Value Type",
|
||||
"options": "\nCurrency\nFloat\nInt\nPercent"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.data_source === \"Account Data\" && !doc.advanced_filtering",
|
||||
"fieldname": "filters_editor",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: ![\"Blank Line\", \"Column Break\", \"Section Break\"].includes(doc.data_source);",
|
||||
"fieldname": "column_break_asfe",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.data_source === \"Account Data\"",
|
||||
"description": "Use <strong>Python</strong> filters to get Accounts",
|
||||
"fieldname": "advanced_filtering",
|
||||
"fieldtype": "Check",
|
||||
"label": "Advanced Filtering",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pvro",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 09:23:27.208072",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Row",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class FinancialReportRow(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
advanced_filtering: DF.Check
|
||||
balance_type: DF.Literal[
|
||||
"", "Opening Balance", "Closing Balance", "Period Movement (Debits - Credits)"
|
||||
]
|
||||
bold_text: DF.Check
|
||||
calculation_formula: DF.Code | None
|
||||
color: DF.Color | None
|
||||
data_source: DF.Literal[
|
||||
"",
|
||||
"Account Data",
|
||||
"Calculated Amount",
|
||||
"Custom API",
|
||||
"Blank Line",
|
||||
"Column Break",
|
||||
"Section Break",
|
||||
]
|
||||
display_name: DF.Data | None
|
||||
fieldtype: DF.Literal["", "Currency", "Float", "Int", "Percent"]
|
||||
hidden_calculation: DF.Check
|
||||
hide_when_empty: DF.Check
|
||||
include_in_charts: DF.Check
|
||||
indentation_level: DF.Int
|
||||
italic_text: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_code: DF.Data | None
|
||||
reverse_sign: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,433 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Financial Report Template", {
|
||||
refresh(frm) {
|
||||
// add custom button to view missed accounts
|
||||
frm.add_custom_button(__("View Account Coverage"), function () {
|
||||
let selected_rows = frm.get_field("rows").grid.get_selected_children();
|
||||
const has_selection = selected_rows.length > 0;
|
||||
if (selected_rows.length === 0) selected_rows = frm.doc.rows;
|
||||
|
||||
show_accounts_tree(selected_rows, has_selection);
|
||||
});
|
||||
|
||||
// add custom button to open the financial report
|
||||
frm.add_custom_button(__("View Report"), function () {
|
||||
frappe.set_route("query-report", frm.doc.report_type, {
|
||||
report_template: frm.doc.name,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
validate(frm) {
|
||||
if (!frm.doc.rows || frm.doc.rows.length === 0) {
|
||||
frappe.msgprint(__("At least one row is required for a financial report template"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Financial Report Row", {
|
||||
data_source(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
update_formula_label(frm, row.data_source);
|
||||
update_formula_description(frm, row.data_source);
|
||||
|
||||
if (row.data_source !== "Account Data") {
|
||||
frappe.model.set_value(cdt, cdn, "balance_type", "");
|
||||
}
|
||||
|
||||
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
|
||||
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
|
||||
}
|
||||
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
form_render(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
update_formula_label(frm, row.data_source);
|
||||
update_advanced_formula_property(frm, cdt, cdn);
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
update_formula_description(frm, row.data_source);
|
||||
},
|
||||
|
||||
calculation_formula(frm, cdt, cdn) {
|
||||
update_advanced_formula_property(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
advanced_filtering(frm, cdt, cdn) {
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
},
|
||||
});
|
||||
|
||||
// FILTERS EDITOR
|
||||
|
||||
function set_up_filters_editor(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
if (row.data_source !== "Account Data" || row.advanced_filtering) return;
|
||||
|
||||
const grid_row = frm.fields_dict["rows"].grid.get_row(cdn);
|
||||
const wrapper = grid_row.get_field("filters_editor").$wrapper;
|
||||
wrapper.empty();
|
||||
|
||||
const ACCOUNT = "Account";
|
||||
const FIELD_IDX = 1;
|
||||
const OPERATOR_IDX = 2;
|
||||
const VALUE_IDX = 3;
|
||||
|
||||
// Parse saved filters
|
||||
let saved_filters = [];
|
||||
|
||||
if (row.calculation_formula) {
|
||||
try {
|
||||
const parsed = JSON.parse(row.calculation_formula);
|
||||
|
||||
if (Array.isArray(parsed)) saved_filters = [parsed];
|
||||
else if (parsed.and) saved_filters = parsed.and;
|
||||
} catch (e) {
|
||||
frappe.show_alert({
|
||||
message: __("Invalid filter formula. Please check the syntax."),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (saved_filters.length)
|
||||
// Ensure every filter starts with "Account"
|
||||
saved_filters = saved_filters.map((f) => [ACCOUNT, ...f]);
|
||||
|
||||
frappe.model.with_doctype(ACCOUNT, () => {
|
||||
const filter_group = new frappe.ui.FilterGroup({
|
||||
parent: wrapper,
|
||||
doctype: ACCOUNT,
|
||||
on_change: () => {
|
||||
// only need [[field, operator, value]]
|
||||
const filters = filter_group
|
||||
.get_filters()
|
||||
.map((f) => [f[FIELD_IDX], f[OPERATOR_IDX], f[VALUE_IDX]]);
|
||||
|
||||
const current = filters.length > 1 ? { and: filters } : filters[0];
|
||||
frappe.model.set_value(cdt, cdn, "calculation_formula", JSON.stringify(current));
|
||||
},
|
||||
});
|
||||
|
||||
filter_group.add_filters_to_filter_group(saved_filters);
|
||||
});
|
||||
}
|
||||
|
||||
function update_advanced_formula_property(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
const is_advanced = is_advanced_formula(row);
|
||||
|
||||
frm.set_df_property("rows", "read_only", is_advanced, frm.doc.name, "advanced_filtering", cdn);
|
||||
|
||||
if (is_advanced && !row.advanced_filtering) {
|
||||
row.advanced_filtering = 1;
|
||||
frm.refresh_field("rows");
|
||||
}
|
||||
}
|
||||
|
||||
function is_advanced_formula(row) {
|
||||
if (!row || row.data_source !== "Account Data") return false;
|
||||
|
||||
let parsed = null;
|
||||
if (row.calculation_formula) {
|
||||
try {
|
||||
parsed = JSON.parse(row.calculation_formula);
|
||||
} catch (e) {
|
||||
console.warn("Invalid JSON in calculation_formula:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed)) return false;
|
||||
if (parsed?.or) return true;
|
||||
if (parsed?.and) return parsed.and.some((cond) => !Array.isArray(cond));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ACCOUNTS TREE VIEW
|
||||
|
||||
function show_accounts_tree(template_rows, has_selection) {
|
||||
// filtered rows
|
||||
const account_rows = template_rows.filter((row) => row.data_source === "Account Data");
|
||||
|
||||
if (account_rows.length === 0) {
|
||||
frappe.show_alert(__("No <strong>Account Data</strong> row found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Accounts Missing from Report"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
label: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
onchange: () => {
|
||||
const company_field = dialog.get_field("company");
|
||||
if (!company_field.value || company_field.value === company_field.last_value) return;
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "view_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Missing Accounts", "Filtered Accounts"],
|
||||
label: "View",
|
||||
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
dialog.set_title(
|
||||
dialog.get_value("view_type") === "Missing Accounts"
|
||||
? __("Accounts Missing from Report")
|
||||
: __("Accounts Included in Report")
|
||||
);
|
||||
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "tip",
|
||||
fieldtype: "HTML",
|
||||
label: "Tip",
|
||||
options: `
|
||||
<div class="alert alert-success" role="alert">
|
||||
Tip: Select report lines to view their accounts
|
||||
</div>
|
||||
`,
|
||||
depends_on: has_selection ? "eval: false" : "eval: true",
|
||||
},
|
||||
{
|
||||
fieldname: "tree_area",
|
||||
fieldtype: "HTML",
|
||||
label: "Chart of Accounts",
|
||||
read_only: 1,
|
||||
depends_on: "eval: doc.company",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Done"),
|
||||
primary_action() {
|
||||
dialog.hide();
|
||||
},
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
refresh_tree_view(dialog, account_rows);
|
||||
}
|
||||
|
||||
async function refresh_tree_view(dialog, account_rows) {
|
||||
const missed = dialog.get_value("view_type") === "Missing Accounts";
|
||||
const company = dialog.get_value("company");
|
||||
|
||||
const wrapper = dialog.get_field("tree_area").$wrapper;
|
||||
wrapper.empty();
|
||||
|
||||
// get filtered accounts
|
||||
const { message: filtered_accounts } = await frappe.call({
|
||||
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_filtered_accounts",
|
||||
args: { company: company, account_rows: account_rows },
|
||||
});
|
||||
|
||||
// render tree
|
||||
const tree = new FilteredTree({
|
||||
parent: wrapper,
|
||||
label: company,
|
||||
root_value: company,
|
||||
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
|
||||
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
|
||||
toolbar: [],
|
||||
});
|
||||
|
||||
tree.load_children(tree.root_node, true);
|
||||
}
|
||||
|
||||
class FilteredTree extends frappe.ui.Tree {
|
||||
render_children_of_all_nodes(data_list) {
|
||||
data_list = this.get_filtered_data_list(data_list);
|
||||
super.render_children_of_all_nodes(data_list);
|
||||
}
|
||||
|
||||
get_filtered_data_list(data_list) {
|
||||
let removed_nodes = new Set();
|
||||
|
||||
// Filter nodes with no data
|
||||
data_list = data_list.filter((d) => {
|
||||
if (d.data.length === 0) {
|
||||
removed_nodes.add(d.parent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove references to removed nodes and iteratively remove empty parents
|
||||
while (removed_nodes.size > 0) {
|
||||
const current_removed = [...removed_nodes];
|
||||
removed_nodes.clear();
|
||||
|
||||
data_list = data_list.filter((d) => {
|
||||
d.data = d.data.filter((a) => !current_removed.includes(a.value));
|
||||
|
||||
if (d.data.length === 0) {
|
||||
removed_nodes.add(d.parent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return data_list;
|
||||
}
|
||||
}
|
||||
|
||||
function update_formula_label(frm, data_source) {
|
||||
const grid = frm.fields_dict.rows.grid;
|
||||
const field = grid.fields_map.calculation_formula;
|
||||
if (!field) return;
|
||||
|
||||
const labels = {
|
||||
"Account Data": "Account Filter",
|
||||
"Custom API": "API Method Path",
|
||||
};
|
||||
|
||||
grid.update_docfield_property(
|
||||
"calculation_formula",
|
||||
"label",
|
||||
labels[data_source] || "Calculation Formula"
|
||||
);
|
||||
}
|
||||
|
||||
// FORMULA DESCRIPTION
|
||||
|
||||
function update_formula_description(frm, data_source) {
|
||||
if (!data_source) return;
|
||||
|
||||
let grid = frm.fields_dict.rows.grid;
|
||||
let field = grid.fields_map.formula_description;
|
||||
if (!field) return;
|
||||
|
||||
// Common CSS styles and elements
|
||||
const container_style = `style="padding: var(--padding-md); border: 1px solid var(--border-color); border-radius: var(--border-radius); margin-top: var(--margin-sm);"`;
|
||||
const title_style = `style="margin-top: 0; color: var(--text-color);"`;
|
||||
const subtitle_style = `style="color: var(--text-color); margin-bottom: var(--margin-xs);"`;
|
||||
const text_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted);"`;
|
||||
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
|
||||
|
||||
let description_html = "";
|
||||
|
||||
if (data_source === "Account Data") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Account Filter Guide</h5>
|
||||
<p ${text_style}>Specify which accounts to include in this line.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Basic Examples:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>["account_type", "=", "Cash"]</code> - All Cash accounts</li>
|
||||
<li><code>["root_type", "in", ["Asset", "Liability"]]</code> - All Asset and Liability accounts</li>
|
||||
<li><code>["account_category", "like", "Revenue"]</code> - Revenue accounts</li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Multiple Conditions (AND/OR):</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>{"and": [["root_type", "=", "Asset"], ["account_type", "=", "Cash"]]}</code></li>
|
||||
<li><code>{"or": [["account_category", "like", "Revenue"], ["account_category", "like", "Income"]]}</code></li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Available operators:</strong> <code>=, !=, in, not in, like, not like, is</code></p>
|
||||
<p ${tip_style}><strong>Multi-Company Tip:</strong> Use fields like <code>account_type</code>, <code>root_type</code>, and <code>account_category</code> for templates that work across multiple companies.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Calculated Amount") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Formula Guide</h5>
|
||||
<p ${text_style}>Create calculations using reference codes from other lines.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Basic Examples:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>REV100 + REV200</code> - Add two revenue lines</li>
|
||||
<li><code>ASSETS - LIABILITIES</code> - Calculate equity</li>
|
||||
<li><code>REVENUE * 0.1</code> - 10% of revenue</li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Common Functions:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>abs(value)</code> - Remove negative sign</li>
|
||||
<li><code>round(value)</code> - Round to whole number</li>
|
||||
<li><code>max(val1, val2)</code> - Larger of two values</li>
|
||||
<li><code>min(val1, val2)</code> - Smaller of two values</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Required:</strong> Use "Reference Code" from other rows in your formulas.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Custom API") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Custom API Setup</h5>
|
||||
<p ${text_style}>Path to your custom method that returns financial data.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Format:</h6>
|
||||
<ul ${list_style}>
|
||||
<li><code>erpnext.custom.financial_apis.get_custom_revenue</code></li>
|
||||
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Return Format:</h6>
|
||||
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
||||
</div>`;
|
||||
} else if (data_source === "Blank Line") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Blank Line</h5>
|
||||
<p ${text_style}>Adds empty space for better visual separation.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Separating major sections</li>
|
||||
<li>Adding space before totals</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - creates visual spacing only.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Column Break") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Column Break</h5>
|
||||
<p ${text_style}>Creates a visual break for side-by-side layout.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Horizontal P&L statements</li>
|
||||
<li>Side-by-side Balance Sheet sections</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
|
||||
</div>`;
|
||||
} else if (data_source === "Section Break") {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Section Break</h5>
|
||||
<p ${text_style}>Creates a visual break for separating different sections.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Use For:</h6>
|
||||
<ul ${list_style}>
|
||||
<li>Separating major sections in a report - say trading & profit and loss</li>
|
||||
<li>Improving readability by adding space</li>
|
||||
</ul>
|
||||
|
||||
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
grid.update_docfield_property("formula_description", "options", description_html);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2025-08-02 04:44:15.184541",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"template_name",
|
||||
"report_type",
|
||||
"module",
|
||||
"column_break_lvnq",
|
||||
"disabled",
|
||||
"section_break_fvlw",
|
||||
"rows"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Descriptive name for your template (e.g., 'Standard P&L', 'Detailed Balance Sheet')",
|
||||
"fieldname": "template_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Template Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"description": "Type of financial statement this template generates",
|
||||
"fieldname": "report_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:frappe.boot.developer_mode",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module (for Export)",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lvnq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fvlw",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "rows",
|
||||
"fieldtype": "Table",
|
||||
"label": "Report Line Items",
|
||||
"options": "Financial Report Row"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Disable template to prevent use in reports",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-14 00:11:03.508139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Template",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "template_name"
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.account_category.account_category import import_account_categories
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import TemplateValidator
|
||||
|
||||
|
||||
class FinancialReportTemplate(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_row.financial_report_row import FinancialReportRow
|
||||
|
||||
disabled: DF.Check
|
||||
module: DF.Link | None
|
||||
report_type: DF.Literal[
|
||||
"", "Profit and Loss Statement", "Balance Sheet", "Cash Flow", "Custom Financial Statement"
|
||||
]
|
||||
rows: DF.Table[FinancialReportRow]
|
||||
template_name: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
validator = TemplateValidator(self)
|
||||
result = validator.validate()
|
||||
result.notify_user()
|
||||
|
||||
def on_update(self):
|
||||
self._export_template()
|
||||
|
||||
def on_trash(self):
|
||||
self._delete_template()
|
||||
|
||||
def _export_template(self):
|
||||
from frappe.modules.utils import export_module_json
|
||||
|
||||
if not self.module:
|
||||
return
|
||||
|
||||
export_module_json(self, True, self.module)
|
||||
self._export_account_categories()
|
||||
|
||||
def _delete_template(self):
|
||||
if not self.module or not frappe.conf.developer_mode:
|
||||
return
|
||||
|
||||
module_path = frappe.get_module_path(self.module)
|
||||
dir_path = os.path.join(module_path, "financial_report_template", frappe.scrub(self.name))
|
||||
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
|
||||
def _export_account_categories(self):
|
||||
import json
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FormulaFieldExtractor,
|
||||
)
|
||||
|
||||
if not self.module or not frappe.conf.developer_mode or frappe.flags.in_import:
|
||||
return
|
||||
|
||||
# Extract category from rows
|
||||
extractor = FormulaFieldExtractor(
|
||||
field_name="account_category", exclude_operators=["like", "not like"]
|
||||
)
|
||||
account_data_rows = [row for row in self.rows if row.data_source == "Account Data"]
|
||||
category_names = extractor.extract_from_rows(account_data_rows)
|
||||
|
||||
if not category_names:
|
||||
return
|
||||
|
||||
# Get path
|
||||
module_path = frappe.get_module_path(self.module)
|
||||
categories_file = os.path.join(module_path, "financial_report_template", "account_categories.json")
|
||||
|
||||
# Load existing categories
|
||||
existing_categories = {}
|
||||
if os.path.exists(categories_file):
|
||||
try:
|
||||
with open(categories_file) as f:
|
||||
existing_data = json.load(f)
|
||||
existing_categories = {cat["account_category_name"]: cat for cat in existing_data}
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass # Create new file
|
||||
|
||||
# Fetch categories from database
|
||||
if category_names:
|
||||
db_categories = frappe.get_all(
|
||||
"Account Category",
|
||||
filters={"account_category_name": ["in", list(category_names)]},
|
||||
fields=["account_category_name", "description"],
|
||||
)
|
||||
|
||||
for cat in db_categories:
|
||||
existing_categories[cat["account_category_name"]] = cat
|
||||
|
||||
# Sort by category name
|
||||
sorted_categories = sorted(existing_categories.values(), key=lambda x: x["account_category_name"])
|
||||
|
||||
# Write to file
|
||||
os.makedirs(os.path.dirname(categories_file), exist_ok=True)
|
||||
with open(categories_file, "w") as f:
|
||||
json.dump(sorted_categories, f, indent=2)
|
||||
|
||||
|
||||
def sync_financial_report_templates(chart_of_accounts=None, existing_company=None):
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import get_chart
|
||||
|
||||
# If COA is being created for an existing company,
|
||||
# skip syncing templates as they are likely already present
|
||||
if existing_company:
|
||||
return
|
||||
|
||||
# Allow regional templates to completely override ERPNext
|
||||
# templates based on the chart of accounts selected
|
||||
disable_default_financial_report_template = False
|
||||
if chart_of_accounts:
|
||||
coa = get_chart(chart_of_accounts)
|
||||
if coa.get("disable_default_financial_report_template", False):
|
||||
disable_default_financial_report_template = True
|
||||
|
||||
installed_apps = frappe.get_installed_apps()
|
||||
|
||||
for app in installed_apps:
|
||||
if disable_default_financial_report_template and app == "erpnext":
|
||||
continue
|
||||
|
||||
_sync_templates_for(app)
|
||||
|
||||
|
||||
def _sync_templates_for(app_name):
|
||||
templates = []
|
||||
|
||||
for module_name in frappe.local.app_modules.get(app_name) or []:
|
||||
module_path = frappe.get_module_path(module_name)
|
||||
template_path = os.path.join(module_path, "financial_report_template")
|
||||
|
||||
if not os.path.isdir(template_path):
|
||||
continue
|
||||
|
||||
import_account_categories(template_path)
|
||||
|
||||
for template_dir in os.listdir(template_path):
|
||||
json_file = os.path.join(template_path, template_dir, f"{template_dir}.json")
|
||||
if os.path.isfile(json_file):
|
||||
templates.append(json_file)
|
||||
|
||||
if not templates:
|
||||
return
|
||||
|
||||
# ensure files are not exported
|
||||
frappe.flags.in_import = True
|
||||
|
||||
for template_path in templates:
|
||||
with open(template_path) as f:
|
||||
template_data = frappe._dict(frappe.parse_json(f.read()))
|
||||
|
||||
template_name = template_data.get("name")
|
||||
|
||||
if not frappe.db.exists("Financial Report Template", template_name):
|
||||
doc = frappe.get_doc(template_data)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert()
|
||||
|
||||
frappe.flags.in_import = False
|
||||
@@ -0,0 +1,545 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.database.query import SQLFunctionParser
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""Represents a single validation issue"""
|
||||
|
||||
message: str
|
||||
row_idx: int | None = None
|
||||
field: str | None = None
|
||||
details: dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.details is None:
|
||||
self.details = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
|
||||
field_info = f"[{self.field}] " if self.field else ""
|
||||
message = f"{prefix}{field_info}{self.message}"
|
||||
return _(message)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
issues: list[ValidationIssue] = field(default_factory=list)
|
||||
warnings: list[ValidationIssue] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return len(self.issues) == 0
|
||||
|
||||
@property
|
||||
def has_warnings(self) -> bool:
|
||||
return len(self.warnings) > 0
|
||||
|
||||
@property
|
||||
def error_count(self) -> int:
|
||||
return len(self.issues)
|
||||
|
||||
@property
|
||||
def warning_count(self) -> int:
|
||||
return len(self.warnings)
|
||||
|
||||
def merge(self, other: "ValidationResult") -> "ValidationResult":
|
||||
self.issues.extend(other.issues)
|
||||
self.warnings.extend(other.warnings)
|
||||
return self
|
||||
|
||||
def add_error(self, issue: ValidationIssue) -> None:
|
||||
"""Add a critical error that prevents functionality"""
|
||||
self.issues.append(issue)
|
||||
|
||||
def add_warning(self, issue: ValidationIssue) -> None:
|
||||
"""Add a warning for recommendatory validation"""
|
||||
self.warnings.append(issue)
|
||||
|
||||
def notify_user(self) -> None:
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues)
|
||||
|
||||
if warnings:
|
||||
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
||||
|
||||
if errors:
|
||||
frappe.throw(errors, title=_("Errors"))
|
||||
|
||||
|
||||
class TemplateValidator:
|
||||
"""Main validator that orchestrates all validations"""
|
||||
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.validators = [
|
||||
TemplateStructureValidator(),
|
||||
DependencyValidator(template),
|
||||
]
|
||||
self.formula_validator = FormulaValidator(template)
|
||||
|
||||
def validate(self) -> ValidationResult:
|
||||
result = ValidationResult([])
|
||||
|
||||
# Run template-level validators
|
||||
for validator in self.validators:
|
||||
result.merge(validator.validate(self.template))
|
||||
|
||||
# Run row-level validations
|
||||
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
for row in self.template.rows:
|
||||
result.merge(self.formula_validator.validate(row, account_fields))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Validator(ABC):
|
||||
@abstractmethod
|
||||
def validate(self, context: Any) -> ValidationResult:
|
||||
pass
|
||||
|
||||
|
||||
class TemplateStructureValidator(Validator):
|
||||
def validate(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
result.merge(self._validate_reference_codes(template))
|
||||
result.merge(self._validate_required_fields(template))
|
||||
|
||||
return result
|
||||
|
||||
def _validate_reference_codes(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
used_codes = set()
|
||||
|
||||
for row in template.rows:
|
||||
if not row.reference_code:
|
||||
continue
|
||||
|
||||
ref_code = row.reference_code.strip()
|
||||
|
||||
# Check format
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Check uniqueness
|
||||
if ref_code in used_codes:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Duplicate line reference: '{ref_code}'",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
used_codes.add(ref_code)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_required_fields(self, template) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
for row in template.rows:
|
||||
# Balance type required
|
||||
if row.data_source == "Account Data" and not row.balance_type:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Balance Type is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Calculation formula required
|
||||
if row.data_source in ["Account Data", "Calculated Amount", "Custom API"]:
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula is required for {row.data_source}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DependencyValidator(Validator):
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.dependencies = self._build_dependency_graph()
|
||||
|
||||
def validate(self, context=None) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
result.merge(self._validate_circular_dependencies())
|
||||
result.merge(self._validate_missing_dependencies())
|
||||
|
||||
return result
|
||||
|
||||
def _build_dependency_graph(self) -> dict[str, list[str]]:
|
||||
graph = {}
|
||||
available_codes = {row.reference_code for row in self.template.rows if row.reference_code}
|
||||
|
||||
for row in self.template.rows:
|
||||
if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
|
||||
deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
|
||||
if deps:
|
||||
graph[row.reference_code] = deps
|
||||
|
||||
return graph
|
||||
|
||||
def _validate_circular_dependencies(self) -> ValidationResult:
|
||||
"""
|
||||
Efficient cycle detection using DFS (Depth-First Search) with three-color algorithm:
|
||||
- WHITE (0): unvisited node
|
||||
- GRAY (1): currently being processed (on recursion stack)
|
||||
- BLACK (2): fully processed
|
||||
|
||||
Example cycle detection:
|
||||
A → B → C → A (cycle detected when A is GRAY and visited again)
|
||||
"""
|
||||
result = ValidationResult()
|
||||
WHITE, GRAY, BLACK = 0, 1, 2
|
||||
colors = {node: WHITE for node in self.dependencies}
|
||||
|
||||
def dfs(node, path):
|
||||
if node not in colors:
|
||||
return # External dependency
|
||||
|
||||
if colors[node] == GRAY:
|
||||
# Found cycle
|
||||
cycle_start = path.index(node)
|
||||
cycle = [*path[cycle_start:], node]
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Circular dependency detected: {' → '.join(cycle)}",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if colors[node] == BLACK:
|
||||
return # Already processed
|
||||
|
||||
colors[node] = GRAY
|
||||
path.append(node)
|
||||
|
||||
for neighbor in self.dependencies.get(node, []):
|
||||
dfs(neighbor, path.copy())
|
||||
|
||||
colors[node] = BLACK
|
||||
|
||||
for node in self.dependencies:
|
||||
if colors[node] == WHITE:
|
||||
dfs(node, [])
|
||||
|
||||
return result
|
||||
|
||||
def _validate_missing_dependencies(self) -> ValidationResult:
|
||||
available = {row.reference_code for row in self.template.rows if row.reference_code}
|
||||
result = ValidationResult()
|
||||
|
||||
for ref_code, deps in self.dependencies.items():
|
||||
undefined = [d for d in deps if d not in available]
|
||||
if undefined:
|
||||
row_idx = self._get_row_idx(ref_code)
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Line References undefined in Formula: {', '.join(undefined)}",
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _get_row_idx(self, reference_code: str) -> int | None:
|
||||
for row in self.template.rows:
|
||||
if row.reference_code == reference_code:
|
||||
return row.idx
|
||||
return None
|
||||
|
||||
|
||||
class CalculationFormulaValidator(Validator):
|
||||
"""Validates calculation formulas used in Calculated Amount rows"""
|
||||
|
||||
def __init__(self, reference_codes: set[str]):
|
||||
self.reference_codes = reference_codes
|
||||
|
||||
def validate(self, row) -> ValidationResult:
|
||||
"""Validate calculation formula for a single row"""
|
||||
result = ValidationResult()
|
||||
|
||||
if row.data_source != "Calculated Amount":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula is required for Calculated Amount",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
formula = self._preprocess_formula(row.calculation_formula)
|
||||
row.calculation_formula = formula
|
||||
|
||||
# Check parentheses
|
||||
if not self._are_parentheses_balanced(formula):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula has unbalanced parentheses",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Check self-reference
|
||||
available_codes = list(self.reference_codes)
|
||||
refs = extract_reference_codes_from_formula(formula, available_codes)
|
||||
if row.reference_code and row.reference_code in refs:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references itself ('{row.reference_code}')",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Check undefined references
|
||||
undefined = set(refs) - set(available_codes)
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references undefined codes: {', '.join(undefined)}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Try to evaluate with dummy values
|
||||
eval_error = self._test_formula_evaluation(formula, available_codes)
|
||||
if eval_error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula evaluation error: {eval_error}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _preprocess_formula(self, formula: str) -> str:
|
||||
if not formula or not isinstance(formula, str):
|
||||
return ""
|
||||
|
||||
return formula.strip()
|
||||
|
||||
@staticmethod
|
||||
def _are_parentheses_balanced(formula: str) -> bool:
|
||||
return formula.count("(") == formula.count(")")
|
||||
|
||||
def _test_formula_evaluation(self, formula: str, available_codes: list[str]) -> str | None:
|
||||
try:
|
||||
context = {code: 1.0 for code in available_codes}
|
||||
context.update(
|
||||
{
|
||||
"abs": abs,
|
||||
"round": round,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"sum": sum,
|
||||
"sqrt": lambda x: x**0.5,
|
||||
"pow": pow,
|
||||
"ceil": lambda x: int(x) + (1 if x % 1 else 0),
|
||||
"floor": lambda x: int(x),
|
||||
}
|
||||
)
|
||||
|
||||
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
|
||||
|
||||
if not isinstance(result, (int, float)): # noqa: UP038
|
||||
return f"Formula must return a numeric value, got {type(result).__name__}"
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
class AccountFilterValidator(Validator):
|
||||
"""Validates account filter expressions used in Account Data rows"""
|
||||
|
||||
def __init__(self, account_fields: set | None = None):
|
||||
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
|
||||
|
||||
def validate(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if row.data_source != "Account Data":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Account filter is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
filter_config = json.loads(row.calculation_formula)
|
||||
error = self._validate_filter_structure(filter_config, self.account_fields)
|
||||
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=error,
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid JSON format: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
return "Filter must be [field, operator, value]"
|
||||
|
||||
field, operator, value = filter_config
|
||||
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return "Field and operator must be strings"
|
||||
|
||||
if field not in account_fields:
|
||||
return f"Field '{field}' is not a valid account field"
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return f"Invalid operator '{operator}'"
|
||||
|
||||
if operator in ["in", "not in"] and not isinstance(value, list):
|
||||
return f"Operator '{operator}' requires a list value"
|
||||
|
||||
# logical condition: {"and": [condition1, condition2]}
|
||||
elif isinstance(filter_config, dict):
|
||||
if len(filter_config) != 1:
|
||||
return "Logical condition must have exactly one operator"
|
||||
|
||||
op = next(iter(filter_config.keys())).lower()
|
||||
if op not in ["and", "or"]:
|
||||
return "Logical operators must be 'and' or 'or'"
|
||||
|
||||
conditions = filter_config[next(iter(filter_config.keys()))]
|
||||
if not isinstance(conditions, list) or len(conditions) < 1:
|
||||
return "Logical conditions need at least 1 sub-condition"
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
error = self._validate_filter_structure(condition, account_fields)
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
return "Filter must be a list or dict"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class FormulaValidator(Validator):
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
reference_codes = {row.reference_code for row in template.rows if row.reference_code}
|
||||
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
||||
self.account_filter_validator = AccountFilterValidator()
|
||||
|
||||
def validate(self, row, account_fields: set) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if not row.calculation_formula:
|
||||
return result
|
||||
|
||||
if row.data_source == "Calculated Amount":
|
||||
return self.calculation_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Account Data":
|
||||
# Update account fields if provided
|
||||
if account_fields:
|
||||
self.account_filter_validator.account_fields = account_fields
|
||||
return self.account_filter_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Custom API":
|
||||
result.merge(self._validate_custom_api(row))
|
||||
|
||||
return result
|
||||
|
||||
def _validate_custom_api(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
api_path = row.calculation_formula
|
||||
|
||||
if "." not in api_path:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Custom API path should be in format: app.module.method",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Method exists?
|
||||
try:
|
||||
module_path, method_name = api_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
|
||||
if not hasattr(module, method_name):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Could not validate API path: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_reference_codes_from_formula(formula: str, available_codes: list[str]) -> list[str]:
|
||||
found_codes = []
|
||||
for code in available_codes:
|
||||
# Match complete words only to avoid partial matches
|
||||
pattern = r"\b" + re.escape(code) + r"\b"
|
||||
if re.search(pattern, formula):
|
||||
found_codes.append(code)
|
||||
return found_codes
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests.utils import make_test_records
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class TestFinancialReportTemplate(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class FinancialReportTemplateTestCase(IntegrationTestCase):
|
||||
"""Utility class with common setup and helper methods for all test classes"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up test data"""
|
||||
make_test_records("Company")
|
||||
make_test_records("Fiscal Year")
|
||||
cls.create_test_template()
|
||||
|
||||
@classmethod
|
||||
def create_test_template(cls):
|
||||
"""Create a test financial report template"""
|
||||
if not frappe.db.exists("Financial Report Template", "Test P&L Template"):
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Financial Report Template",
|
||||
"template_name": "Test P&L Template",
|
||||
"report_type": "Profit and Loss Statement",
|
||||
"rows": [
|
||||
{
|
||||
"reference_code": "INC001",
|
||||
"display_name": "Income",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": '["root_type", "=", "Income"]',
|
||||
},
|
||||
{
|
||||
"reference_code": "EXP001",
|
||||
"display_name": "Expenses",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": '["root_type", "=", "Expense"]',
|
||||
},
|
||||
{
|
||||
"reference_code": "NET001",
|
||||
"display_name": "Net Profit/Loss",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Calculated Amount",
|
||||
"bold_text": 1,
|
||||
"calculation_formula": "INC001 - EXP001",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
cls.test_template = frappe.get_doc("Financial Report Template", "Test P&L Template")
|
||||
|
||||
@staticmethod
|
||||
def create_test_template_with_rows(rows_data):
|
||||
"""Helper method to create test template with specific rows"""
|
||||
template_name = f"Test Template {frappe.generate_hash()[:8]}"
|
||||
template = frappe.get_doc(
|
||||
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
|
||||
)
|
||||
return template
|
||||
@@ -4,6 +4,7 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "fiscal_year",
|
||||
"non_standard_fieldnames": {"Budget": "from_fiscal_year"},
|
||||
"transactions": [
|
||||
{"label": _("Budgets"), "items": ["Budget"]},
|
||||
{"label": _("References"), "items": ["Period Closing Voucher"]},
|
||||
|
||||
@@ -100,7 +100,7 @@ class GLEntry(Document):
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
@@ -193,7 +193,6 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -207,7 +206,6 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -276,7 +274,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
@@ -419,16 +417,16 @@ def update_outstanding_amt(
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
def validate_frozen_account(company, account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
if not role_allowed_for_frozen_entries:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
@@ -442,7 +440,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = {
|
||||
return [__("Disbursed"), "blue", "status,=,Disbursed"];
|
||||
} else if (doc.status == "Settled") {
|
||||
return [__("Settled"), "orange", "status,=,Settled"];
|
||||
} else if (doc.status == "Canceled") {
|
||||
return [__("Canceled"), "red", "status,=,Canceled"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2022-09-13 16:18:59.404842",
|
||||
"creation": "2025-07-17 12:24:05.609186",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_name",
|
||||
"item_row",
|
||||
"tax_row",
|
||||
"rate",
|
||||
"amount",
|
||||
"taxable_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldname": "item_row",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type"
|
||||
"label": "Item Row",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldname": "tax_row",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name"
|
||||
"label": "Tax Row",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -31,17 +46,18 @@
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified": "2025-09-26 15:54:19.750714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
"naming_rule": "Random",
|
||||
"name": "Item Wise Tax Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxWithheldVouchers(Document):
|
||||
class ItemWiseTaxDetail(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -14,12 +14,14 @@ class TaxWithheldVouchers(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
item_row: DF.Data
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
rate: DF.Float
|
||||
tax_row: DF.Data
|
||||
taxable_amount: DF.Currency
|
||||
voucher_name: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -43,6 +43,20 @@ frappe.ui.form.on("Journal Entry", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
@@ -111,6 +125,12 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
|
||||
if (frm.doc.voucher_type !== "Exchange Gain Or Loss") {
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
before_save: function (frm) {
|
||||
if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) {
|
||||
@@ -197,6 +217,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
@@ -247,6 +268,10 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
});
|
||||
|
||||
var update_jv_details = function (doc, r) {
|
||||
@@ -716,6 +741,8 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
erpnext.journal_entry.clear_fields(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
@@ -740,4 +767,13 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
clear_fields: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
|
||||
row.party_type = null;
|
||||
row.party = null;
|
||||
row.bank_account = null;
|
||||
|
||||
frm.refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
@@ -517,7 +522,7 @@
|
||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
"label": "Consider for Tax Withholding "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
@@ -586,6 +591,39 @@
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -600,7 +638,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
@@ -17,9 +18,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -33,6 +32,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -49,6 +49,7 @@ class JournalEntry(AccountsController):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -65,6 +66,7 @@ class JournalEntry(AccountsController):
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
is_system_generated: DF.Check
|
||||
@@ -73,6 +75,7 @@ class JournalEntry(AccountsController):
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -84,6 +87,8 @@ class JournalEntry(AccountsController):
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
title: DF.Data | None
|
||||
total_amount: DF.Currency
|
||||
total_amount_currency: DF.Link | None
|
||||
@@ -150,8 +155,8 @@ class JournalEntry(AccountsController):
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
JournalTaxWithholding(self).on_validate()
|
||||
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
@@ -175,15 +180,16 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
queue_submission(self, "_submit")
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
queue_submission(self, "_cancel")
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
@@ -199,6 +205,7 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
JournalTaxWithholding(self).on_submit()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
@@ -282,6 +289,8 @@ class JournalEntry(AccountsController):
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# Cancel tax withholding entries
|
||||
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -295,8 +304,10 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
JournalTaxWithholding(self).on_cancel()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -352,95 +363,6 @@ class JournalEntry(AccountsController):
|
||||
StockAccountInvalidTransaction,
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
self.update_asset_on_depreciation()
|
||||
self.update_asset_on_disposal()
|
||||
@@ -453,7 +375,7 @@ class JournalEntry(AccountsController):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_cached_doc("Asset", d.reference_name)
|
||||
@@ -635,12 +557,27 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
@@ -1719,6 +1656,9 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
|
||||
"party_type": party_type,
|
||||
"account_type": account_details.account_type,
|
||||
"account_currency": account_details.account_currency or company_currency,
|
||||
"bank_account": (
|
||||
frappe.db.get_value("Bank Account", {"account": account, "company": company}) or None
|
||||
),
|
||||
# The date used to retreive the exchange rate here is the date passed in
|
||||
# as an argument to this function. It is assumed to be the date on which the balance is sought
|
||||
"exchange_rate": get_exchange_rate(
|
||||
@@ -1751,8 +1691,6 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1774,8 +1712,8 @@ def get_exchange_rate(
|
||||
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -281,12 +282,19 @@
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -11,6 +11,5 @@ def get_data():
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
|
||||
{"items": ["Budget"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ class OpeningInvoiceCreationTool(Document):
|
||||
max_count = {}
|
||||
fields = [
|
||||
"company",
|
||||
"count(name) as total_invoices",
|
||||
"sum(outstanding_amount) as outstanding_amount",
|
||||
{"COUNT": "*", "as": "total_invoices"},
|
||||
{"SUM": "outstanding_amount", "as": "outstanding_amount"},
|
||||
]
|
||||
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
|
||||
if not companies:
|
||||
@@ -214,6 +214,9 @@ class OpeningInvoiceCreationTool(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if self.invoice_type == "Purchase" and row.supplier_invoice_date:
|
||||
invoice.update({"bill_date": row.supplier_invoice_date})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
for dimension in accounting_dimension:
|
||||
invoice.update({dimension: self.get(dimension) or item.get(dimension)})
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"due_date",
|
||||
"supplier_invoice_date",
|
||||
"section_break_5",
|
||||
"item_name",
|
||||
"outstanding_amount",
|
||||
@@ -111,19 +112,26 @@
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invoice Number"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Supplier Invoice Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:06.703006",
|
||||
"modified": "2025-12-01 16:18:07.997594",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
qty: DF.Data | None
|
||||
supplier_invoice_date: DF.Date | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -181,7 +182,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Dunning",
|
||||
];
|
||||
|
||||
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
||||
if (party_type_doctypes.includes(child.reference_doctype)) {
|
||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||
}
|
||||
|
||||
@@ -426,7 +427,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
"party_name",
|
||||
],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
@@ -532,6 +541,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
() => frm.set_value("party_name", r.message.party_name),
|
||||
() => frm.clear_table("references"),
|
||||
() => frm.clear_table("tax_withholding_entries"),
|
||||
() => frm.events.hide_unhide_fields(frm),
|
||||
() => frm.events.set_dynamic_labels(frm),
|
||||
() => {
|
||||
@@ -564,19 +574,22 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
apply_tax_withholding_amount: function (frm) {
|
||||
if (!frm.doc.apply_tax_withholding_amount) {
|
||||
apply_tds: function (frm) {
|
||||
if (!frm.doc.apply_tds) {
|
||||
frm.set_value("tax_withholding_category", "");
|
||||
} else {
|
||||
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
|
||||
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
||||
});
|
||||
}
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
paid_from: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_from,
|
||||
@@ -593,6 +606,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_to: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_to,
|
||||
@@ -1026,7 +1041,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
if (!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
|
||||
if (!frm.events.get_order_doctypes(frm).includes(d.voucher_type)) {
|
||||
if (flt(d.outstanding_amount) > 0)
|
||||
total_positive_outstanding += flt(d.outstanding_amount);
|
||||
else total_negative_outstanding += Math.abs(flt(d.outstanding_amount));
|
||||
@@ -1042,7 +1057,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
} else {
|
||||
c.exchange_rate = 1;
|
||||
}
|
||||
if (in_list(frm.events.get_invoice_doctypes(frm), d.reference_doctype)) {
|
||||
if (frm.events.get_invoice_doctypes(frm).includes(d.reference_doctype)) {
|
||||
c.due_date = d.due_date;
|
||||
}
|
||||
});
|
||||
@@ -1273,15 +1288,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
company_defaults?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.cost_center = company_defaults?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
@@ -1325,6 +1339,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
if (frm.set_company_bank_account_based_on_coa) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
@@ -1363,6 +1379,34 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
set_company_bank_account: function (frm) {
|
||||
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
|
||||
if (!frm.doc.company || !frm.doc[field]) return;
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = true;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Bank Account",
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
account: frm.doc[field],
|
||||
disabled: 0,
|
||||
},
|
||||
fieldname: ["name"],
|
||||
},
|
||||
callback: async function (r) {
|
||||
if (r.message) await frm.set_value("bank_account", r.message.name);
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
sales_taxes_and_charges_template: function (frm) {
|
||||
frm.trigger("fetch_taxes_from_template");
|
||||
},
|
||||
@@ -1421,7 +1465,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
$.each(frm.doc["taxes"] || [], function (i, tax) {
|
||||
frm.events.validate_taxes_and_charges(tax);
|
||||
frm.events.validate_inclusive_tax(tax);
|
||||
tax.item_wise_tax_detail = {};
|
||||
let tax_fields = [
|
||||
"total",
|
||||
"tax_fraction_for_current_item",
|
||||
@@ -1462,18 +1505,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
||||
);
|
||||
d.row_id = "";
|
||||
} else if (
|
||||
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
|
||||
d.row_id
|
||||
) {
|
||||
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
|
||||
if (d.idx == 1) {
|
||||
msg = __(
|
||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||
);
|
||||
d.charge_type = "";
|
||||
} else if (!d.row_id) {
|
||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
||||
d.row_id = "";
|
||||
d.row_id = d.idx - 1;
|
||||
} else if (d.row_id && d.row_id >= d.idx) {
|
||||
msg = __(
|
||||
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -60,10 +62,6 @@
|
||||
"taxes_and_charges_section",
|
||||
"purchase_taxes_and_charges_template",
|
||||
"sales_taxes_and_charges_template",
|
||||
"column_break_55",
|
||||
"apply_tax_withholding_amount",
|
||||
"tax_withholding_category",
|
||||
"section_break_56",
|
||||
"taxes",
|
||||
"section_break_60",
|
||||
"base_total_taxes_and_charges",
|
||||
@@ -71,6 +69,11 @@
|
||||
"total_taxes_and_charges",
|
||||
"deductions_or_loss_section",
|
||||
"deductions",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"transaction_references",
|
||||
"reference_no",
|
||||
"column_break_23",
|
||||
@@ -578,24 +581,17 @@
|
||||
"label": "Custom Remarks"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"depends_on": "eval:doc.apply_tds",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"mandatory_depends_on": "eval:doc.apply_tds",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "apply_tax_withholding_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "taxes_and_charges_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Taxes and Charges"
|
||||
},
|
||||
{
|
||||
@@ -648,15 +644,6 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_55",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_56",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
@@ -695,8 +682,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
@@ -753,6 +739,46 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -767,7 +793,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-08 11:18:10.238085",
|
||||
"modified": "2025-12-18 13:56:40.206038",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -30,9 +30,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PaymentTaxWithholding
|
||||
from erpnext.accounts.general_ledger import (
|
||||
make_gl_entries,
|
||||
make_reverse_gl_entries,
|
||||
@@ -80,9 +78,10 @@ class PaymentEntry(AccountsController):
|
||||
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
|
||||
PaymentEntryReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
amended_from: DF.Link | None
|
||||
apply_tax_withholding_amount: DF.Check
|
||||
apply_tds: DF.Check
|
||||
auto_repeat: DF.Link | None
|
||||
bank: DF.ReadOnly | None
|
||||
bank_account: DF.Link | None
|
||||
@@ -103,11 +102,13 @@ class PaymentEntry(AccountsController):
|
||||
custom_remarks: DF.Check
|
||||
deductions: DF.Table[PaymentEntryDeduction]
|
||||
difference_amount: DF.Currency
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
in_words: DF.SmallText | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
letter_head: DF.Link | None
|
||||
mode_of_payment: DF.Link | None
|
||||
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
|
||||
override_tax_withholding_entries: DF.Check
|
||||
paid_amount: DF.Currency
|
||||
paid_amount_after_tax: DF.Currency
|
||||
paid_from: DF.Link
|
||||
@@ -139,6 +140,8 @@ class PaymentEntry(AccountsController):
|
||||
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
||||
target_exchange_rate: DF.Float
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
taxes: DF.Table[AdvanceTaxesandCharges]
|
||||
title: DF.Data | None
|
||||
total_allocated_amount: DF.Currency
|
||||
@@ -189,7 +192,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_tax_withholding()
|
||||
PaymentTaxWithholding(self).on_validate()
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
@@ -199,6 +202,7 @@ class PaymentEntry(AccountsController):
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
PaymentTaxWithholding(self).on_submit()
|
||||
self.update_payment_requests()
|
||||
self.update_payment_schedule()
|
||||
self.make_gl_entries()
|
||||
@@ -300,8 +304,10 @@ class PaymentEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
PaymentTaxWithholding(self).on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -937,93 +943,6 @@ class PaymentEntry(AccountsController):
|
||||
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||
self.in_words = money_in_words(amount, currency)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.party_type != "Supplier":
|
||||
return
|
||||
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"doctype": "Payment Entry",
|
||||
"supplier": self.party,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
tax_withholding_details.update(
|
||||
{"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)}
|
||||
)
|
||||
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
# Preserve user updated included in paid amount
|
||||
if d.included_in_paid_amount:
|
||||
tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
|
||||
|
||||
d.update(tax_withholding_details)
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.taxes
|
||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -1366,8 +1285,11 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
|
||||
merge_entries = frappe.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
|
||||
gl_entries = process_gl_map(gl_entries, merge_entries=merge_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj, merge_entries=merge_entries)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
else:
|
||||
@@ -1437,6 +1359,7 @@ class PaymentEntry(AccountsController):
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
"transaction_exchange_rate": self.target_exchange_rate,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
@@ -1873,7 +1796,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
|
||||
@@ -1045,6 +1045,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
)
|
||||
|
||||
def test_gl_of_multi_currency_payment_with_taxes(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
)
|
||||
@@ -1606,6 +1607,96 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
|
||||
def test_payment_entry_merges_gl_entries_with_same_account_head(self):
|
||||
"""
|
||||
Test that Payment Entry merges GL entries with same account head
|
||||
when 'Merge Similar Account Heads' setting is enabled.
|
||||
"""
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
|
||||
pe = create_payment_entry(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
paid_from="_Test Bank - _TC",
|
||||
paid_to="Creditors - _TC",
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 50,
|
||||
},
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 30,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0},
|
||||
fields=["debit", "credit"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(gl_entries), 1)
|
||||
self.assertEqual(gl_entries[0].debit, 80)
|
||||
|
||||
def test_payment_entry_does_not_merge_gl_entries_when_setting_disabled(self):
|
||||
"""
|
||||
Test that Payment Entry does NOT merge GL entries
|
||||
when 'Merge Similar Account Heads' is disabled.
|
||||
"""
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
pe = create_payment_entry(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
paid_from="_Test Bank - _TC",
|
||||
paid_to="Creditors - _TC",
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 50,
|
||||
},
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 30,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0},
|
||||
fields=["debit", "credit"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(gl_entries), 2)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
|
||||
def check_pl_entries(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
pl_entries = (
|
||||
|
||||
@@ -59,14 +59,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified": "2025-08-13 06:52:46.130142",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"columns": 4,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_global_search": 1,
|
||||
@@ -49,8 +49,10 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -68,7 +70,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"print_hide": 1,
|
||||
@@ -77,7 +79,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Outstanding",
|
||||
"read_only": 1
|
||||
@@ -85,7 +87,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated"
|
||||
},
|
||||
@@ -174,7 +176,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified": "2026-01-05 14:18:03.286224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -18,12 +18,12 @@ class PaymentEntryReference(Document):
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Float
|
||||
allocated_amount: DF.Currency
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
outstanding_amount: DF.Float
|
||||
outstanding_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -34,7 +34,7 @@ class PaymentEntryReference(Document):
|
||||
reconcile_effect_on: DF.Date | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
total_amount: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
@property
|
||||
|
||||
@@ -131,7 +131,6 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -144,7 +143,6 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -159,7 +157,7 @@ class PaymentLedgerEntry(Document):
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
if not self.delinked:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
|
||||
@@ -50,12 +50,10 @@ class TestPaymentOrder(IntegrationTestCase):
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
|
||||
payment_order = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
doc = make_payment_order(ref_doc.name, payment_order)
|
||||
doc.save()
|
||||
|
||||
@@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "payments", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "allocation", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
@@ -318,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
options: "<b> New Journal Entry will be posted for the difference amount </b>",
|
||||
options: __(
|
||||
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
|
||||
).bold(),
|
||||
},
|
||||
],
|
||||
primary_action: () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user