mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-11 01:12:26 +00:00
Compare commits
609 Commits
v14.78.7
...
version-14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc800e39c | ||
|
|
83fd655042 | ||
|
|
34e94d6e7a | ||
|
|
32d46b3e88 | ||
|
|
6eb2868a15 | ||
|
|
e1ff203f95 | ||
|
|
ab188c4404 | ||
|
|
c5ed69c2c2 | ||
|
|
21311dab86 | ||
|
|
9dc88a1283 | ||
|
|
a6eadb18c9 | ||
|
|
f6d3a811ed | ||
|
|
e1674d2017 | ||
|
|
65d7c6b882 | ||
|
|
6f1616bc95 | ||
|
|
07ac93d06a | ||
|
|
824beaa893 | ||
|
|
b4dc1be5ed | ||
|
|
cf49c32b94 | ||
|
|
d54f45469a | ||
|
|
ad5181c52f | ||
|
|
6e03d45034 | ||
|
|
7a18f86dd5 | ||
|
|
39e1bddce6 | ||
|
|
d09de53d1d | ||
|
|
90d88b1c26 | ||
|
|
22eec09175 | ||
|
|
dbfd4ea4d3 | ||
|
|
8d28dcb18b | ||
|
|
3f186b24f3 | ||
|
|
4b0ddbf73a | ||
|
|
cddf1e1ee5 | ||
|
|
22e5aba02b | ||
|
|
21d13859a0 | ||
|
|
6267b9aea5 | ||
|
|
6499a1dae0 | ||
|
|
478992812f | ||
|
|
634dd11d72 | ||
|
|
0e6586734a | ||
|
|
5e129374b9 | ||
|
|
55d2a54785 | ||
|
|
3a6d056fd3 | ||
|
|
d713f39fce | ||
|
|
1dee10077c | ||
|
|
c86cf5b9c6 | ||
|
|
5c9e0bba2f | ||
|
|
793688710a | ||
|
|
98414385e5 | ||
|
|
33f7b742ec | ||
|
|
e9c1d0ad52 | ||
|
|
67c0d08569 | ||
|
|
6f6cbb717e | ||
|
|
387657fa92 | ||
|
|
ef03d90a08 | ||
|
|
a15b010868 | ||
|
|
9d00afe1b1 | ||
|
|
6ea7393f7d | ||
|
|
8de0d60581 | ||
|
|
a67092894f | ||
|
|
77b9044d8d | ||
|
|
ba9f571886 | ||
|
|
a299392128 | ||
|
|
bc58fd1fa4 | ||
|
|
10f1c9c3ac | ||
|
|
a3b120b3b2 | ||
|
|
5e05873226 | ||
|
|
db828c08b6 | ||
|
|
e6f505ef81 | ||
|
|
1ef9471c79 | ||
|
|
304aad6745 | ||
|
|
c80fed8b2f | ||
|
|
5979683e8e | ||
|
|
367bd58e76 | ||
|
|
8dc80c948e | ||
|
|
b54c5f61d6 | ||
|
|
c51a9025b3 | ||
|
|
00a75d4984 | ||
|
|
8b826c2369 | ||
|
|
210786119d | ||
|
|
85c509ea69 | ||
|
|
6e35b6b6b3 | ||
|
|
94605687c7 | ||
|
|
002575244f | ||
|
|
aadc83d0d9 | ||
|
|
d82106cb76 | ||
|
|
03aeac6c9b | ||
|
|
014f5bff5c | ||
|
|
819667ab24 | ||
|
|
aa20e224fb | ||
|
|
dcae024fd2 | ||
|
|
30a6fe55ca | ||
|
|
57f9894f41 | ||
|
|
2c383a69f9 | ||
|
|
b7d76d8fad | ||
|
|
9b5183e0e5 | ||
|
|
0b43d518af | ||
|
|
14e92d589f | ||
|
|
3d51c1b363 | ||
|
|
3c408ff66b | ||
|
|
f6bc9fc505 | ||
|
|
0c7911d0fe | ||
|
|
d062e50f35 | ||
|
|
646de0bec1 | ||
|
|
02de47c673 | ||
|
|
42b38b7998 | ||
|
|
180f406917 | ||
|
|
0b3215cd42 | ||
|
|
b9f9be3d88 | ||
|
|
faeedb01a8 | ||
|
|
a2402ddf52 | ||
|
|
6b29c06511 | ||
|
|
507d27d509 | ||
|
|
e27dd64044 | ||
|
|
7f617a54d1 | ||
|
|
7986e69839 | ||
|
|
836545bdb4 | ||
|
|
4fccef0636 | ||
|
|
2b6b0b32a7 | ||
|
|
43a723546d | ||
|
|
ea454ef770 | ||
|
|
0d6be69ce5 | ||
|
|
22d7bbb081 | ||
|
|
4b643feb92 | ||
|
|
06f04cad3e | ||
|
|
c3ebc39f3b | ||
|
|
9d06e093a6 | ||
|
|
1c24bedaf6 | ||
|
|
5a0e690307 | ||
|
|
64e6d65c00 | ||
|
|
2f4bfa77b2 | ||
|
|
5eeaaf4757 | ||
|
|
8faabb8110 | ||
|
|
92ee871b79 | ||
|
|
25ff9e0a4d | ||
|
|
a230a0d94c | ||
|
|
8fc8aa2dfd | ||
|
|
e0d9a47ff7 | ||
|
|
9d0034f0a4 | ||
|
|
f3d780c5bc | ||
|
|
89d00ee4a2 | ||
|
|
22e63099ee | ||
|
|
06c3839abc | ||
|
|
b3035ec7d4 | ||
|
|
0ecc056a3f | ||
|
|
7a9150aac0 | ||
|
|
388b5fb8f6 | ||
|
|
96a1a4a8b8 | ||
|
|
eb691226d5 | ||
|
|
00e61a531a | ||
|
|
7abc4c4022 | ||
|
|
1ae3fd3e27 | ||
|
|
ade44fbdb4 | ||
|
|
82e8fb8a5e | ||
|
|
f415bc3d14 | ||
|
|
5bc3273460 | ||
|
|
c263e6d67e | ||
|
|
11b5a81170 | ||
|
|
27485b9472 | ||
|
|
0de4477ddf | ||
|
|
235f271729 | ||
|
|
d38fd0779c | ||
|
|
093c6bb17e | ||
|
|
2c98081be6 | ||
|
|
451326fdeb | ||
|
|
4d12ae069e | ||
|
|
4297c58e5e | ||
|
|
b43fbf2a7d | ||
|
|
6cdc7811b8 | ||
|
|
59ac7aeb23 | ||
|
|
4524c74968 | ||
|
|
80ae9820f0 | ||
|
|
5d92f38bbe | ||
|
|
a944eaf054 | ||
|
|
493c8ce04e | ||
|
|
77ea06dc7d | ||
|
|
360f3b7c25 | ||
|
|
581221f1ec | ||
|
|
e0be83caa5 | ||
|
|
8934adfef4 | ||
|
|
2d6f4740be | ||
|
|
7aaea9f5b6 | ||
|
|
9e7a5bb1bb | ||
|
|
27105501fb | ||
|
|
4fe09fd7e8 | ||
|
|
76a233e1b7 | ||
|
|
5ddfe3ce1d | ||
|
|
ff998572e7 | ||
|
|
f364a41490 | ||
|
|
804edad233 | ||
|
|
e533158a1c | ||
|
|
5bcf642dff | ||
|
|
d7822f493b | ||
|
|
5188745c53 | ||
|
|
006a8c71fd | ||
|
|
ba25d334b1 | ||
|
|
dd1020ccfb | ||
|
|
de2f80811f | ||
|
|
12eaa64572 | ||
|
|
da1dcb8b21 | ||
|
|
9dad08274a | ||
|
|
d8d53bc43e | ||
|
|
8966c956d5 | ||
|
|
a518fc080a | ||
|
|
3b36be214f | ||
|
|
c332fcdeff | ||
|
|
51adea3b89 | ||
|
|
083402e9a8 | ||
|
|
1116a0fc8e | ||
|
|
b15d7e573e | ||
|
|
9207d79ce6 | ||
|
|
33eb72035b | ||
|
|
2c6099e051 | ||
|
|
5f4aaed5a0 | ||
|
|
94a17b6ec2 | ||
|
|
bb901cab88 | ||
|
|
711e06b537 | ||
|
|
c54b65acc2 | ||
|
|
9fcca18bf3 | ||
|
|
b98cce8b9a | ||
|
|
bd9655a781 | ||
|
|
1df3b608ec | ||
|
|
62b7a8d21a | ||
|
|
6b31e54891 | ||
|
|
15ae0196c6 | ||
|
|
945bdabebb | ||
|
|
d7c47e8ea5 | ||
|
|
2627fb10ce | ||
|
|
0790db79fd | ||
|
|
8208a9ec27 | ||
|
|
fbfc6a6f62 | ||
|
|
d60ab92082 | ||
|
|
85a0581145 | ||
|
|
2cac05e56c | ||
|
|
10e4e610db | ||
|
|
9ab80cfd6c | ||
|
|
8f2b83d434 | ||
|
|
7f0ebb37da | ||
|
|
1d42c4a305 | ||
|
|
186173a21e | ||
|
|
96c937bf6a | ||
|
|
52aafc9410 | ||
|
|
3232310c0f | ||
|
|
6397c366fd | ||
|
|
f5ef376486 | ||
|
|
c9eeca22f5 | ||
|
|
df779bb7dd | ||
|
|
20f701ff0b | ||
|
|
ca4858318e | ||
|
|
b664781fae | ||
|
|
69463b4798 | ||
|
|
3b686827e9 | ||
|
|
d37b12f385 | ||
|
|
43aeff38aa | ||
|
|
ad13dcd695 | ||
|
|
792f3afa1b | ||
|
|
43dda4a5d2 | ||
|
|
274ce10329 | ||
|
|
caf145e3ca | ||
|
|
05911ad563 | ||
|
|
de937a6bad | ||
|
|
70e6ea6b3f | ||
|
|
70bcfb4748 | ||
|
|
1f1cb338fe | ||
|
|
d5d4b3a7f3 | ||
|
|
5d9be7366b | ||
|
|
537c917bfc | ||
|
|
13f393c59a | ||
|
|
54197ff760 | ||
|
|
6f1109226d | ||
|
|
9ce86b135b | ||
|
|
d88feecf46 | ||
|
|
00407cd0ee | ||
|
|
a04feff264 | ||
|
|
d6f5ac90b1 | ||
|
|
c6c1b00559 | ||
|
|
b86dc294f1 | ||
|
|
949ed59f84 | ||
|
|
558225b027 | ||
|
|
2389fd5145 | ||
|
|
d66a34411c | ||
|
|
1bbbd261cb | ||
|
|
3993525bf6 | ||
|
|
8343c92ac5 | ||
|
|
f278120aa0 | ||
|
|
862e6330ac | ||
|
|
bd395e2406 | ||
|
|
4376fbc3ed | ||
|
|
c49ee563fd | ||
|
|
8e0bd976c3 | ||
|
|
d66e8e8597 | ||
|
|
d41fec7d2b | ||
|
|
378821d9e6 | ||
|
|
698e260526 | ||
|
|
fc793f0f25 | ||
|
|
204d1d6a53 | ||
|
|
aee917b790 | ||
|
|
892d3980d3 | ||
|
|
e743d5f66b | ||
|
|
9adb863787 | ||
|
|
150cc5a664 | ||
|
|
b507e63375 | ||
|
|
27a8856dca | ||
|
|
b13e0a6b9f | ||
|
|
0c5c34409e | ||
|
|
9dbc1cc5fb | ||
|
|
7130e5ccfb | ||
|
|
0e42516977 | ||
|
|
48748e7142 | ||
|
|
0bc3bc4851 | ||
|
|
6868da5f6a | ||
|
|
f906568fea | ||
|
|
17dc87a2df | ||
|
|
522597d22b | ||
|
|
e4c2442ba2 | ||
|
|
0d9bc4424c | ||
|
|
35c9375f0d | ||
|
|
f04c73b9c2 | ||
|
|
a766aa0837 | ||
|
|
503b89a764 | ||
|
|
b88f6c1252 | ||
|
|
e5156e6666 | ||
|
|
8801584c8d | ||
|
|
55ac96ff09 | ||
|
|
3f3fb323cf | ||
|
|
253a067592 | ||
|
|
4ac5c7e5e7 | ||
|
|
694f158fc8 | ||
|
|
5844aafd12 | ||
|
|
98ffbfb432 | ||
|
|
6ec33c0098 | ||
|
|
a2595350f6 | ||
|
|
ac7700dff0 | ||
|
|
7a74dac2c2 | ||
|
|
a3d4d34454 | ||
|
|
443ed5b2ce | ||
|
|
033fa09eb4 | ||
|
|
056cc35379 | ||
|
|
f1a864349e | ||
|
|
18282b2632 | ||
|
|
46115d6f71 | ||
|
|
f62905f7a7 | ||
|
|
88facb7523 | ||
|
|
512877ab46 | ||
|
|
c7e6b2356f | ||
|
|
8e2bfc6bcb | ||
|
|
98260b990c | ||
|
|
3be0f00b3b | ||
|
|
f44478db78 | ||
|
|
db8cb70bd8 | ||
|
|
f26b22ee7f | ||
|
|
1e4807f16d | ||
|
|
698d5be840 | ||
|
|
59e46e22d3 | ||
|
|
14b4d147a8 | ||
|
|
2bed1c8336 | ||
|
|
9e3a04136c | ||
|
|
dcc53cea55 | ||
|
|
430d1e8b2e | ||
|
|
36a366d962 | ||
|
|
dda35b8e51 | ||
|
|
4e0d7d88ec | ||
|
|
36b951d018 | ||
|
|
233e0d8049 | ||
|
|
5f467be0c8 | ||
|
|
70d117e858 | ||
|
|
2fe9fa7ef7 | ||
|
|
b4ca51ad4c | ||
|
|
fb5777cf10 | ||
|
|
06c32993ea | ||
|
|
466c6ee3d4 | ||
|
|
a8831351e3 | ||
|
|
d1f9444be7 | ||
|
|
9cadb89678 | ||
|
|
da6affbba7 | ||
|
|
6293b08540 | ||
|
|
576ce7e882 | ||
|
|
15bb1fdb24 | ||
|
|
509c5c4d17 | ||
|
|
cb028b8740 | ||
|
|
c3447c030a | ||
|
|
1d65f995e7 | ||
|
|
3a6d8cd9a1 | ||
|
|
88e664b79f | ||
|
|
090ee7f042 | ||
|
|
b311b6eb7f | ||
|
|
df64d2ef4e | ||
|
|
690a939572 | ||
|
|
a3639b055c | ||
|
|
331ecc1964 | ||
|
|
736b125e14 | ||
|
|
b9b08c35ef | ||
|
|
300f1a860b | ||
|
|
1441ef2532 | ||
|
|
64fd5c0ba9 | ||
|
|
16fe53b8c7 | ||
|
|
af3089b96e | ||
|
|
3cb29fc035 | ||
|
|
8a93057844 | ||
|
|
805549f793 | ||
|
|
6e497f73f1 | ||
|
|
33f090a8c8 | ||
|
|
20b43b4d93 | ||
|
|
5c1f6f0107 | ||
|
|
6f0c67a242 | ||
|
|
f311a0fc1c | ||
|
|
a1585b3c53 | ||
|
|
05c0bf5d99 | ||
|
|
247a006b5a | ||
|
|
e0cf6393ee | ||
|
|
1f54c272ac | ||
|
|
9a1321ab02 | ||
|
|
6ca1f9bc73 | ||
|
|
b3b7e62a90 | ||
|
|
35a728d22b | ||
|
|
ac25d3e1c4 | ||
|
|
90b5f0b7bf | ||
|
|
557a05b0ad | ||
|
|
b41ee667b9 | ||
|
|
fbf6d8c9e2 | ||
|
|
167069b823 | ||
|
|
2d72a37d0c | ||
|
|
77ad224cf6 | ||
|
|
b5b55fad6c | ||
|
|
7cde990d69 | ||
|
|
cedf577b4c | ||
|
|
8e02dcfcaa | ||
|
|
399ed331e3 | ||
|
|
59236bc5bf | ||
|
|
1089cdf213 | ||
|
|
df4f4d9a31 | ||
|
|
f8bbcab3a5 | ||
|
|
369a692af9 | ||
|
|
8cc92e9ca0 | ||
|
|
eebf6cf877 | ||
|
|
ca94ad3a24 | ||
|
|
1a8bf0cf3d | ||
|
|
811953b9b0 | ||
|
|
6748780591 | ||
|
|
69a8e0dfac | ||
|
|
3e21e343d3 | ||
|
|
c734373c9f | ||
|
|
703fd816d1 | ||
|
|
101c71c508 | ||
|
|
a0cd08e9ea | ||
|
|
57a0717778 | ||
|
|
0cf9c94a37 | ||
|
|
e48a03f130 | ||
|
|
e7d97865e5 | ||
|
|
4b16272a01 | ||
|
|
1ff0edd492 | ||
|
|
ef37388993 | ||
|
|
116798df96 | ||
|
|
e515b91988 | ||
|
|
2edf083c35 | ||
|
|
86ddabeae6 | ||
|
|
fc6f568a6c | ||
|
|
cd3a411401 | ||
|
|
6f6133f2e2 | ||
|
|
5a8b81409e | ||
|
|
44a16bb544 | ||
|
|
206d0f1856 | ||
|
|
84432fc035 | ||
|
|
a344b8b9ae | ||
|
|
c65f421da9 | ||
|
|
5c6028340f | ||
|
|
aa0ada9670 | ||
|
|
534b25c448 | ||
|
|
e53a78c2bd | ||
|
|
2ed3bdcc2e | ||
|
|
c61e4e2ddf | ||
|
|
08ba77538b | ||
|
|
de14bf1010 | ||
|
|
46eba50c8c | ||
|
|
c4358c049a | ||
|
|
a43f1badd5 | ||
|
|
9f79da0015 | ||
|
|
ca9df9db07 | ||
|
|
49074aa2fa | ||
|
|
6b9dad7768 | ||
|
|
9e36cac0d1 | ||
|
|
256318bb1c | ||
|
|
91caca05bb | ||
|
|
ceb5997256 | ||
|
|
573ce645b2 | ||
|
|
09cefd9d63 | ||
|
|
222bd9351d | ||
|
|
9985a03f39 | ||
|
|
b217a7ee3e | ||
|
|
49787b6d84 | ||
|
|
95903c9f96 | ||
|
|
d0ff91b0e0 | ||
|
|
82cfafb610 | ||
|
|
d14acb4f58 | ||
|
|
202693d4c3 | ||
|
|
3c6ed0a565 | ||
|
|
2f33f6bdf5 | ||
|
|
9510758ce4 | ||
|
|
f01765db6f | ||
|
|
3f6beebeec | ||
|
|
8a9d554c32 | ||
|
|
7823f1b06f | ||
|
|
4d5becbd7c | ||
|
|
43a5c33dbf | ||
|
|
50c26ba017 | ||
|
|
7bfe0526a1 | ||
|
|
235b38a3af | ||
|
|
c6ed82a304 | ||
|
|
e9d934d378 | ||
|
|
0eddd1e2d7 | ||
|
|
cc275318e3 | ||
|
|
620cdc2489 | ||
|
|
ef6e264887 | ||
|
|
5a62bd6e85 | ||
|
|
035139d4c7 | ||
|
|
b5637c43fa | ||
|
|
dac53074f2 | ||
|
|
b6a6bced61 | ||
|
|
7b3c35c167 | ||
|
|
83bce785ff | ||
|
|
0086656748 | ||
|
|
36b1c436ea | ||
|
|
9f4b3e86b3 | ||
|
|
9f5d7e41ec | ||
|
|
50aa4ed55a | ||
|
|
dee6e2b697 | ||
|
|
9217e919c3 | ||
|
|
bf3d68e76d | ||
|
|
39e82dfbc1 | ||
|
|
a0155279e0 | ||
|
|
e682d2c9ae | ||
|
|
1160df9350 | ||
|
|
80ed2fb1fb | ||
|
|
9eda931b97 | ||
|
|
30f001edea | ||
|
|
8befe7f244 | ||
|
|
de531a81b6 | ||
|
|
ecbeaaf533 | ||
|
|
befc16cc97 | ||
|
|
b8e4d80b4e | ||
|
|
9767dc61a6 | ||
|
|
03068ab96c | ||
|
|
c484563bea | ||
|
|
914f4bffea | ||
|
|
2fb1aaa5c3 | ||
|
|
cca5fbd81a | ||
|
|
4d2352af00 | ||
|
|
24dc1bf1a3 | ||
|
|
c6bc928f50 | ||
|
|
9518063a81 | ||
|
|
452b205021 | ||
|
|
35f801feda | ||
|
|
69464ab7ff | ||
|
|
b6b453ca5d | ||
|
|
1622fc8728 | ||
|
|
ecdff8f320 | ||
|
|
0c7219159a | ||
|
|
431fa225e3 | ||
|
|
f27e35c8f4 | ||
|
|
5fbffcbd7b | ||
|
|
bb949da334 | ||
|
|
8764a321c7 | ||
|
|
49e3865265 | ||
|
|
33a1da8194 | ||
|
|
52309fe0b6 | ||
|
|
0fdd6817a6 | ||
|
|
17535095e2 | ||
|
|
4d74597f94 | ||
|
|
f9420db3ca | ||
|
|
8996685f44 | ||
|
|
7046a01921 | ||
|
|
0b8cf3a369 | ||
|
|
fe5de30256 | ||
|
|
20bb15167d | ||
|
|
1ccf30d97b | ||
|
|
524a8d77f7 | ||
|
|
667e659e3f | ||
|
|
77e92b38eb | ||
|
|
a66d475b56 | ||
|
|
6a52f79cce | ||
|
|
49ffeccafa | ||
|
|
6bc210d9f4 | ||
|
|
f4b7fa8980 | ||
|
|
c331a4fa84 | ||
|
|
d42173beb5 | ||
|
|
b2d35fae10 | ||
|
|
8d13ef050e | ||
|
|
8ac40f07e3 | ||
|
|
46894a5b86 | ||
|
|
821cfe2c39 | ||
|
|
b8b76a5b58 | ||
|
|
01d2794968 | ||
|
|
02cfb589a2 | ||
|
|
caf5faceda | ||
|
|
07653c54f3 | ||
|
|
affa67e74d | ||
|
|
2d63fc98d0 | ||
|
|
8bb4415f65 | ||
|
|
90b2ec9aba | ||
|
|
24ae74ebb3 | ||
|
|
7d1d0c8e0c | ||
|
|
b3d545f91a | ||
|
|
7b0e19499b | ||
|
|
34504a23ec | ||
|
|
b603adce5e | ||
|
|
9efc1de40e | ||
|
|
ff622ef552 | ||
|
|
49778432ea | ||
|
|
b7509e326e | ||
|
|
eb5505187e |
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-release-notes
|
||||
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Auto-label PRs based on title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-label-if-prefix-matches:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title and add label if it matches prefixes
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title.toLowerCase();
|
||||
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||
|
||||
// Check if the PR title starts with any of the prefixes
|
||||
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['skip-release-notes']
|
||||
});
|
||||
}
|
||||
7
.github/workflows/linters.yml
vendored
7
.github/workflows/linters.yml
vendored
@@ -9,15 +9,16 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
6
.github/workflows/patch.yml
vendored
6
.github/workflows/patch.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
6
.github/workflows/server-tests-mariadb.yml
vendored
6
.github/workflows/server-tests-mariadb.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
6
.github/workflows/server-tests-postgres.yml
vendored
6
.github/workflows/server-tests-postgres.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.db.get_value("Account", name, "company", as_dict=True)
|
||||
if not account:
|
||||
return
|
||||
@@ -420,7 +421,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
if old_name and not from_descendant:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
@@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -514,3 +516,27 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Office Maintenance Expenses": {},
|
||||
"Office Rent": {},
|
||||
"Postal Expenses": {},
|
||||
"Print and Stationary": {},
|
||||
"Print and Stationery": {},
|
||||
"Rounded Off": {
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only_depends_on": "eval:!doc.__islocal",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
||||
@@ -25,6 +25,7 @@ class AccountingDimension(Document):
|
||||
"Accounting Dimension Detail",
|
||||
"Company",
|
||||
"Account",
|
||||
"Finance Book",
|
||||
):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -3,4 +3,14 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"stale_days",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -73,7 +74,13 @@
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -383,7 +390,7 @@
|
||||
{
|
||||
"fieldname": "section_break_jpd0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Reconciliations"
|
||||
"label": "Payment Reconciliation Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -462,6 +469,49 @@
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "Payment",
|
||||
"description": "Only applies for Normal Payments",
|
||||
"fieldname": "exchange_gain_loss_posting_date",
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Ignores legacy Is Opening field in GL Entry that allows adding opening balance post the system is in use while generating reports",
|
||||
"fieldname": "ignore_is_opening_check_for_reporting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Is Opening check for reporting"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -469,7 +519,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-22 12:10:10.151819",
|
||||
"modified": "2025-05-05 12:29:38.302027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -65,3 +65,11 @@ class AccountsSettings(Document):
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
@frappe.whitelist()
|
||||
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}")
|
||||
|
||||
@@ -46,9 +46,6 @@ class BankClearance(Document):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -70,7 +67,6 @@ class BankClearance(Document):
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
"to": self.to_date,
|
||||
"bank_account": self.bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -93,7 +89,7 @@ class BankClearance(Document):
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_disbursement.disbursement_account == self.account)
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
)
|
||||
@@ -121,7 +117,7 @@ class BankClearance(Document):
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
.where(loan_repayment.payment_account == self.account)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -19,10 +19,15 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.company) {
|
||||
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||
}
|
||||
|
||||
// Set default filter dates
|
||||
let today = frappe.datetime.get_today();
|
||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||
frm.doc.bank_statement_to_date = today;
|
||||
|
||||
frm.trigger("bank_account");
|
||||
},
|
||||
|
||||
@@ -94,7 +99,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
|
||||
make_reconciliation_tool(frm) {
|
||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
frm.trigger("get_cleared_balance").then(() => {
|
||||
if (
|
||||
frm.doc.bank_account &&
|
||||
@@ -110,7 +115,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_account_opening_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
@@ -125,7 +130,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_cleared_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
|
||||
@@ -45,42 +45,41 @@ class AutoMatchbyAccountIBAN:
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
return None
|
||||
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
return self.match_account_in_party()
|
||||
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
"""
|
||||
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
|
||||
1. Get party from a matching (iban/account no) Bank Account
|
||||
2. If not found, get party from Employee with matching bank account details (iban/account no)
|
||||
"""
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
# Nothing to match
|
||||
return None
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
# Search for a matching Bank Account that has party set
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account",
|
||||
or_filters=self.get_or_filters(),
|
||||
filters={"party_type": ("is", "set"), "party": ("is", "set")},
|
||||
fields=["party", "party_type"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
if result := party_result[0] if party_result else None:
|
||||
return (result["party_type"], result["party"])
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
# If no party is found, search in Employee (since it has bank account details)
|
||||
if employee_result := frappe.db.get_all(
|
||||
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
|
||||
):
|
||||
return ("Employee", employee_result[0])
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
def get_or_filters(self, party: str | None = None) -> dict:
|
||||
"""Return OR filters for Bank Account and IBAN"""
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
|
||||
or_filters[bank_ac_field] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
@@ -100,8 +99,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
return self.match_party_name_desc_in_party()
|
||||
|
||||
def match_party_name_desc_in_party(self) -> tuple | None:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
@@ -110,7 +108,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
field = party.lower() + "_name"
|
||||
field = f"{party.lower()}_name"
|
||||
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
@@ -137,13 +135,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
return None, skip
|
||||
|
||||
return (
|
||||
party,
|
||||
party_name,
|
||||
), skip
|
||||
return ((party, party_name), skip) if party_name else (None, skip)
|
||||
|
||||
def process_fuzzy_result(self, result: list | None):
|
||||
"""
|
||||
@@ -161,8 +153,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
second_result = result[1]
|
||||
# If multiple matches with the same score, return None but discontinue matching
|
||||
# Matches were found but were too close to distinguish between
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
@@ -174,8 +166,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
|
||||
def get_parties_in_order(deposit: float) -> list:
|
||||
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
|
||||
if flt(deposit) > 0:
|
||||
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
|
||||
|
||||
return parties
|
||||
return (
|
||||
["Customer", "Supplier", "Employee"] # most -> least likely to pay us
|
||||
if flt(deposit) > 0
|
||||
else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us
|
||||
)
|
||||
|
||||
@@ -460,13 +460,20 @@ def get_actual_expense(args):
|
||||
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
|
||||
distribution = {}
|
||||
if monthly_distribution:
|
||||
for d in frappe.db.sql(
|
||||
"""select mdp.month, mdp.percentage_allocation
|
||||
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
|
||||
where mdp.parent=md.name and md.fiscal_year=%s""",
|
||||
fiscal_year,
|
||||
as_dict=1,
|
||||
):
|
||||
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
|
||||
md = frappe.qb.DocType("Monthly Distribution")
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
for d in res:
|
||||
distribution.setdefault(d.month, d.percentage_allocation)
|
||||
|
||||
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
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
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -448,9 +448,8 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
@@ -190,6 +192,31 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-18 08:32:26.895076",
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -141,8 +141,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,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", [])
|
||||
|
||||
@@ -80,11 +80,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:
|
||||
|
||||
@@ -55,46 +55,46 @@ class Dunning(AccountsController):
|
||||
"conversion_rate",
|
||||
"cost_center",
|
||||
]
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
invoice_fields.extend(accounting_dimensions)
|
||||
|
||||
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||
|
||||
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
},
|
||||
inv.party_account_currency,
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"project": inv.project,
|
||||
},
|
||||
item=inv,
|
||||
)
|
||||
)
|
||||
debit = {
|
||||
"account": inv.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.income_account,
|
||||
"debit": dunning_in_company_currency,
|
||||
"debit_in_account_currency": self.dunning_amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": "Dunning",
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
}
|
||||
|
||||
credit = {
|
||||
"account": self.income_account,
|
||||
"against": self.customer,
|
||||
"credit": dunning_in_company_currency,
|
||||
"credit_in_account_currency": self.dunning_amount,
|
||||
"cost_center": inv.cost_center or default_cost_center,
|
||||
"project": inv.project,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if val := inv.get(dimension):
|
||||
debit[dimension] = credit[dimension] = val
|
||||
|
||||
gl_entries = [
|
||||
self.get_gl_dict(debit, inv.party_account_currency, item=inv),
|
||||
self.get_gl_dict(credit, item=inv),
|
||||
]
|
||||
make_gl_entries(
|
||||
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import set_name_from_naming_options
|
||||
from frappe.utils import flt, fmt_money
|
||||
from frappe.utils import flt, fmt_money, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -83,7 +83,7 @@ class GLEntry(Document):
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if not (self.party_type and self.party):
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
@@ -261,7 +261,7 @@ def validate_balance_type(account, adv_adj=False):
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where account = %s""",
|
||||
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
@@ -405,7 +405,7 @@ def rename_temporarily_named_docs(doctype):
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -34,7 +34,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
finance_book: frm.doc.finance_book,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -433,8 +433,22 @@ class JournalEntry(AccountsController):
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
|
||||
customer_details = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Customer Credit Limit",
|
||||
filters={
|
||||
"parent": ["in", customers],
|
||||
"parenttype": ["=", "Customer"],
|
||||
"company": ["=", self.company],
|
||||
},
|
||||
fields=["parent", "bypass_credit_limit_check"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for customer in customers:
|
||||
check_credit_limit(customer, self.company)
|
||||
ignore_outstanding_sales_order = bool(customer_details.get(customer))
|
||||
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
|
||||
|
||||
def validate_cheque_info(self):
|
||||
if self.voucher_type in ["Bank Entry"]:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -18,22 +19,30 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
else:
|
||||
|
||||
@@ -60,6 +60,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
|
||||
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
||||
party_link.secondary_party = secondary_party
|
||||
|
||||
party_link.save(ignore_permissions=True)
|
||||
party_link.save()
|
||||
|
||||
return party_link
|
||||
|
||||
@@ -319,7 +319,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
"from_date": frm.doc.posting_date,
|
||||
"to_date": moment(frm.doc.modified).format('YYYY-MM-DD'),
|
||||
"company": frm.doc.company,
|
||||
"group_by": "",
|
||||
"categorize_by": "",
|
||||
"show_cancelled_entries": frm.doc.docstatus === 2
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
@@ -330,7 +330,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
payment_type: function(frm) {
|
||||
set_default_party_type(frm);
|
||||
if(frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(["party", "party_balance", "paid_from", "paid_to",
|
||||
$.each(["party", "party_type", "party_balance", "paid_from", "paid_to",
|
||||
"references", "total_allocated_amount"], function(i, field) {
|
||||
frm.set_value(field, null);
|
||||
});
|
||||
|
||||
@@ -350,15 +350,25 @@ class PaymentEntry(AccountsController):
|
||||
self.set(self.party_account_field, party_account)
|
||||
self.party_account = party_account
|
||||
|
||||
if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance):
|
||||
if self.paid_from and (
|
||||
not self.paid_from_account_currency
|
||||
or not self.paid_from_account_balance
|
||||
or not self.paid_from_account_type
|
||||
):
|
||||
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
|
||||
self.paid_from_account_currency = acc.account_currency
|
||||
self.paid_from_account_balance = acc.account_balance
|
||||
self.paid_from_account_type = acc.account_type
|
||||
|
||||
if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance):
|
||||
if self.paid_to and (
|
||||
not self.paid_to_account_currency
|
||||
or not self.paid_to_account_balance
|
||||
or not self.paid_to_account_type
|
||||
):
|
||||
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
|
||||
self.paid_to_account_currency = acc.account_currency
|
||||
self.paid_to_account_balance = acc.account_balance
|
||||
self.paid_to_account_type = acc.account_type
|
||||
|
||||
self.party_account_currency = (
|
||||
self.paid_from_account_currency
|
||||
@@ -465,7 +475,7 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or(_(d) for d in valid_reference_doctypes)
|
||||
comma_or([_(d) for d in valid_reference_doctypes])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1519,7 +1529,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
elif self.party_type in ("Supplier", "Customer"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
@@ -1580,7 +1590,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
@@ -1631,7 +1641,7 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
@@ -2481,6 +2491,7 @@ def get_payment_entry(
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||
|
||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||
pe.project = doc.get("project") or reduce(
|
||||
|
||||
@@ -270,6 +270,7 @@ class PaymentReconciliation(Document):
|
||||
for payment in non_reconciled_payments:
|
||||
row = self.append("payments", {})
|
||||
row.update(payment)
|
||||
row.is_advance = payment.book_advance_payments_in_separate_party_account
|
||||
|
||||
def get_invoice_entries(self):
|
||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||
@@ -354,6 +355,9 @@ class PaymentReconciliation(Document):
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
|
||||
exc_gain_loss_posting_date = frappe.db.get_single_value(
|
||||
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
|
||||
)
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
@@ -380,6 +384,11 @@ class PaymentReconciliation(Document):
|
||||
res.difference_account = default_exchange_gain_loss_account
|
||||
res.exchange_rate = inv.get("exchange_rate")
|
||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||
if not pay.get("is_advance"):
|
||||
if exc_gain_loss_posting_date == "Invoice":
|
||||
res.update({"gain_loss_posting_date": inv.get("invoice_date")})
|
||||
elif exc_gain_loss_posting_date == "Reconciliation Date":
|
||||
res.update({"gain_loss_posting_date": nowdate()})
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
|
||||
@@ -246,6 +246,7 @@ class PaymentRequest(Document):
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
"payment_gateway": self.payment_gateway,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"invoice_portion",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"section_break_6",
|
||||
"discount_type",
|
||||
"discount_date",
|
||||
"column_break_9",
|
||||
"discount",
|
||||
"discount_type",
|
||||
"column_break_9",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
@@ -155,12 +160,50 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
|
||||
"fieldname": "credit_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Days",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
|
||||
"fieldname": "credit_months",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Months",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount",
|
||||
"fieldname": "discount_validity_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Discount Validity Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount_validity_based_on",
|
||||
"fieldname": "discount_validity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discount Validity",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-16 13:57:06.382859",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -171,4 +214,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "company", "=", frm.doc.company],
|
||||
["Account", "is_group", "=", "0"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "freeze_account", "=", "No"],
|
||||
["Account", "root_type", "in", "Liability, Equity"],
|
||||
["Account", "root_type", "in", ["Liability", "Equity"]],
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -29,7 +29,7 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
group_by: "",
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -32,8 +32,13 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Account Closing Balance",
|
||||
)
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
@@ -201,6 +206,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
debit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
|
||||
credit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
|
||||
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -209,16 +217,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
|
||||
@@ -1623,6 +1623,5 @@
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -560,7 +560,13 @@ class POSInvoice(SalesInvoice):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"column_break_42",
|
||||
"free_item_uom",
|
||||
"round_free_qty",
|
||||
"dont_enforce_free_item_qty",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
@@ -643,12 +644,19 @@
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
|
||||
"fieldname": "dont_enforce_free_item_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Enforce Free Item Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -328,8 +328,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get("child_docname"),
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -555,7 +553,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
|
||||
if pricing_rule.margin_type in ["Percentage", "Amount"]:
|
||||
item_details.margin_rate_or_amount = 0.0
|
||||
item_details.margin_type = None
|
||||
elif pricing_rule.get("free_item"):
|
||||
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
|
||||
item_details.remove_free_item = (
|
||||
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
|
||||
)
|
||||
|
||||
@@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase):
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
def test_dont_enforce_free_item_qty(self):
|
||||
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"min_qty": 0,
|
||||
"max_qty": 7,
|
||||
"discount_percentage": 17.5,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 0,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
pricing_rule = frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With enforcement
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 2)
|
||||
|
||||
# Without enforcement
|
||||
pricing_rule.dont_enforce_free_item_qty = 1
|
||||
pricing_rule.save()
|
||||
|
||||
# Test 2 : Deleted free item will not be fetched again on save without enforcement
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_cumulative_pricing_rule(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
|
||||
test_record = {
|
||||
@@ -1239,6 +1287,7 @@ def make_pricing_rule(**args):
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -691,7 +691,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
|
||||
args.pop((item.item_code, item.pricing_rules))
|
||||
|
||||
for free_item in args.values():
|
||||
doc.append("items", free_item)
|
||||
if doc.is_new() or not frappe.get_value(
|
||||
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
|
||||
):
|
||||
doc.append("items", free_item)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
@@ -153,11 +154,16 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled"
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-20 21:05:43.121945",
|
||||
"modified": "2025-01-23 16:09:01.058574",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log Allocations",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"posting_date",
|
||||
"company",
|
||||
"account",
|
||||
"group_by",
|
||||
"categorize_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
@@ -172,14 +172,6 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"default": "Group by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "group_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group By",
|
||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "currency",
|
||||
@@ -395,10 +387,18 @@
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
},
|
||||
{
|
||||
"default": "Categorize by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "categorize_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
@@ -433,4 +433,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"categorize_by": doc.categorize_by,
|
||||
"currency": doc.currency,
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
@@ -177,17 +177,21 @@ def get_ar_filters(doc, entry):
|
||||
|
||||
def get_html(doc, filters, entry, col, res, ageing):
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
if doc.report == "General Ledger"
|
||||
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
)
|
||||
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
if doc.report == "General Ledger":
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
)
|
||||
|
||||
process_soa_html = frappe.get_hooks("process_soa_html")
|
||||
# fetching custom print format for Process Statement of Accounts
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
@@ -203,7 +207,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
@@ -262,9 +265,12 @@ def get_recipients_and_cc(customer, doc):
|
||||
recipients = []
|
||||
for clist in doc.customers:
|
||||
if clist.customer == customer:
|
||||
recipients.append(clist.billing_email)
|
||||
if clist.billing_email:
|
||||
for email in clist.billing_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
if doc.primary_mandatory and clist.primary_email:
|
||||
recipients.append(clist.primary_email)
|
||||
for email in clist.primary_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
cc = []
|
||||
if doc.cc_to != "":
|
||||
try:
|
||||
@@ -443,7 +449,7 @@ def send_auto_email():
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"enable_auto_email": 1},
|
||||
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
|
||||
or_filters={"to_date": today(), "posting_date": today()},
|
||||
)
|
||||
for entry in selected:
|
||||
send_emails(entry.name, from_scheduler=True)
|
||||
|
||||
@@ -29,7 +29,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
filters: {
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -302,7 +305,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
||||
|
||||
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
|
||||
let payment_terms_template = this.frm.doc.payment_terms_template;
|
||||
|
||||
erpnext.utils.get_party_details(
|
||||
this.frm,
|
||||
"erpnext.accounts.party.get_party_details",
|
||||
{
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
bill_date: this.frm.doc.bill_date,
|
||||
@@ -320,7 +327,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||
})
|
||||
|
||||
// while duplicating, don't change payment terms
|
||||
if (me.frm.doc.__run_link_triggers === false) {
|
||||
me.frm.set_value("payment_terms_template", payment_terms_template);
|
||||
me.frm.refresh_field("payment_terms_template");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
apply_tds(frm) {
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
@@ -33,7 +32,7 @@ from erpnext.accounts.general_ledger import (
|
||||
merge_similar_entries,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
@@ -174,7 +173,12 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
if not self.due_date:
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
|
||||
self.posting_date,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.company,
|
||||
self.bill_date,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
@@ -661,12 +665,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def update_supplier_outstanding(self, update_outstanding):
|
||||
if update_outstanding == "No":
|
||||
update_outstanding_amt(
|
||||
self.credit_to,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
account=self.credit_to,
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
@@ -782,7 +786,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
@@ -1017,6 +1021,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
@@ -1125,6 +1132,30 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
|
||||
@@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
},
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Receipt")) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1816,19 +1816,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -1852,9 +1849,8 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -1984,6 +1980,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dr_note.save)
|
||||
|
||||
def test_apply_discount_on_grand_total(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
the grand total is calculated correctly without any rounding errors
|
||||
"""
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 21.39,
|
||||
},
|
||||
)
|
||||
invoice.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 15.5,
|
||||
},
|
||||
)
|
||||
|
||||
# the grand total here will be 255.71
|
||||
invoice.disable_rounded_total = 1
|
||||
# apply discount on grand total to adjust the grand total to 255
|
||||
invoice.discount_amount = 0.71
|
||||
invoice.save()
|
||||
|
||||
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 255)
|
||||
|
||||
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||
"""
|
||||
To test if after applying discount on grand total,
|
||||
where the tax is calculated on previous row total, the grand total is calculated correctly
|
||||
"""
|
||||
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.extend(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"tax_amount": 100,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
{
|
||||
"charge_type": "On Previous Row Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"row_id": 1,
|
||||
"rate": 10,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
# the total here will be 340, so applying 40 discount
|
||||
invoice.discount_amount = 40
|
||||
invoice.save()
|
||||
|
||||
self.assertEqual(invoice.grand_total, 300)
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -40,7 +41,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -111,13 +112,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 15:47:28.167371",
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -6,6 +6,8 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -77,6 +79,9 @@ class RepostAccountingLedger(Document):
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
warehouse_account_map = get_warehouse_account_map(doc.company)
|
||||
gle_map = doc.get_gl_entries(warehouse_account_map)
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
@@ -146,7 +151,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
@@ -155,6 +160,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
|
||||
@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -202,9 +204,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_06_repost_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# change the provisional account
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
pr.items[0].name,
|
||||
"provisional_expense_account",
|
||||
another_provisional_account,
|
||||
)
|
||||
|
||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_doc.company = self.company
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
repost_doc.cancel()
|
||||
repost_doc.delete()
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.default_provisional_account = None
|
||||
company.save()
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
allowed_types = [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Receipt",
|
||||
]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
|
||||
@@ -9,6 +9,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
setup(doc) {
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
Dunning: this.make_dunning.bind(this),
|
||||
"Invoice Discounting": this.make_invoice_discounting.bind(this),
|
||||
};
|
||||
}
|
||||
company() {
|
||||
super.company();
|
||||
@@ -94,26 +98,35 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount>0) {
|
||||
cur_frm.add_custom_button(__('Payment Request'), function() {
|
||||
me.make_payment_request();
|
||||
}, __('Create'));
|
||||
if (doc.outstanding_amount > 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
this.make_invoice_discounting.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
|
||||
cur_frm.events.create_invoice_discounting(cur_frm);
|
||||
}, __('Create'));
|
||||
const payment_is_overdue = doc.payment_schedule
|
||||
.map((row) => Date.parse(row.due_date) < Date.now())
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (doc.due_date < frappe.datetime.get_today()) {
|
||||
cur_frm.add_custom_button(__('Dunning'), function() {
|
||||
cur_frm.events.create_dunning(cur_frm);
|
||||
}, __('Create'));
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1) {
|
||||
cur_frm.add_custom_button(__('Maintenance Schedule'), function () {
|
||||
cur_frm.cscript.make_maintenance_schedule();
|
||||
}, __('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Schedule"),
|
||||
this.make_maintenance_schedule.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
if(!doc.auto_repeat) {
|
||||
@@ -146,6 +159,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
make_invoice_discounting() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_dunning() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
@@ -948,20 +975,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
frm.set_df_property('return_against', 'label', __('Adjustment Against'));
|
||||
}
|
||||
},
|
||||
|
||||
create_invoice_discounting: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm
|
||||
});
|
||||
},
|
||||
|
||||
create_dunning: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
|
||||
@@ -24,7 +24,11 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
@@ -87,8 +91,8 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
@@ -494,7 +498,13 @@ class SalesInvoice(SellingController):
|
||||
"Account", self.debit_to, "account_currency", cache=True
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
@@ -1015,18 +1025,17 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
|
||||
update_outstanding_amt(
|
||||
self.debit_to,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
account=self.debit_to,
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
)
|
||||
|
||||
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
|
||||
|
||||
@@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -416,9 +416,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500.01)
|
||||
self.assertEqual(si.grand_total, 1500.01)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.rounding_adjustment, 0)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
@@ -1773,17 +1773,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
@@ -2839,6 +2828,60 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
def test_item_tax_template_change_with_grand_total_discount(self):
|
||||
"""
|
||||
Test that when item tax template changes due to discount on Grand Total,
|
||||
the tax calculations are consistent.
|
||||
"""
|
||||
item = create_item("Test Item With Multiple Tax Templates")
|
||||
|
||||
item.set("taxes", [])
|
||||
item.append(
|
||||
"taxes",
|
||||
{
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"minimum_net_rate": 0,
|
||||
"maximum_net_rate": 500,
|
||||
},
|
||||
)
|
||||
|
||||
item.append(
|
||||
"taxes",
|
||||
{
|
||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||
"minimum_net_rate": 501,
|
||||
"maximum_net_rate": 1000,
|
||||
},
|
||||
)
|
||||
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"rate": 0,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.discount_amount = 300
|
||||
si.save()
|
||||
|
||||
# Verify template changed to 10%
|
||||
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
|
||||
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
|
||||
|
||||
si.submit()
|
||||
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
discount_account = create_account(
|
||||
@@ -3298,6 +3341,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3305,24 +3349,23 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -3860,6 +3903,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.company = "_Test Company"
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
@@ -3870,6 +3914,89 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_create_return_invoice_for_self_update(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
invoice = create_sales_invoice()
|
||||
|
||||
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
|
||||
payment_entry.reference_no = "test001"
|
||||
payment_entry.reference_date = getdate()
|
||||
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
r_invoice = make_return_doc(invoice.doctype, invoice.name)
|
||||
|
||||
r_invoice.update_outstanding_for_self = 0
|
||||
r_invoice.save()
|
||||
|
||||
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
|
||||
|
||||
r_invoice.submit()
|
||||
|
||||
self.assertNotEqual(r_invoice.outstanding_amount, 0)
|
||||
|
||||
invoice.reload()
|
||||
|
||||
self.assertEqual(invoice.outstanding_amount, 0)
|
||||
|
||||
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "10"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe.source_exchange_rate = 85
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.received_amount = 8500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = si.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.save()
|
||||
ral.submit()
|
||||
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
q = (
|
||||
(
|
||||
frappe.qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.docstatus)
|
||||
.where(
|
||||
(je.voucher_type == "Exchange Gain Or Loss")
|
||||
& (jea.reference_name == si.name)
|
||||
& (jea.reference_type == "Sales Invoice")
|
||||
& (je.is_system_generated == 1)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
self.assertEqual(q[0][0], 1)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "250px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +42,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,13 +113,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-26 15:47:46.911595",
|
||||
"modified": "2024-12-20 11:58:28.962370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Int",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
@@ -102,7 +102,7 @@
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
@@ -199,7 +199,7 @@
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Int",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
@@ -221,7 +221,7 @@
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
@@ -324,7 +324,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-01-10 18:32:36.201124",
|
||||
"modified": "2025-12-10 08:06:40.611761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Share Balance",
|
||||
@@ -339,4 +339,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
"label": "Voucher Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
@@ -36,7 +34,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-13 13:40:41.479208",
|
||||
"modified": "2025-02-05 16:39:14.863698",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount",
|
||||
@@ -102,10 +102,11 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-27 21:47:34.396071",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -148,4 +149,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
cost_center = get_cost_center(inv)
|
||||
tax_row.update({"cost_center": cost_center})
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
@@ -233,7 +236,7 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
vouchers, voucher_wise_amount, zero_rate_ldc_invoices = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
@@ -287,7 +290,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
|
||||
taxable_vouchers = list(set(vouchers) - set(zero_rate_ldc_invoices))
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, taxable_vouchers)
|
||||
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
@@ -306,12 +310,33 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = (
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total"
|
||||
)
|
||||
field = [
|
||||
"name",
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"posting_date",
|
||||
]
|
||||
voucher_wise_amount = {}
|
||||
vouchers = []
|
||||
|
||||
ldcs = frappe.db.get_all(
|
||||
"Lower Deduction Certificate",
|
||||
filters={
|
||||
"valid_from": [">=", tax_details.from_date],
|
||||
"valid_upto": ["<=", tax_details.to_date],
|
||||
"company": company,
|
||||
"supplier": ["in", parties],
|
||||
},
|
||||
fields=["supplier", "valid_from", "valid_upto", "rate"],
|
||||
)
|
||||
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = [
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"name",
|
||||
"grand_total",
|
||||
"posting_date",
|
||||
]
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
frappe.scrub(party_type): ["in", parties],
|
||||
@@ -325,11 +350,31 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
||||
|
||||
ldcs = frappe.db.get_all(
|
||||
"Lower Deduction Certificate",
|
||||
filters={
|
||||
"valid_from": [">=", tax_details.from_date],
|
||||
"valid_upto": ["<=", tax_details.to_date],
|
||||
"company": company,
|
||||
"supplier": ["in", parties],
|
||||
"rate": 0,
|
||||
},
|
||||
fields=["name", "supplier", "valid_from", "valid_upto"],
|
||||
)
|
||||
|
||||
zero_rate_ldc_invoices = []
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
|
||||
_voucher_detail = {"amount": d.base_net_total, "voucher_type": doctype}
|
||||
|
||||
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
|
||||
if ldc[0].supplier in parties:
|
||||
_voucher_detail.update({"amount": 0})
|
||||
zero_rate_ldc_invoices.append(d.name)
|
||||
|
||||
voucher_wise_amount.update({d.name: _voucher_detail})
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
@@ -360,7 +405,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
|
||||
|
||||
return vouchers, voucher_wise_amount
|
||||
return vouchers, voucher_wise_amount, zero_rate_ldc_invoices
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
@@ -523,9 +568,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
else:
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
has_cumulative_threshold_breached = (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
@@ -533,9 +580,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
@@ -577,6 +622,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.utils import add_days, add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -243,17 +243,18 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
pe1 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
pe1.paid_from = "Debtors - _TC"
|
||||
pe1.paid_to = "Cash - _TC"
|
||||
pe1.submit()
|
||||
vouchers.append(pe1)
|
||||
|
||||
# create invoice
|
||||
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
|
||||
@@ -275,6 +276,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
# make another invoice
|
||||
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
|
||||
# TDS should be calculated
|
||||
|
||||
# this payment should not be considered for TCS calculation as it is outside of fiscal year
|
||||
pe2 = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
|
||||
)
|
||||
pe2.paid_from = "Debtors - _TC"
|
||||
pe2.paid_to = "Cash - _TC"
|
||||
pe2.posting_date = add_days(fiscal_year[1], -10)
|
||||
pe2.submit()
|
||||
vouchers.append(pe2)
|
||||
|
||||
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
|
||||
si2.submit()
|
||||
vouchers.append(si2)
|
||||
@@ -614,6 +626,49 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def test_ldc_at_0_rate(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
valid_from = fiscal_year[1]
|
||||
valid_upto = add_months(valid_from, 1)
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=0,
|
||||
limit=50000,
|
||||
valid_from=valid_from,
|
||||
valid_upto=valid_upto,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True
|
||||
)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
pi2 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier",
|
||||
rate=35000,
|
||||
posting_date=add_days(valid_upto, 1),
|
||||
set_posting_time=True,
|
||||
)
|
||||
pi2.submit()
|
||||
self.assertEqual(len(pi2.taxes), 1)
|
||||
# pi1 net total shouldn't be included as it lies within LDC at rate of '0'
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
@@ -771,7 +826,8 @@ def create_purchase_invoice(**args):
|
||||
pi = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Invoice",
|
||||
"posting_date": today(),
|
||||
"set_posting_time": args.set_posting_time or False,
|
||||
"posting_date": args.posting_date or today(),
|
||||
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||
"supplier": args.supplier,
|
||||
"company": "_Test Company",
|
||||
@@ -1099,7 +1155,9 @@ def create_tax_withholding_category(
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit):
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
@@ -1110,8 +1168,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"valid_from": valid_from or fiscal_year[1],
|
||||
"valid_upto": valid_upto or fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
|
||||
@@ -179,6 +179,15 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
round_off_account, default_currency = frappe.get_cached_value(
|
||||
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
|
||||
)
|
||||
if not precision:
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=default_currency,
|
||||
)
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
@@ -192,6 +201,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
if d.account == round_off_account:
|
||||
d.cost_center = cost_center_allocation[0][0]
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
|
||||
@@ -552,12 +552,13 @@ def validate_party_accounts(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
|
||||
"""Get due date from `Payment Terms Template`"""
|
||||
due_date = None
|
||||
if (bill_date or posting_date) and party:
|
||||
due_date = bill_date or posting_date
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
if not template_name:
|
||||
template_name = get_payment_terms_template(party, party_type, company)
|
||||
|
||||
if template_name:
|
||||
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
||||
@@ -597,35 +598,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
|
||||
|
||||
def validate_due_date(
|
||||
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
|
||||
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None, doctype=None
|
||||
):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
else:
|
||||
if not template_name:
|
||||
return
|
||||
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
|
||||
|
||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
|
||||
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
|
||||
|
||||
msgprint(
|
||||
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
|
||||
party_type, date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -903,12 +903,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
fields=["name", "is_shipping_address"],
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
|
||||
return shipping_addresses[0].name
|
||||
if len(shipping_addresses) == 1:
|
||||
return shipping_addresses[0].name
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
|
||||
@@ -111,6 +111,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -282,4 +282,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -14,7 +15,10 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_currency_precision,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -49,6 +53,10 @@ class ReceivablePayableReport:
|
||||
self.age_as_on = (
|
||||
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
|
||||
)
|
||||
self.ple_fetch_method = (
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -85,13 +93,7 @@ class ReceivablePayableReport:
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
# Get invoice details like bill_no, due_date etc for all invoices
|
||||
self.get_invoice_details()
|
||||
@@ -105,12 +107,45 @@ class ReceivablePayableReport:
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.prepare_ple_query()
|
||||
self.data = []
|
||||
self.voucher_balance = OrderedDict()
|
||||
|
||||
if self.ple_fetch_method == "Buffered Cursor":
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
|
||||
self.build_data()
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
@@ -131,26 +166,22 @@ class ReceivablePayableReport:
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
def init_voucher_balance(self, ple):
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
self.get_invoices(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
@@ -272,6 +303,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -498,7 +602,7 @@ class ReceivablePayableReport:
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
@@ -764,7 +868,7 @@ class ReceivablePayableReport:
|
||||
index = 4
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
def prepare_ple_query(self):
|
||||
# get all the GL entries filtered by the given filters
|
||||
|
||||
self.prepare_conditions()
|
||||
@@ -812,12 +916,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_entries = query.run(as_dict=True)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@@ -1204,3 +1311,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
**args,
|
||||
)
|
||||
if not no_payment_schedule:
|
||||
si.append(
|
||||
@@ -111,7 +112,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
def test_accounts_receivable_with_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -151,11 +152,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, True)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
|
||||
|
||||
row = report[1][0]
|
||||
row = report[1][-1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note,
|
||||
[
|
||||
@@ -168,6 +173,105 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_accounts_receivable_without_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertEqual(cr_note.update_outstanding_for_self, False)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(qty=2)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment[i - 1],
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
|
||||
self.assertFalse(cr_note.update_outstanding_for_self)
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[200, 100, 0, 80, 20, self.debit_to],
|
||||
[200, 40, 0, 0, 40, self.debit_to],
|
||||
]
|
||||
|
||||
for i in range(2):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(
|
||||
expected_data_after_credit_note[i - 1],
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.party_account,
|
||||
],
|
||||
)
|
||||
|
||||
def test_cr_note_flag_to_update_self(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -88,6 +88,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options: ["Asset Category", "Asset"],
|
||||
default: "Asset Category",
|
||||
},
|
||||
{
|
||||
fieldname: "asset_category",
|
||||
label: __("Asset Category"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category",
|
||||
depends_on: "eval: doc.group_by == 'Asset Category'",
|
||||
},
|
||||
{
|
||||
fieldname: "asset",
|
||||
label: __("Asset"),
|
||||
fieldtype: "Link",
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,21 +14,28 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
return get_group_by_asset_category_data(filters)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
return get_group_by_asset_data(filters)
|
||||
|
||||
|
||||
def get_group_by_asset_category_data(filters):
|
||||
data = []
|
||||
|
||||
asset_categories = get_asset_categories(filters)
|
||||
assets = get_assets(filters)
|
||||
asset_categories = get_asset_categories_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_category(filters)
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_category)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(
|
||||
@@ -38,17 +45,19 @@ def get_data(filters):
|
||||
if asset["asset_category"] == asset_category.get("asset_category", "")
|
||||
)
|
||||
)
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
@@ -57,52 +66,71 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
condition += " and a.asset_category = %(asset_category)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT asset_category,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by asset_category
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
@@ -114,14 +142,17 @@ def get_asset_categories(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
@@ -130,6 +161,11 @@ def get_assets(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -149,15 +185,22 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -165,51 +208,272 @@ def get_assets(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and a.name = %(asset)s"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT a.name,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_scrapped_asset,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_capitalized_asset
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
and not exists(
|
||||
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
|
||||
where acai.asset = a.name
|
||||
and ac.posting_date < %(from_date)s
|
||||
and ac.docstatus=1
|
||||
)
|
||||
group by a.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = f" and a.name = '{filters.get('asset')}'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.name) as results
|
||||
group by results.name
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
columns = []
|
||||
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset"),
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "cost_as_on_from_date",
|
||||
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "value_as_on_from_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of New Purchase"),
|
||||
"fieldname": "cost_of_new_purchase",
|
||||
"label": _("Value of New Purchase"),
|
||||
"fieldname": "value_of_new_purchase",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Sold Asset"),
|
||||
"fieldname": "cost_of_sold_asset",
|
||||
"label": _("Value of Sold Asset"),
|
||||
"fieldname": "value_of_sold_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost of Scrapped Asset"),
|
||||
"fieldname": "cost_of_scrapped_asset",
|
||||
"label": _("Value of Scrapped Asset"),
|
||||
"fieldname": "value_of_scrapped_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "cost_as_on_to_date",
|
||||
"label": _("Value of New Capitalized Asset"),
|
||||
"fieldname": "value_of_capitalized_asset",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Value as on") + " " + formatdate(filters.to_date),
|
||||
"fieldname": "value_as_on_to_date",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
@@ -237,6 +501,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
@@ -250,3 +520,5 @@ def get_columns(filters):
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<div style="margin-bottom: 7px;">
|
||||
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %}
|
||||
</div>
|
||||
<h2 class="text-center">{%= __("Bank Reconciliation Statement") %}</h2>
|
||||
<h4 class="text-center">{%= filters.account && (filters.account + ", "+filters.report_date) || "" %} {%= filters.company %}</h4>
|
||||
<hr>
|
||||
@@ -46,4 +43,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
||||
["Purchase Invoice", "docstatus", "=", 1],
|
||||
["Purchase Invoice", "per_received", "<", 100],
|
||||
["Purchase Invoice", "update_stock", "=", 0],
|
||||
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||
]
|
||||
|
||||
if report_filters.get("purchase_invoice"):
|
||||
|
||||
@@ -91,6 +91,7 @@ function get_filters() {
|
||||
fieldname: "budget_against_filter",
|
||||
label: __("Dimension Filter"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "budget_against",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
||||
and ba.account=gl.account
|
||||
and b.{budget_against} = gl.{budget_against}
|
||||
and gl.fiscal_year between %s and %s
|
||||
and gl.is_cancelled = 0
|
||||
and b.{budget_against} = %s
|
||||
and exists(
|
||||
select
|
||||
|
||||
@@ -67,5 +67,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">
|
||||
Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
|
||||
{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}
|
||||
</p>
|
||||
|
||||
@@ -512,12 +512,16 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
|
||||
if ignore_opening_entries:
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
|
||||
@@ -78,4 +78,4 @@
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
|
||||
|
||||
@@ -49,9 +49,14 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "against_voucher_no",
|
||||
label: __("Against Voucher No"),
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
fieldtype: "Break",
|
||||
},
|
||||
@@ -61,13 +66,14 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldtype: "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
frappe.query_report.set_filter_value("party", []);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
@@ -106,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
"",
|
||||
{
|
||||
label: __("Group by Voucher"),
|
||||
value: "Group by Voucher",
|
||||
label: __("Categorize by Voucher"),
|
||||
value: "Categorize by Voucher",
|
||||
},
|
||||
{
|
||||
label: __("Group by Voucher (Consolidated)"),
|
||||
value: "Group by Voucher (Consolidated)",
|
||||
label: __("Categorize by Voucher (Consolidated)"),
|
||||
value: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
label: __("Group by Account"),
|
||||
value: "Group by Account",
|
||||
label: __("Categorize by Account"),
|
||||
value: "Categorize by Account",
|
||||
},
|
||||
{
|
||||
label: __("Group by Party"),
|
||||
value: "Group by Party",
|
||||
label: __("Categorize by Party"),
|
||||
value: "Categorize by Party",
|
||||
},
|
||||
],
|
||||
default: "Group by Voucher (Consolidated)",
|
||||
default: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
fieldname: "tax_id",
|
||||
@@ -146,6 +152,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -156,6 +163,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -189,6 +197,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Show Net Values in Party Account"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_amount_in_company_currency",
|
||||
label: __("Show Credit / Debit in Company Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_remarks",
|
||||
label: __("Show Remarks"),
|
||||
|
||||
@@ -72,13 +72,17 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
||||
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||
filters["categorize_by"] = filters["group_by"]
|
||||
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||
|
||||
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
|
||||
filters.account = frappe.parse_json(filters.get("account"))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
||||
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@@ -172,9 +176,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if filters.get("include_dimensions"):
|
||||
order_by_statement = "order by posting_date, creation"
|
||||
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
if filters.get("categorize_by") == "Categorize by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
@@ -201,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map)
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
else:
|
||||
return gl_entries
|
||||
|
||||
@@ -209,6 +213,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
if filters.account:
|
||||
@@ -221,6 +229,9 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("against_voucher_no"):
|
||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
||||
|
||||
if filters.get("ignore_err"):
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
@@ -254,7 +265,7 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
if filters.get("party_type"):
|
||||
@@ -266,11 +277,17 @@ def get_conditions(filters):
|
||||
if not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
):
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date <=%(to_date)s")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
@@ -355,13 +372,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
for _acc, acc_dict in gle_map.items():
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher":
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
@@ -370,7 +387,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher":
|
||||
data.append(acc_dict.totals.closing)
|
||||
data.append({})
|
||||
else:
|
||||
@@ -403,9 +420,9 @@ def get_totals_dict():
|
||||
|
||||
|
||||
def group_by_field(group_by):
|
||||
if group_by == "Group by Party":
|
||||
if group_by == "Categorize by Party":
|
||||
return "party"
|
||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
||||
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
|
||||
return "account"
|
||||
else:
|
||||
return "voucher_no"
|
||||
@@ -413,7 +430,7 @@ def group_by_field(group_by):
|
||||
|
||||
def initialize_gle_map(gl_entries, filters):
|
||||
gle_map = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
|
||||
for gle in gl_entries:
|
||||
gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[]))
|
||||
@@ -424,8 +441,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
totals = get_totals_dict()
|
||||
entries = []
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
|
||||
|
||||
if filters.get("show_net_values_in_party_account"):
|
||||
account_type_map = get_account_type_map(filters.get("company"))
|
||||
@@ -496,6 +513,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
keylist.append(gle.get("project"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
@@ -520,17 +538,20 @@ def get_account_type_map(company):
|
||||
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
balance = get_balance(d, balance, "debit", "credit")
|
||||
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
|
||||
d["presentation_currency"] = filters.presentation_currency
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -556,11 +577,21 @@ def get_columns(filters):
|
||||
if filters.get("presentation_currency"):
|
||||
currency = filters["presentation_currency"]
|
||||
else:
|
||||
if filters.get("company"):
|
||||
currency = get_company_currency(filters["company"])
|
||||
else:
|
||||
company = get_default_company()
|
||||
currency = get_company_currency(company)
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
company_currency = get_company_currency(filters.get("company") or get_default_company())
|
||||
|
||||
if (
|
||||
filters.get("show_amount_in_company_currency")
|
||||
and filters["presentation_currency"] != company_currency
|
||||
):
|
||||
frappe.throw(
|
||||
_("Presentation Currency cannot be {0} , When {1} is enabled.").format(
|
||||
frappe.bold(filters["presentation_currency"]),
|
||||
frappe.bold("Show Credit / Debit in Company Currency"),
|
||||
)
|
||||
)
|
||||
|
||||
columns = [
|
||||
{
|
||||
@@ -581,19 +612,22 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Debit ({0})").format(currency),
|
||||
"fieldname": "debit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Credit ({0})").format(currency),
|
||||
"fieldname": "credit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Balance ({0})").format(currency),
|
||||
"fieldname": "balance",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
|
||||
@@ -607,10 +641,11 @@ def get_columns(filters):
|
||||
{"label": _("Against Account"), "fieldname": "against", "width": 120},
|
||||
{"label": _("Party Type"), "fieldname": "party_type", "width": 100},
|
||||
{"label": _("Party"), "fieldname": "party", "width": 100},
|
||||
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
|
||||
|
||||
for dim in get_accounting_dimensions(as_list=False):
|
||||
columns.append(
|
||||
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": True,
|
||||
}
|
||||
)
|
||||
@@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": False,
|
||||
}
|
||||
)
|
||||
@@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
@@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disable_prepared_report": 0,
|
||||
@@ -9,7 +9,7 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2022-02-11 10:18:36.956558",
|
||||
"modified": "2025-01-27 18:40:24.493829",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
|
||||
@@ -166,7 +166,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
for src in gross_profit_data.si_list:
|
||||
if src.indent == 1:
|
||||
total_base_amount += src.base_amount or 0.0
|
||||
total_buying_amount += src.buying_amount or 0.0
|
||||
|
||||
row = frappe._dict()
|
||||
row.indent = src.indent
|
||||
row.parent_invoice = src.parent_invoice
|
||||
@@ -177,17 +184,57 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"sales_invoice": "Total",
|
||||
"qty": None,
|
||||
"avg._selling_rate": None,
|
||||
"valuation_rate": None,
|
||||
"selling_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_%": flt(
|
||||
(total_gross_profit / total_base_amount) * 100.0,
|
||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||
)
|
||||
if total_base_amount
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for src in gross_profit_data.grouped_data:
|
||||
row = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
row.append(filters.currency)
|
||||
group_columns = group_wise_columns.get(scrub(filters.group_by))
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
total_base_amount += src.base_amount or 0.00
|
||||
total_buying_amount += src.buying_amount or 0.00
|
||||
|
||||
row = [src.get(col) for col in group_columns] + [filters.currency]
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
"base_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
|
||||
}
|
||||
|
||||
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
|
||||
data.append(total_row)
|
||||
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -558,3 +558,33 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
@@ -11,7 +11,7 @@ import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
apply_order_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
|
||||
@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query = query.orderby(ii.item_code)
|
||||
query += f" order by {ii.item_code}"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query = query.orderby(ii.item_group)
|
||||
query += f" order by {ii.item_group}"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
|
||||
return query
|
||||
|
||||
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
@@ -51,6 +51,7 @@ function get_filters() {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class PaymentLedger:
|
||||
against_voucher_no=ple.against_voucher_no,
|
||||
amount=ple.amount,
|
||||
currency=ple.account_currency,
|
||||
company=ple.company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -77,6 +78,7 @@ class PaymentLedger:
|
||||
against_voucher_no="Outstanding:",
|
||||
amount=total,
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
@@ -85,7 +87,12 @@ class PaymentLedger:
|
||||
voucher_data.append(entry)
|
||||
|
||||
# empty row
|
||||
voucher_data.append(frappe._dict())
|
||||
voucher_data.append(
|
||||
frappe._dict(
|
||||
currency=voucher_data[0].currency,
|
||||
company=voucher_data[0].company,
|
||||
)
|
||||
)
|
||||
self.data.extend(voucher_data)
|
||||
|
||||
def build_conditions(self):
|
||||
|
||||
@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.orderby(pi.posting_date, pi.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -413,8 +412,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -438,7 +438,6 @@ def get_invoices(filters, additional_query_columns):
|
||||
si.company,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.orderby(si.posting_date, si.name, order=Order.desc)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
@@ -453,8 +452,17 @@ def get_invoices(filters, additional_query_columns):
|
||||
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
||||
)
|
||||
|
||||
invoices = query.run(as_dict=True)
|
||||
return invoices
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -52,28 +52,25 @@ def group_by_party_and_category(data, filters):
|
||||
party_category_wise_map = {}
|
||||
|
||||
for row in data:
|
||||
key = (row.get("party_type"), row.get("party"), row.get("tax_withholding_category"))
|
||||
party_category_wise_map.setdefault(
|
||||
(row.get("party"), row.get("section_code")),
|
||||
key,
|
||||
{
|
||||
"pan": row.get("pan"),
|
||||
"tax_id": row.get("tax_id"),
|
||||
"party": row.get("party"),
|
||||
"party_type": row.get("party_type"),
|
||||
"party_name": row.get("party_name"),
|
||||
"section_code": row.get("section_code"),
|
||||
"entity_type": row.get("entity_type"),
|
||||
"tax_withholding_category": row.get("tax_withholding_category"),
|
||||
"party_entity_type": row.get("party_entity_type"),
|
||||
"rate": row.get("rate"),
|
||||
"total_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
|
||||
"total_amount", 0.0
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
|
||||
"tax_amount", 0.0
|
||||
)
|
||||
party_category_wise_map.get(key)["total_amount"] += row.get("total_amount", 0.0)
|
||||
party_category_wise_map.get(key)["tax_amount"] += row.get("tax_amount", 0.0)
|
||||
|
||||
final_result = get_final_result(party_category_wise_map)
|
||||
|
||||
@@ -114,13 +111,18 @@ def get_columns(filters):
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"label": _("Tax Withholding Category"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
|
||||
{
|
||||
"label": _("{0} Type").format(_(filters.get("party_type", "Party"))),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -33,6 +34,7 @@ def execute(filters=None):
|
||||
|
||||
def validate_filters(filters):
|
||||
"""Validate if dates are properly set"""
|
||||
filters = frappe._dict(filters or {})
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
|
||||
out = []
|
||||
entries = {}
|
||||
for name, details in gle_map.items():
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
@@ -68,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
@@ -103,8 +106,8 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"tax_withholding_category": tax_withholding_category or "",
|
||||
"party_entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
@@ -117,9 +120,14 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
key = entry.voucher_no
|
||||
if key in entries:
|
||||
entries[key]["tax_amount"] += tax_amount
|
||||
else:
|
||||
entries[key] = row
|
||||
out = list(entries.values())
|
||||
out.sort(key=lambda x: (x["tax_withholding_category"], x["transaction_date"], x["ref_no"]))
|
||||
|
||||
return out
|
||||
|
||||
@@ -169,9 +177,9 @@ def get_columns(filters):
|
||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
||||
columns = [
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"label": _("Tax Withholding Category"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
@@ -200,7 +208,12 @@ def get_columns(filters):
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("{0} Type").format(_(filters.get("party_type", "Party"))),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
)
|
||||
if filters.party_type == "Supplier":
|
||||
@@ -435,12 +448,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
def get_tax_rate_map(filters):
|
||||
rate_map = frappe.get_all(
|
||||
"Tax Withholding Rate",
|
||||
filters={
|
||||
"from_date": ("<=", filters.get("from_date")),
|
||||
"to_date": (">=", filters.get("to_date")),
|
||||
},
|
||||
fields=["parent", "tax_withholding_rate"],
|
||||
as_list=1,
|
||||
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
|
||||
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
|
||||
)
|
||||
|
||||
return frappe._dict(rate_map)
|
||||
rate_list = frappe._dict()
|
||||
|
||||
for rate in rate_map:
|
||||
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
|
||||
|
||||
return rate_list
|
||||
|
||||
|
||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
||||
# returns the row that matches with the fiscal year from posting date
|
||||
for rate in tax_withholding:
|
||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
||||
return rate.tax_withholding_rate
|
||||
|
||||
return 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_to_date, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -60,13 +60,65 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def test_date_filters_in_multiple_tax_withholding_rules(self):
|
||||
create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1)
|
||||
# insert new rate in same fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
},
|
||||
)
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
company="_Test Company",
|
||||
party_type="Supplier",
|
||||
from_date=fiscal_year[1],
|
||||
to_date=fiscal_year[2],
|
||||
)
|
||||
)[1]
|
||||
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def check_expected_values(self, result, expected_values):
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
voucher_expected_values = expected_values[i]
|
||||
voucher_actual_values = (
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.tax_withholding_category,
|
||||
voucher.rate,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
|
||||
@@ -79,8 +79,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry",
|
||||
label: __("Period Closing Entry"),
|
||||
fieldname: "with_period_closing_entry_for_opening",
|
||||
label: __("With Period Closing Entry For Opening Balances"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_current_period",
|
||||
label: __("Period Closing Entry For Current Period"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user