mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-10 17:02:28 +00:00
Compare commits
589 Commits
mergify/bp
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ae191478 | ||
|
|
6cacead726 | ||
|
|
2d8513de4e | ||
|
|
992027fe89 | ||
|
|
4eb045d927 | ||
|
|
1717a7c983 | ||
|
|
2de4b2ea56 | ||
|
|
b08e0014f7 | ||
|
|
f912c8419a | ||
|
|
f757adc7f7 | ||
|
|
df65fbbc4a | ||
|
|
59bd35c64d | ||
|
|
bd03bcdcb2 | ||
|
|
7fcf277055 | ||
|
|
1e2bcde0f5 | ||
|
|
f23d6911f3 | ||
|
|
12c1b8a910 | ||
|
|
f4c37f1f20 | ||
|
|
a799af7f9f | ||
|
|
f697679b37 | ||
|
|
f14b3ed723 | ||
|
|
fab7f9ee53 | ||
|
|
6e07aac5b7 | ||
|
|
a4fe0fb809 | ||
|
|
533257c4f3 | ||
|
|
33110951b3 | ||
|
|
12ebab1657 | ||
|
|
50eb6786bf | ||
|
|
590207419a | ||
|
|
92f69ae484 | ||
|
|
610dcbb974 | ||
|
|
e3ca318e93 | ||
|
|
98838b1dd5 | ||
|
|
abc7bf2fd6 | ||
|
|
8a19dc4a20 | ||
|
|
301b294da9 | ||
|
|
bf6c331ac4 | ||
|
|
780d3f5ba4 | ||
|
|
dbf9faa87c | ||
|
|
6494fc42c6 | ||
|
|
3abdfcb269 | ||
|
|
0d58dfd0fa | ||
|
|
f6ebf2d0b3 | ||
|
|
842a3645dc | ||
|
|
a6d92e5ec7 | ||
|
|
ce19514a2c | ||
|
|
e223731924 | ||
|
|
39b6aab714 | ||
|
|
6703610596 | ||
|
|
f4f2d11fa4 | ||
|
|
5c4f778223 | ||
|
|
5f97bec2b3 | ||
|
|
f1a2e1b725 | ||
|
|
17397ae652 | ||
|
|
6b83309750 | ||
|
|
e08f82909c | ||
|
|
4cc2afbd83 | ||
|
|
c780796284 | ||
|
|
ddf4a83cf8 | ||
|
|
b5c739d1cc | ||
|
|
2bc19783cb | ||
|
|
1adbf90d8c | ||
|
|
7386270fce | ||
|
|
156dda8157 | ||
|
|
af974fbccd | ||
|
|
e85238383f | ||
|
|
b380b60486 | ||
|
|
7fa800b874 | ||
|
|
8b6e58d02a | ||
|
|
58203a89f1 | ||
|
|
eb5899c786 | ||
|
|
4605051903 | ||
|
|
15397b17f3 | ||
|
|
b63566681b | ||
|
|
a5e49ea8a1 | ||
|
|
98f186b0e0 | ||
|
|
eac6e6a7dd | ||
|
|
2de3f63478 | ||
|
|
334bb609f0 | ||
|
|
b4cf6a1fb9 | ||
|
|
4a26810871 | ||
|
|
96cd8cdb38 | ||
|
|
912ffc2d64 | ||
|
|
f1f61ff61b | ||
|
|
aaca906a0f | ||
|
|
94b75e80b9 | ||
|
|
67f7341721 | ||
|
|
b11d064a2a | ||
|
|
959c311795 | ||
|
|
1d57bbca11 | ||
|
|
c9d98eb4f0 | ||
|
|
fc7a33ebf8 | ||
|
|
b48bff2029 | ||
|
|
a82c0c20f0 | ||
|
|
83d575206b | ||
|
|
e2d4ce74d9 | ||
|
|
13ce7279a8 | ||
|
|
4672c2c383 | ||
|
|
946073cfd9 | ||
|
|
1d97b7cc2b | ||
|
|
128e243945 | ||
|
|
a6a04e8245 | ||
|
|
6c8e909599 | ||
|
|
aab6271b14 | ||
|
|
ff2faf36a7 | ||
|
|
231356a005 | ||
|
|
630d873214 | ||
|
|
6cc421eec6 | ||
|
|
67427264d3 | ||
|
|
5d0958c5b1 | ||
|
|
1d7a8dda26 | ||
|
|
323d8eaccd | ||
|
|
d5301d3111 | ||
|
|
ef41654fcf | ||
|
|
aedefc867e | ||
|
|
4a4c2188ec | ||
|
|
22e4c7446e | ||
|
|
6cf24feffc | ||
|
|
b6e9b532aa | ||
|
|
8f43b41cad | ||
|
|
bc17d778a6 | ||
|
|
de6e787087 | ||
|
|
58eda49549 | ||
|
|
f00a63b69d | ||
|
|
0678638106 | ||
|
|
d610d1dccd | ||
|
|
3c70cbbaf8 | ||
|
|
6a8bd0ae9e | ||
|
|
47c0b47722 | ||
|
|
dbab718aaa | ||
|
|
ff0969ace6 | ||
|
|
6836b8830a | ||
|
|
8e2d4b2b77 | ||
|
|
d652fbeb01 | ||
|
|
06702ffae2 | ||
|
|
083a28d3b4 | ||
|
|
870181de87 | ||
|
|
67170d0a27 | ||
|
|
6026e9b3d4 | ||
|
|
da59db357e | ||
|
|
b2da214346 | ||
|
|
70117d3b06 | ||
|
|
0168639125 | ||
|
|
c848c2dba8 | ||
|
|
a60f7eaf3a | ||
|
|
cb952285b0 | ||
|
|
c25a85199c | ||
|
|
3773f56b0b | ||
|
|
235acd4713 | ||
|
|
acb3ef78a7 | ||
|
|
2ac2e02b2f | ||
|
|
ab4b47c0af | ||
|
|
2322a26916 | ||
|
|
05f2b43344 | ||
|
|
bd928e0d56 | ||
|
|
4cfd186aec | ||
|
|
85737327a3 | ||
|
|
09bedef9e1 | ||
|
|
1edd030e60 | ||
|
|
d22f4682b1 | ||
|
|
c021cf01fc | ||
|
|
58abcdf0c9 | ||
|
|
dd281b6375 | ||
|
|
0a186328e4 | ||
|
|
ed7c021900 | ||
|
|
c3c1b1f830 | ||
|
|
6e1fcfd210 | ||
|
|
2bc097a82c | ||
|
|
c6c1ab458c | ||
|
|
72efd21c47 | ||
|
|
c7290ce4a7 | ||
|
|
126fe8c974 | ||
|
|
cf492c3eb7 | ||
|
|
a1c74679da | ||
|
|
59f5fb6494 | ||
|
|
c75fbbd8f4 | ||
|
|
c261a436ac | ||
|
|
b85817d9c1 | ||
|
|
86b30c422b | ||
|
|
3fcab6e727 | ||
|
|
770297fd43 | ||
|
|
7d9bd48a4f | ||
|
|
a5a3f52c64 | ||
|
|
f9cafcc282 | ||
|
|
5fe8692a8d | ||
|
|
69cb2ca839 | ||
|
|
72b4aa1aac | ||
|
|
e77144414a | ||
|
|
b452e06b82 | ||
|
|
dffa8010c1 | ||
|
|
dcbcc596f2 | ||
|
|
c0c2e2367c | ||
|
|
95b9870de1 | ||
|
|
374e89ab33 | ||
|
|
523a5d0a49 | ||
|
|
25cafa6044 | ||
|
|
3ed8a99603 | ||
|
|
cdeeb36fe4 | ||
|
|
8598ca9a9d | ||
|
|
bdc04bf531 | ||
|
|
88097e78d2 | ||
|
|
ee65ceebad | ||
|
|
21c0fc5db6 | ||
|
|
3bbca629c6 | ||
|
|
be820ffe59 | ||
|
|
c253fb8902 | ||
|
|
8756f91857 | ||
|
|
da716b824f | ||
|
|
039f5e6143 | ||
|
|
44fd94c0d4 | ||
|
|
41d1703e7c | ||
|
|
4f503ac7f6 | ||
|
|
0fef95bfbb | ||
|
|
8c82b86b42 | ||
|
|
a93eed0fb7 | ||
|
|
437d0eea77 | ||
|
|
0ec30a1cea | ||
|
|
7e51346946 | ||
|
|
6849149176 | ||
|
|
a5e29e3659 | ||
|
|
87cbed0911 | ||
|
|
ca3e3a7941 | ||
|
|
584f6c42f0 | ||
|
|
282d28fbce | ||
|
|
20e9706ec3 | ||
|
|
9c1be96990 | ||
|
|
25e5a623d6 | ||
|
|
35a8d02866 | ||
|
|
44ff6ed6a1 | ||
|
|
a403940612 | ||
|
|
bf5f24c0e0 | ||
|
|
35474d997d | ||
|
|
ad886b6389 | ||
|
|
6408975b61 | ||
|
|
877f5611b1 | ||
|
|
d65c715e11 | ||
|
|
a7a8ff2086 | ||
|
|
71a8df2189 | ||
|
|
181ad0bdcd | ||
|
|
1963e03264 | ||
|
|
d383c70020 | ||
|
|
27fac7a352 | ||
|
|
bccbfe97b3 | ||
|
|
0e8f8677b8 | ||
|
|
3ffd50c772 | ||
|
|
b527d38bfa | ||
|
|
526b850e61 | ||
|
|
4024d8846b | ||
|
|
2757368579 | ||
|
|
b593150521 | ||
|
|
14128a47e7 | ||
|
|
7592c0956c | ||
|
|
a2d907d8bc | ||
|
|
d864d166f9 | ||
|
|
4a01c53cca | ||
|
|
3057a47994 | ||
|
|
29cbddbc77 | ||
|
|
34d2c8d9c2 | ||
|
|
fed8236919 | ||
|
|
9b09dd063d | ||
|
|
f18385c35d | ||
|
|
8411e4c5b2 | ||
|
|
81614939ab | ||
|
|
ea4379e4f2 | ||
|
|
89a603f20c | ||
|
|
ea63bfc9af | ||
|
|
073f88892e | ||
|
|
3c03c94f1a | ||
|
|
d22434d31e | ||
|
|
dc14a629ff | ||
|
|
f746540420 | ||
|
|
7fcdebcbd1 | ||
|
|
6e46c8f7c7 | ||
|
|
3cc9fb92d8 | ||
|
|
d5c457b8c5 | ||
|
|
fb802bc26b | ||
|
|
324bdcb177 | ||
|
|
452eaaf44e | ||
|
|
e57e8aa708 | ||
|
|
acdfdb1389 | ||
|
|
3a1c12d49c | ||
|
|
875cf68df8 | ||
|
|
6bc0d71fc8 | ||
|
|
552c6eb9f5 | ||
|
|
8202d2ed47 | ||
|
|
3718ac0c33 | ||
|
|
a3937ed44e | ||
|
|
fb515c8ddc | ||
|
|
02c7006525 | ||
|
|
cd8d4af900 | ||
|
|
dc5fd40a0c | ||
|
|
e3fe298297 | ||
|
|
533af66057 | ||
|
|
dbda66a62f | ||
|
|
bebbfd8f94 | ||
|
|
82741fbbe7 | ||
|
|
b11a1ecb7a | ||
|
|
b11d5ab04d | ||
|
|
5cee8edbb4 | ||
|
|
2dd5e2abd0 | ||
|
|
4a771fe765 | ||
|
|
8d10759631 | ||
|
|
d5ab4c1d7d | ||
|
|
81ae03e1a5 | ||
|
|
9f9120451b | ||
|
|
76a27541f3 | ||
|
|
9889d23b8c | ||
|
|
3578ee1195 | ||
|
|
4f8b2e520a | ||
|
|
0dc2545fb9 | ||
|
|
6e597b9c42 | ||
|
|
48acbe6b50 | ||
|
|
75cf70c8f3 | ||
|
|
d10530ee47 | ||
|
|
cf4b395ee3 | ||
|
|
90f399d0fc | ||
|
|
689172ff22 | ||
|
|
b2e109318f | ||
|
|
23b1b7ee04 | ||
|
|
06177ffaff | ||
|
|
a664f3039b | ||
|
|
daf1d52fc9 | ||
|
|
23f9d4c600 | ||
|
|
096e74b1ee | ||
|
|
33ab24943c | ||
|
|
b75940bf0e | ||
|
|
5ffbf59d78 | ||
|
|
47055901c0 | ||
|
|
a4e291bb77 | ||
|
|
1b0fc0541b | ||
|
|
cee3813ced | ||
|
|
6bd36a137c | ||
|
|
f4b18f2ad7 | ||
|
|
62a8e4a561 | ||
|
|
99b7a9d15c | ||
|
|
9391c8911c | ||
|
|
d3d03e8d83 | ||
|
|
6730960f56 | ||
|
|
1f91dcb1bd | ||
|
|
320f0056a2 | ||
|
|
a7ec01bf21 | ||
|
|
36f923c540 | ||
|
|
8bc7fe7e55 | ||
|
|
ff78aaeb3b | ||
|
|
b10cf4a928 | ||
|
|
027a4ea1bf | ||
|
|
c21a713750 | ||
|
|
b98977dc75 | ||
|
|
1979879b07 | ||
|
|
f5057cfb66 | ||
|
|
5a26d593e4 | ||
|
|
866b252309 | ||
|
|
2065f2b117 | ||
|
|
b99d2e16c4 | ||
|
|
468d181a00 | ||
|
|
997d573dc0 | ||
|
|
2442be5773 | ||
|
|
381072170a | ||
|
|
e3ab0e7c67 | ||
|
|
a68cbb177c | ||
|
|
f8f47d0a73 | ||
|
|
e6ad752c99 | ||
|
|
d8756fc7de | ||
|
|
fc967fceb2 | ||
|
|
b7fbe31558 | ||
|
|
eef77291ad | ||
|
|
82285e236f | ||
|
|
2579402852 | ||
|
|
609164fb9a | ||
|
|
66712fa8b5 | ||
|
|
99a0ba0b45 | ||
|
|
790876ea5b | ||
|
|
f7a37d2812 | ||
|
|
993ba4cf45 | ||
|
|
bb081e46d7 | ||
|
|
6985f0efc3 | ||
|
|
302ff49b7f | ||
|
|
ee2b65806b | ||
|
|
ba459204b0 | ||
|
|
7d4785784b | ||
|
|
35b503932d | ||
|
|
736a776d3d | ||
|
|
ae8b34e03c | ||
|
|
11b9b1adc5 | ||
|
|
9f6bc7fe49 | ||
|
|
1de0c46c51 | ||
|
|
2abb011816 | ||
|
|
eda1dae882 | ||
|
|
9164162a9e | ||
|
|
b7c6d8e2a6 | ||
|
|
7e63f1d220 | ||
|
|
3d8502f408 | ||
|
|
c3e869c701 | ||
|
|
aee03417de | ||
|
|
14d8b87c8e | ||
|
|
2a86a1fb98 | ||
|
|
28180ccaa4 | ||
|
|
7bf17372b0 | ||
|
|
a5b881ea74 | ||
|
|
ac40b46a6d | ||
|
|
69682cb064 | ||
|
|
8b543e5503 | ||
|
|
c4f90c3b34 | ||
|
|
244dce5098 | ||
|
|
194ab87fef | ||
|
|
e0299e1cbd | ||
|
|
9c970acbda | ||
|
|
06dde659c2 | ||
|
|
9fc17e0e3a | ||
|
|
535f8657ed | ||
|
|
c77781a14f | ||
|
|
c9d22386ed | ||
|
|
2e7c3207c4 | ||
|
|
0aef591f5d | ||
|
|
39049948b8 | ||
|
|
39b9d798d9 | ||
|
|
bebb8ae1ea | ||
|
|
1c8266af39 | ||
|
|
73729f6ab0 | ||
|
|
a287201011 | ||
|
|
a4e2fbdcf9 | ||
|
|
21dc0a0b1a | ||
|
|
98626aaa6c | ||
|
|
86a8015cea | ||
|
|
8330b349d2 | ||
|
|
29197af11a | ||
|
|
a00a3868ed | ||
|
|
335dcc976c | ||
|
|
6972f161b8 | ||
|
|
79b8505972 | ||
|
|
fe7b797e5f | ||
|
|
df329964dd | ||
|
|
1845d12951 | ||
|
|
67c2ab4c9f | ||
|
|
405e1ab6d3 | ||
|
|
cf066edd7f | ||
|
|
96e2e356b6 | ||
|
|
0fef2d4b02 | ||
|
|
b265b82f0b | ||
|
|
c92a06d77d | ||
|
|
c0236191aa | ||
|
|
5f083d55b5 | ||
|
|
56da3bd2e4 | ||
|
|
f27077a45f | ||
|
|
38b51df17e | ||
|
|
f145e6267b | ||
|
|
09d9c0ddd3 | ||
|
|
e4b5507446 | ||
|
|
a028d856bc | ||
|
|
e3e4680ad2 | ||
|
|
276406bc1c | ||
|
|
156e46ccb0 | ||
|
|
07a5aba2aa | ||
|
|
4ce8d9af6a | ||
|
|
0474b8595b | ||
|
|
ca8677a0ff | ||
|
|
03e5467ba2 | ||
|
|
e960f8217b | ||
|
|
a81e807a70 | ||
|
|
3b4ee30dd7 | ||
|
|
521ebc25aa | ||
|
|
1291df9a63 | ||
|
|
70d7ceb2f2 | ||
|
|
512419eee7 | ||
|
|
c6be380e83 | ||
|
|
200496254b | ||
|
|
2596ef202b | ||
|
|
ac71969512 | ||
|
|
734a7b8be9 | ||
|
|
aa9c4555fd | ||
|
|
7e5be50997 | ||
|
|
03e3a693ff | ||
|
|
d4ae2f89b2 | ||
|
|
17c24a4168 | ||
|
|
c41824c4d0 | ||
|
|
3ad611966e | ||
|
|
4b7cb6bfad | ||
|
|
4c7a0a4e4c | ||
|
|
4527877bb5 | ||
|
|
ddc97df31a | ||
|
|
c21ebafaa5 | ||
|
|
17276a2c0c | ||
|
|
4ecdd1fd0e | ||
|
|
7e093d08a9 | ||
|
|
955e8714ee | ||
|
|
1a262483a4 | ||
|
|
eead27560c | ||
|
|
b658330881 | ||
|
|
4f968f5c65 | ||
|
|
838183941a | ||
|
|
92d86eb30b | ||
|
|
efeda90cad | ||
|
|
47d4319f83 | ||
|
|
eb5a9db749 | ||
|
|
a2ccb5fa87 | ||
|
|
b56dbe98cf | ||
|
|
cf5a2d6351 | ||
|
|
564de01463 | ||
|
|
ac8637d5a0 | ||
|
|
7c10775bc8 | ||
|
|
49a96f4306 | ||
|
|
161ed5290e | ||
|
|
a0d3b931f3 | ||
|
|
11b82ba008 | ||
|
|
1b73170e8c | ||
|
|
59c46a1789 | ||
|
|
24e0e3505d | ||
|
|
5824b5effd | ||
|
|
fba16efc07 | ||
|
|
9aeb21d0c8 | ||
|
|
000135a3d4 | ||
|
|
260574719e | ||
|
|
ead1ce2742 | ||
|
|
991413608b | ||
|
|
fe74e0888b | ||
|
|
ceff8c92fd | ||
|
|
d8babf66ae | ||
|
|
8dbbcf5ffb | ||
|
|
fab9c4d7df | ||
|
|
2383051b74 | ||
|
|
d2d3294f02 | ||
|
|
62037301ee | ||
|
|
b860f3d31d | ||
|
|
b8c72c05bb | ||
|
|
f1eda7c4ec | ||
|
|
83eafe118e | ||
|
|
0b178b9449 | ||
|
|
1e7f374d6e | ||
|
|
fcb86023cb | ||
|
|
2dba591d37 | ||
|
|
385a2beaf9 | ||
|
|
cc2cc812cc | ||
|
|
a1c3c60fca | ||
|
|
bbbd693c1c | ||
|
|
f42f59a6b2 | ||
|
|
886cec797c | ||
|
|
2ea2c5c11e | ||
|
|
b0f59ebf79 | ||
|
|
35fee187b6 | ||
|
|
3e2fb85ae6 | ||
|
|
0cb5b571b0 | ||
|
|
3131cf335e | ||
|
|
785845a425 | ||
|
|
fa3ee91414 | ||
|
|
f8fa8bdda7 | ||
|
|
69d509a098 | ||
|
|
5a9fb3db1f | ||
|
|
8c8bfd4277 | ||
|
|
47f0507643 | ||
|
|
cb68c784fe | ||
|
|
b1d3d39a11 | ||
|
|
31d9fc5367 | ||
|
|
80a38732f9 | ||
|
|
c228d1a05a | ||
|
|
6bbba727a5 | ||
|
|
0149bc633c | ||
|
|
574198bceb | ||
|
|
edd3383f7d | ||
|
|
2384b37305 | ||
|
|
c71dd00cc3 | ||
|
|
bd18ce7326 | ||
|
|
9a85e1a811 | ||
|
|
ad1b77f280 | ||
|
|
99d89b207e | ||
|
|
66f60c64bd | ||
|
|
9f8abd585a | ||
|
|
1f937a7c76 | ||
|
|
9234e27a70 | ||
|
|
6ba476a3cd | ||
|
|
5f4c1f331d | ||
|
|
0ad348d714 | ||
|
|
7f1240e2eb | ||
|
|
6806c5e977 | ||
|
|
4dd428de41 | ||
|
|
77a9cf6398 | ||
|
|
c45ea53889 | ||
|
|
cab262c147 | ||
|
|
00fd1d2f26 | ||
|
|
7f55f421ab | ||
|
|
781c377588 | ||
|
|
7318c6007d | ||
|
|
e8288a2f63 | ||
|
|
afb067ce50 | ||
|
|
e36cc5641c | ||
|
|
a98eb60a27 | ||
|
|
49bb095152 | ||
|
|
4d3ddeae8d | ||
|
|
e5affb16c7 | ||
|
|
770d6dd8e2 |
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -10,8 +10,10 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_compnay_address()
|
||||
super().validate()
|
||||
self.update_company_address()
|
||||
|
||||
if hasattr(super(), "validate"):
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
@@ -20,7 +22,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super().link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
def update_company_address(self):
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
@@ -38,6 +40,10 @@ class ERPNextAddress(Address):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
|
||||
if hasattr(super(), "on_update"):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
|
||||
@@ -46,7 +46,8 @@ def validate_service_stop_date(doc):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
||||
and item.service_stop_date
|
||||
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
|
||||
):
|
||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"reporting_currency_exchange_rate",
|
||||
"debit_in_reporting_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
@@ -124,12 +127,30 @@
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:56.710541",
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
@@ -158,7 +179,8 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import cint, cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
@@ -26,12 +29,15 @@ class AccountClosingBalance(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
is_period_closing_voucher_entry: DF.Check
|
||||
period_closing_voucher: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -55,6 +61,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
@@ -144,3 +151,29 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
|
||||
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||
if not reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, closing_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||
|
||||
cle.update(
|
||||
{
|
||||
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -49,6 +50,13 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -62,10 +70,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -105,8 +114,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
@@ -116,6 +117,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"use_new_budget_controller"
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -598,12 +598,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_new_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use New Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -651,6 +645,12 @@
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -659,7 +659,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_new_budget_controller: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified": "2025-10-13 15:11:58.300836",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
|
||||
@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["against_voucher_type", "against_voucher_no"],
|
||||
)
|
||||
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["voucher_type", "voucher_no"],
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -95,6 +96,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 30
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -208,6 +209,7 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -250,7 +252,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -282,9 +284,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -72,35 +71,6 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -8,38 +8,4 @@ from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankAccount(IntegrationTestCase):
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
pass
|
||||
|
||||
@@ -155,8 +155,10 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -181,19 +183,20 @@ def get_payment_entries_for_bank_clearance(
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -214,9 +215,10 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.731207",
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
@@ -250,9 +252,10 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "customer",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "customer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ def start_auto_reconcile(
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry"],
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -666,7 +666,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -854,11 +854,14 @@ def get_je_matching_query(
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -871,11 +874,11 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -889,6 +892,9 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -111,20 +111,54 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""Preprocess MT940 content to fix statement number format issues.
|
||||
|
||||
The MT940 standard expects statement numbers to be maximum 5 digits,
|
||||
but some banks provide longer statement numbers that cause parsing errors.
|
||||
This function truncates statement numbers longer than 5 digits to the last 5 digits.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
|
||||
|
||||
def replace_statement_number(match):
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or "" # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or "" # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
if not is_mt940_format(content):
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
transactions = mt940.parse(content)
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
@@ -249,6 +283,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""Update bank transaction mapping database with template options."""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -260,6 +295,7 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""Add bank account information to data rows."""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -276,6 +312,7 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""Write processed data to CSV or Excel files."""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -285,11 +322,12 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension == "xlsx" or "xls":
|
||||
elif extension in ("xlsx", "xls"):
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""Write data to Excel file with formatting."""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
|
||||
@@ -1,10 +1,209 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
|
||||
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""Test boundary conditions for statement number length"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""Test with real-world MT940 content that was failing in production"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
@@ -223,7 +223,8 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -238,7 +239,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified": "2025-09-26 17:06:29.207673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.Data | None
|
||||
reference_number: DF.SmallText | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
|
||||
@@ -7,6 +7,9 @@ from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
IBAN_1 = "DE02000000003716541159"
|
||||
IBAN_2 = "DE02500105170137075030"
|
||||
|
||||
|
||||
class TestAutoMatchParty(IntegrationTestCase):
|
||||
@classmethod
|
||||
@@ -22,24 +25,24 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
create_supplier_for_match(iban=IBAN_1)
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
@@ -51,7 +54,7 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban="DE04000000003716545346",
|
||||
iban=IBAN_2,
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
@@ -23,8 +23,8 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
|
||||
if (!value) {
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,8 +145,10 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
return
|
||||
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
if not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
@@ -134,7 +134,8 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = self.get_accounts_data()
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
self.append("accounts", acc)
|
||||
if acc.get("gain_loss"):
|
||||
self.append("accounts", acc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self):
|
||||
|
||||
@@ -29,14 +29,17 @@
|
||||
"against_voucher",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"reporting_currency_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"debit_in_reporting_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
@@ -353,13 +356,31 @@
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-21 15:29:11.221890",
|
||||
"modified": "2025-08-22 12:57:17.750252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -390,8 +411,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ from erpnext.accounts.party import (
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@@ -42,9 +43,11 @@ class GLEntry(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
credit_in_transaction_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
debit_in_transaction_currency: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -57,6 +60,7 @@ class GLEntry(Document):
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
remarks: DF.Text | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
to_rename: DF.Check
|
||||
transaction_currency: DF.Link | None
|
||||
transaction_date: DF.Date | None
|
||||
@@ -88,6 +92,8 @@ class GLEntry(Document):
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
|
||||
self.set_amount_in_reporting_currency()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
@@ -131,18 +137,20 @@ class GLEntry(Document):
|
||||
|
||||
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(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
|
||||
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (
|
||||
@@ -248,7 +256,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
@@ -292,6 +300,25 @@ class GLEntry(Document):
|
||||
if self.party_type and self.party:
|
||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||
|
||||
def set_amount_in_reporting_currency(self):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", self.company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
transaction_date = self.transaction_date or self.posting_date
|
||||
self.reporting_currency_exchange_rate = get_exchange_rate(
|
||||
default_currency, reporting_currency, transaction_date
|
||||
)
|
||||
if not self.reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, transaction_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
|
||||
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
|
||||
|
||||
def validate_and_set_fiscal_year(self):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
@@ -308,7 +335,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]
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -577,6 +578,14 @@
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -591,7 +600,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -193,8 +194,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -644,8 +645,11 @@ class JournalEntry(AccountsController):
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party):
|
||||
if (
|
||||
not (d.party_type and d.party) and not self.party_not_required
|
||||
): # skipping validation if party_not_required is passed via payroll entry
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -1237,6 +1241,11 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
@@ -1264,6 +1273,7 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
frappe.flags.party_not_required = False
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(IntegrationTestCase):
|
||||
@@ -591,6 +592,15 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified": "2025-09-29 13:01:48.916517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"help_section",
|
||||
"loyalty_program_help"
|
||||
],
|
||||
@@ -144,6 +145,12 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_4",
|
||||
"invoices"
|
||||
],
|
||||
@@ -63,6 +64,12 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
|
||||
@@ -74,6 +74,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
|
||||
|
||||
@@ -585,6 +585,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
}
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -607,6 +608,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.received_amount(frm);
|
||||
}
|
||||
}
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@@ -194,6 +195,12 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
@@ -392,6 +393,12 @@ class PaymentReconciliation(Document):
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||
)
|
||||
difference_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||
)
|
||||
difference_amount = 0
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
@@ -399,8 +406,14 @@ class PaymentReconciliation(Document):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_ref_rate = flt(
|
||||
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
allocated_amount_in_inv_rate = flt(
|
||||
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
|
||||
@@ -228,7 +228,8 @@
|
||||
"fetch_from": "bank_account.iban",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "IBAN"
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.branch_code",
|
||||
@@ -458,11 +459,12 @@
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-04 05:39:32.448857",
|
||||
"modified": "2025-08-29 11:52:48.555415",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -497,8 +499,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,13 @@ class PaymentRequest(Document):
|
||||
|
||||
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
if (
|
||||
flt(
|
||||
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
|
||||
get_currency_precision(),
|
||||
)
|
||||
> ref_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
|
||||
@@ -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",
|
||||
@@ -172,12 +177,50 @@
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"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": "2025-03-11 11:06:51.792982",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -189,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,27 @@ class PaymentSchedule(Document):
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
credit_days: DF.Int
|
||||
credit_months: DF.Int
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
discount_date: DF.Date | None
|
||||
discount_type: DF.Literal["Percentage", "Amount"]
|
||||
discount_validity: DF.Int
|
||||
discount_validity_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
discounted_amount: DF.Currency
|
||||
due_date: DF.Date
|
||||
due_date_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
invoice_portion: DF.Percent
|
||||
mode_of_payment: DF.Link | None
|
||||
outstanding: DF.Currency
|
||||
|
||||
@@ -162,4 +162,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,17 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if previous_fiscal_year_closed:
|
||||
return
|
||||
|
||||
gle_exists_in_previous_year = frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
@@ -86,16 +97,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if not gle_exists_in_previous_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if not previous_fiscal_year_closed:
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def block_if_future_closing_voucher_exists(self):
|
||||
future_closing_voucher = self.get_future_closing_voucher()
|
||||
|
||||
@@ -452,7 +452,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if pricing_rule.get("coupon_code_based") == 1:
|
||||
if not args.coupon_code:
|
||||
continue
|
||||
coupon_code = frappe.db.get_value(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
|
||||
<div>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
@@ -34,11 +34,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
<th style="width: 15%">{{ _("Reference") }}</th>
|
||||
<th style="width: 25%">{{ _("Remarks") }}</th>
|
||||
<th style="width: 20%">{{ _("Reference") }}</th>
|
||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||
{% if filters.show_remarks %}
|
||||
<th style="width: 20%">{{ _("Remarks") }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -47,36 +49,51 @@
|
||||
{% if(row.posting_date) %}
|
||||
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
|
||||
<td>{{ row.voucher_type }}
|
||||
<br>{{ row.voucher_no }}</td>
|
||||
<td>
|
||||
{% if not (filters.party or filters.account) %}
|
||||
<br>{{ row.voucher_no }}
|
||||
{% if not (filters.party or filters.account) %}
|
||||
{{ row.party or row.account }}
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<br>{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% if filters.show_remarks %}
|
||||
<td>
|
||||
{% if row.remarks %}
|
||||
{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
||||
<td>
|
||||
<b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% if filters.show_remarks %}
|
||||
<td>
|
||||
{% if row.remarks %}
|
||||
{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -83,6 +83,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
@@ -106,6 +116,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
customer_collection: function (frm) {
|
||||
frm.set_value("collection_name", "");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-05-22 16:46:18.712954",
|
||||
"doctype": "DocType",
|
||||
@@ -38,6 +39,7 @@
|
||||
"column_break_17",
|
||||
"customers",
|
||||
"preferences",
|
||||
"print_format",
|
||||
"orientation",
|
||||
"include_break",
|
||||
"include_ageing",
|
||||
@@ -68,7 +70,7 @@
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Weekly\nMonthly\nQuarterly"
|
||||
"options": "Daily\nWeekly\nBiweekly\nMonthly\nQuarterly"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@@ -406,10 +408,16 @@
|
||||
"fieldname": "show_future_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Future Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-08-29 00:20:08.088189",
|
||||
"modified": "2025-10-07 12:19:20.719898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_months, format_date, getdate, today
|
||||
from frappe.utils import add_days, add_months, add_to_date, format_date, getdate, today
|
||||
from frappe.utils.jinja import validate_template
|
||||
from frappe.utils.pdf import get_pdf
|
||||
from frappe.www.printview import get_print_style
|
||||
@@ -55,7 +55,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
enable_auto_email: DF.Check
|
||||
filter_duration: DF.Int
|
||||
finance_book: DF.Link | None
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
frequency: DF.Literal["Daily", "Weekly", "Biweekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
@@ -67,6 +67,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
pdf_name: DF.Data | None
|
||||
posting_date: DF.Date | None
|
||||
primary_mandatory: DF.Check
|
||||
print_format: DF.Link | None
|
||||
project: DF.TableMultiSelect[PSOAProject]
|
||||
report: DF.Literal["General Ledger", "Accounts Receivable"]
|
||||
sales_partner: DF.Link | None
|
||||
@@ -109,6 +110,25 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
if self.print_format:
|
||||
pf = frappe.db.get_value(
|
||||
"Print Format",
|
||||
self.print_format,
|
||||
["print_format_type", "print_format_for", "report", "disabled"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not pf:
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Selected Print Format does not exist."))
|
||||
if pf.print_format_type != "Jinja":
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Print Format Type should be Jinja."))
|
||||
if pf.print_format_for != "Report" or pf.report != self.report or pf.disabled:
|
||||
frappe.throw(
|
||||
title=_("Invalid Print Format"),
|
||||
msg=_(
|
||||
"Print Format must be an enabled Report Print Format matching the selected Report."
|
||||
),
|
||||
)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
@@ -290,6 +310,10 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.print_format:
|
||||
custom_html, custom_css = frappe.db.get_value("Print Format", doc.print_format, ["html", "css"])
|
||||
template_path = f"<style>{custom_css}</style> {custom_html}"
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
@@ -531,8 +555,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
new_to_date = getdate(posting_date or today())
|
||||
if doc.frequency == "Weekly":
|
||||
new_to_date = add_days(new_to_date, 7)
|
||||
if doc.frequency in ("Daily", "Weekly", "Biweekly"):
|
||||
frequency = {"Daily": 1, "Weekly": 7, "Biweekly": 14}
|
||||
new_to_date = add_days(new_to_date, frequency[doc.frequency])
|
||||
else:
|
||||
new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3)
|
||||
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h2 class="text-center" style="margin-top:0">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<h4 class="text-center">
|
||||
{{ filters.customer_name }}
|
||||
</h4>
|
||||
@@ -159,7 +159,7 @@
|
||||
{% else %}
|
||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
{% if not(filters.show_future_payments) and filters.show_remarks %}
|
||||
<th style="width: 20%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks") }}
|
||||
@@ -228,7 +228,7 @@
|
||||
<td>{{ data[i]["sales_person"] }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if not (filters.show_future_payments) %}
|
||||
{% if not (filters.show_future_payments) and filters.show_remarks %}
|
||||
<td>
|
||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||
{{ data[i]["party"] }}
|
||||
@@ -327,12 +327,23 @@
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% if (filters.show_future_payments) or filters.show_remarks %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% else %}
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
|
||||
@@ -81,6 +81,7 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||
)
|
||||
|
||||
send_emails(process_soa.name, from_scheduler=True)
|
||||
process_soa.load_from_db()
|
||||
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
|
||||
@@ -884,6 +884,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
self.set_gl_entry_for_purchase_expense(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
@@ -1228,7 +1229,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||
self.update_net_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
@@ -1290,7 +1291,7 @@ class PurchaseInvoice(BuyingController):
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
def update_net_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={
|
||||
@@ -1306,7 +1307,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2147,19 +2147,16 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@IntegrationTestCase.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",
|
||||
@@ -2185,7 +2182,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
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",
|
||||
@@ -2633,6 +2629,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1}
|
||||
)
|
||||
def test_pr_status_rate_adjusted_from_pi(self):
|
||||
pr = make_purchase_receipt(qty=5, rate=100)
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Inital check
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.items[0].rate = 80
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Test 1 : Adjustment amount is negative
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.items[0].rate = 120
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Test 2 : Adjustment amount is positive
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_9",
|
||||
"account_currency",
|
||||
"net_amount",
|
||||
@@ -214,6 +215,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -232,7 +232,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
default_expense_account = company.service_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.tax_id",
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"hide_days": 1,
|
||||
@@ -2241,7 +2242,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-04 19:20:28.732039",
|
||||
"modified": "2025-09-09 14:48:59.472826",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -279,6 +279,59 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def before_print(self, settings=None):
|
||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
||||
|
||||
super().before_print(settings)
|
||||
|
||||
company_details = frappe.get_value(
|
||||
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
||||
)
|
||||
|
||||
required_fields = [
|
||||
company_details.get("company_logo"),
|
||||
company_details.get("phone_no"),
|
||||
company_details.get("email"),
|
||||
]
|
||||
|
||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
address_display_list = get_address_display_list("Company", self.company)
|
||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
||||
|
||||
required_fields.append(self.company_address)
|
||||
required_fields.append(address_line)
|
||||
|
||||
if not all(required_fields):
|
||||
frappe.publish_realtime(
|
||||
"sales_invoice_before_print",
|
||||
{
|
||||
"company_logo": company_details.get("company_logo"),
|
||||
"website": company_details.get("website"),
|
||||
"phone_no": company_details.get("phone_no"),
|
||||
"email": company_details.get("email"),
|
||||
"address_line": address_line,
|
||||
"company": self.company,
|
||||
"company_address": self.company_address,
|
||||
"name": self.name,
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
@@ -2802,6 +2855,59 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_company_master_details(name, company, details):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if isinstance(details, str):
|
||||
details = frappe.parse_json(details)
|
||||
|
||||
if details.get("email"):
|
||||
validate_email_address(details.get("email"), throw=True)
|
||||
|
||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
||||
|
||||
if company_fields_to_update:
|
||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
||||
|
||||
company_address = details.get("company_address")
|
||||
if details.get("address_line1"):
|
||||
address_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": details.get("address_title"),
|
||||
"address_type": details.get("address_type"),
|
||||
"address_line1": details.get("address_line1"),
|
||||
"address_line2": details.get("address_line2"),
|
||||
"city": details.get("city"),
|
||||
"state": details.get("state"),
|
||||
"pincode": details.get("pincode"),
|
||||
"country": details.get("country"),
|
||||
"is_your_company_address": 1,
|
||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
||||
}
|
||||
)
|
||||
address_doc.insert()
|
||||
company_address = address_doc.name
|
||||
|
||||
if company_address:
|
||||
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
|
||||
if not company_address_display or details.get("address_line1"):
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
|
||||
(
|
||||
frappe.qb.update(SalesInvoice)
|
||||
.set(SalesInvoice.company_address, company_address)
|
||||
.set(SalesInvoice.company_address_display, get_address_display(company_address))
|
||||
.where(SalesInvoice.name == name)
|
||||
).run()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -191,6 +192,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"shipping_amount_section",
|
||||
"calculate_based_on",
|
||||
"column_break_8",
|
||||
@@ -138,6 +139,12 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
|
||||
@@ -483,18 +483,23 @@ class Subscription(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]:
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
|
||||
"""
|
||||
Returns the `Item`s linked to `Subscription Plan`
|
||||
"""
|
||||
if prorate is None:
|
||||
prorate = False
|
||||
|
||||
prorate_factor = 1
|
||||
if prorate:
|
||||
prorate_factor = get_prorata_factor(
|
||||
self.current_invoice_end,
|
||||
self.current_invoice_start,
|
||||
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
|
||||
cint(
|
||||
self.generate_invoice_at
|
||||
in [
|
||||
"Beginning of the current subscription period",
|
||||
"Days before the current subscription period",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
items = []
|
||||
@@ -511,33 +516,19 @@ class Subscription(Document):
|
||||
|
||||
deferred = frappe.db.get_value("Item", item_code, deferred_field)
|
||||
|
||||
if not prorate:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
else:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
|
||||
if deferred:
|
||||
item.update(
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
add_years,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
@@ -555,6 +556,33 @@ class TestSubscription(IntegrationTestCase):
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
def test_invoice_generation_days_before_subscription_period_with_prorate(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
settings.prorate = 1
|
||||
settings.save()
|
||||
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 5",
|
||||
cost=1000,
|
||||
billing_interval="Year",
|
||||
billing_interval_count=1,
|
||||
currency="INR",
|
||||
)
|
||||
|
||||
start_date = add_days(nowdate(), 2)
|
||||
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
generate_invoice_at="Days before the current subscription period",
|
||||
generate_new_invoices_past_due_date=1,
|
||||
number_of_days=2,
|
||||
plans=[{"plan": "_Test Plan Name 5", "qty": 1}],
|
||||
)
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -35,7 +35,7 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if (
|
||||
frappe.get_single_value("Accounts Settings", "use_new_budget_controller")
|
||||
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
||||
and gl_map[0].voucher_type != "Period Closing Voucher"
|
||||
):
|
||||
bud_val = BudgetValidation(gl_map=gl_map)
|
||||
@@ -159,6 +159,7 @@ def validate_accounting_period(gl_map):
|
||||
WHERE
|
||||
ap.name = cd.parent
|
||||
AND ap.company = %(company)s
|
||||
AND ap.disabled = 0
|
||||
AND cd.closed = 1
|
||||
AND cd.document_type = %(voucher_type)s
|
||||
AND %(date)s between ap.start_date and ap.end_date
|
||||
|
||||
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<style>
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
padding-right: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.letter-head td{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.invoice-header {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.logo-container {
|
||||
width: 90px;
|
||||
display: block;
|
||||
}
|
||||
.logo-container img {
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.company-details {
|
||||
width: 40%;
|
||||
align-content: center;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #171717;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invoice-info-cell {
|
||||
float: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
.invoice-info {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.invoice-label {
|
||||
color: #7C7C7C;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="invoice-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-cell" style="vertical-align: middle !important;">
|
||||
<div class="logo-container">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
|
||||
{% if company_logo %}
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="company-details">
|
||||
<div class="company-name">
|
||||
{{ doc.company }}
|
||||
</div>
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
|
||||
{{ company_address.get("address_line1") or "" }}<br>
|
||||
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
|
||||
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="invoice-info-cell">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Invoice:") }}</span>
|
||||
<span>{{ doc.name }}</span>
|
||||
</div>
|
||||
{% if company_details.website %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||
<span>{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||
<span>{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||
<span>{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<style>
|
||||
.print-format-preview {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
background: #f8f8f8;
|
||||
padding: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.letterhead-container {
|
||||
width: 100%;
|
||||
}
|
||||
.letterhead-container .other-details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.logo-address {
|
||||
width: 65%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.letter-head .logo {
|
||||
width: 90px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.letter-head .logo img {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: #171717;
|
||||
font-weight: bold;
|
||||
line-height: 23px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.company-address {
|
||||
color: #171717;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invoice-number {
|
||||
color: #7c7c7c;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
color: #7c7c7c;
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
color: #171717;
|
||||
display: inline-block;
|
||||
}
|
||||
.letterhead-container td {
|
||||
padding: 0px !important;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="letterhead-container">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-address">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
|
||||
company_logo %}
|
||||
<div class="logo">
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="company-name">{{ doc.company }}</div>
|
||||
<div class="company-address">
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
{{ company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
|
||||
{{ company_address.city or "" }}, {{ company_address.state or "" }}
|
||||
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="vertical-align: top">
|
||||
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
||||
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
|
||||
<div class="invoice-number">{{ doc.name }}</div>
|
||||
<br />
|
||||
</div>
|
||||
<div style="text-align: left; float: right" class="other-details">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
{% if company_details.website %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Website:") }}</span
|
||||
><span class="contact-value">{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Email:") }}</span
|
||||
><span class="contact-value">{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Contact:") }}</span
|
||||
><span class="contact-value">{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -164,6 +164,12 @@
|
||||
{% } %}
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<tbody>
|
||||
{% for(var i=0, l=data.length; i<l; i++) { %}
|
||||
<tr>
|
||||
|
||||
@@ -976,6 +976,7 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.account_type == "Receivable":
|
||||
self.add_customer_filters()
|
||||
self.exclude_employee_transaction()
|
||||
|
||||
elif self.account_type == "Payable":
|
||||
self.add_supplier_filters()
|
||||
@@ -1055,6 +1056,9 @@ class ReceivablePayableReport:
|
||||
)
|
||||
)
|
||||
|
||||
def exclude_employee_transaction(self):
|
||||
self.qb_selection_filter.append(self.ple.party_type != "Employee")
|
||||
|
||||
def add_supplier_filters(self):
|
||||
supplier = qb.DocType("Supplier")
|
||||
if self.filters.get("supplier_group"):
|
||||
@@ -1268,7 +1272,7 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
ranges = [*self.ranges, "Above"]
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
|
||||
@@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_("Difference"), fieldname="diff")
|
||||
|
||||
self.setup_ageing_columns()
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
self.add_column(label=_("Total Amount Due"), fieldname="total_due")
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
|
||||
@@ -103,7 +103,7 @@ def get_data(filters):
|
||||
"depreciation_amount": d.debit,
|
||||
"depreciation_date": d.posting_date,
|
||||
"value_after_depreciation": (
|
||||
flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
flt(row.net_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
),
|
||||
"depreciation_entry": d.voucher_no,
|
||||
}
|
||||
@@ -119,7 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
"status",
|
||||
@@ -151,7 +151,7 @@ def get_columns():
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -95,7 +95,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -103,7 +103,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -114,7 +114,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -125,7 +125,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -354,10 +354,10 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT a.name,
|
||||
SELECT a.name, a.asset_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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -365,7 +365,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -373,7 +373,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -384,7 +384,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -395,7 +395,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
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
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -583,6 +583,14 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Name"),
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
|
||||
@@ -5,30 +5,35 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
|
||||
get_report_summary as get_pl_summary,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -564,7 +565,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
|
||||
row[company] = flt(d.get(company, 0.0), 3)
|
||||
|
||||
if abs(row[company]) >= 0.005:
|
||||
if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(row[company])
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{% include "accounts/report/financial_statements.html" %}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Trial Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
on_change: function (query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
{
|
||||
fieldname: "presentation_currency",
|
||||
label: __("Currency"),
|
||||
fieldtype: "Select",
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_unclosed_fy_pl_balances",
|
||||
label: __("Show unclosed fiscal year's P&L balances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_group_accounts",
|
||||
label: __("Show Group Accounts"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
formatter: erpnext.financial_statements.formatter,
|
||||
tree: true,
|
||||
name_field: "account",
|
||||
parent_field: "parent_account",
|
||||
initial_depth: 3,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2025-09-03 00:53:22.230646",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-09-03 00:53:22.230646",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Trial Balance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Consolidated Trial Balance",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, getdate, now_datetime, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.account import get_root_company
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
accumulate_values_into_parents,
|
||||
calculate_values,
|
||||
get_opening_balances,
|
||||
hide_group_accounts,
|
||||
prepare_opening_closing,
|
||||
value_fields,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
validate_filters as tb_validate_filters,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_rate_as_at
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
"""Return columns and data for the report.
|
||||
|
||||
This is the main entry point for the report. It accepts the filters as a
|
||||
dictionary and should return columns and data. It is called by the framework
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
validate_filters(filters=filters)
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_companies(filters)
|
||||
filters.show_net_values = True
|
||||
tb_validate_filters(filters)
|
||||
|
||||
|
||||
def validate_companies(filters):
|
||||
if not filters.company:
|
||||
return
|
||||
|
||||
root_company = get_root_company(filters.company[0])
|
||||
root_company = root_company[0] if root_company else filters.company[0]
|
||||
|
||||
lft, rgt = frappe.db.get_value("Company", root_company, fieldname=["lft", "rgt"])
|
||||
|
||||
company_subtree = frappe.db.get_all(
|
||||
"Company",
|
||||
{"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
"name",
|
||||
order_by="lft",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for company in filters.company:
|
||||
if company not in company_subtree:
|
||||
frappe.throw(
|
||||
_("Consolidated Trial Balance can be generated for Companies having same root Company.")
|
||||
)
|
||||
|
||||
sort_companies(filters)
|
||||
|
||||
|
||||
def sort_companies(filters):
|
||||
companies = frappe.db.get_all(
|
||||
"Company", {"name": ["in", filters.company]}, "name", order_by="lft", pluck="name"
|
||||
)
|
||||
filters.company = companies
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
data = []
|
||||
if filters.company:
|
||||
reporting_currency, ignore_reporting_currency = get_reporting_currency(filters)
|
||||
else:
|
||||
return data
|
||||
|
||||
for company in filters.company:
|
||||
company_filter = frappe._dict(filters)
|
||||
company_filter.company = company
|
||||
|
||||
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
|
||||
consolidate_trial_balance_data(data, tb_data)
|
||||
|
||||
for d in data:
|
||||
prepare_opening_closing(d)
|
||||
|
||||
total_row = calculate_total_row(data, reporting_currency)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
update_to_presentation_currency(
|
||||
data,
|
||||
reporting_currency,
|
||||
filters.get("presentation_currency"),
|
||||
filters.get("to_date"),
|
||||
ignore_reporting_currency,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
default_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
opening_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
filters.get("from_date"),
|
||||
)
|
||||
current_date = (
|
||||
filters.get("to_date") if getdate(filters.get("to_date")) <= now_datetime().date() else nowdate()
|
||||
)
|
||||
closing_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
current_date,
|
||||
)
|
||||
|
||||
if not (opening_exchange_rate and closing_exchange_rate):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}.",
|
||||
).format(default_currency, reporting_currency, current_date)
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(
|
||||
filters,
|
||||
ignore_is_opening,
|
||||
exchange_rate=opening_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
group_by_account=True,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
exchange_rate=closing_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
# Prepare opening closing for group account
|
||||
if parent_children_map.get(d.account) and filters.get("show_net_values"):
|
||||
prepare_opening_closing(d)
|
||||
|
||||
has_value = False
|
||||
row = {
|
||||
"account": d.name,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": reporting_currency,
|
||||
"is_group_account": d.is_group,
|
||||
"acc_name": d.account_name,
|
||||
"acc_number": d.account_number,
|
||||
"account_name": (
|
||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||
),
|
||||
"root_type": d.root_type,
|
||||
"account_type": d.account_type,
|
||||
}
|
||||
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= get_zero_cutoff(reporting_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, reporting_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": reporting_currency,
|
||||
}
|
||||
|
||||
for d in data:
|
||||
if not d.get("parent_account"):
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
calculate_foreign_currency_translation_reserve(total_row, data)
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def calculate_foreign_currency_translation_reserve(total_row, data):
|
||||
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
|
||||
dr_cr_diff = total_row["debit"] - total_row["credit"]
|
||||
|
||||
idx = get_fctr_root_row_index(data)
|
||||
|
||||
fctr_row = {
|
||||
"account": _("Foreign Currency Translation Reserve"),
|
||||
"account_name": _("Foreign Currency Translation Reserve"),
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": abs(opening_dr_cr_diff) if opening_dr_cr_diff < 0 else 0.0,
|
||||
"opening_credit": abs(opening_dr_cr_diff) if opening_dr_cr_diff > 0 else 0.0,
|
||||
"debit": abs(dr_cr_diff) if dr_cr_diff < 0 else 0.0,
|
||||
"credit": abs(dr_cr_diff) if dr_cr_diff > 0 else 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"root_type": data[idx].get("root_type"),
|
||||
"account_type": "Equity",
|
||||
"parent_account": data[idx].get("account"),
|
||||
"indent": data[idx].get("indent") + 1,
|
||||
"has_value": True,
|
||||
"currency": total_row.get("currency"),
|
||||
}
|
||||
|
||||
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
||||
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
||||
|
||||
prepare_opening_closing(fctr_row)
|
||||
|
||||
data.insert(idx + 1, fctr_row)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += fctr_row[field]
|
||||
|
||||
|
||||
def get_fctr_root_row_index(data):
|
||||
"""
|
||||
Returns: index, root_type, parent_account
|
||||
"""
|
||||
liabilities_idx, equity_idx, tmp_idx = -1, -1, 0
|
||||
for d in data:
|
||||
if liabilities_idx == -1 and d.get("root_type") == "Liability":
|
||||
liabilities_idx = tmp_idx
|
||||
|
||||
if equity_idx == -1 and d.get("root_type") == "Equity":
|
||||
equity_idx = tmp_idx
|
||||
|
||||
tmp_idx += 1
|
||||
|
||||
if equity_idx == -1:
|
||||
return liabilities_idx
|
||||
|
||||
return equity_idx
|
||||
|
||||
|
||||
def consolidate_trial_balance_data(data, tb_data):
|
||||
if not data:
|
||||
data.extend(list(tb_data))
|
||||
return
|
||||
|
||||
for entry in tb_data:
|
||||
if entry:
|
||||
consolidate_gle_data(data, entry, tb_data)
|
||||
|
||||
|
||||
def get_reporting_currency(filters):
|
||||
reporting_currency = frappe.get_cached_value("Company", filters.company[0], "reporting_currency")
|
||||
default_currency = None
|
||||
for company in filters.company:
|
||||
company_default_currency = erpnext.get_company_currency(company)
|
||||
if not default_currency:
|
||||
default_currency = company_default_currency
|
||||
|
||||
if company_default_currency != default_currency:
|
||||
return (reporting_currency, False)
|
||||
|
||||
return (default_currency, True)
|
||||
|
||||
|
||||
def consolidate_gle_data(data, entry, tb_data):
|
||||
entry_gle_exists = False
|
||||
for gle in data:
|
||||
if gle and gle["account_name"] == entry["account_name"]:
|
||||
entry_gle_exists = True
|
||||
gle["closing_credit"] += entry["closing_credit"]
|
||||
gle["closing_debit"] += entry["closing_debit"]
|
||||
gle["credit"] += entry["credit"]
|
||||
gle["debit"] += entry["debit"]
|
||||
gle["opening_credit"] += entry["opening_credit"]
|
||||
gle["opening_debit"] += entry["opening_debit"]
|
||||
gle["has_value"] = 1
|
||||
|
||||
if not entry_gle_exists:
|
||||
entry_parent_account = next(
|
||||
(d for d in tb_data if d.get("account") == entry.get("parent_account")), None
|
||||
)
|
||||
parent_account_in_data = None
|
||||
if entry_parent_account:
|
||||
parent_account_in_data = next(
|
||||
(d for d in data if d and d.get("account_name") == entry_parent_account.get("account_name")),
|
||||
None,
|
||||
)
|
||||
if parent_account_in_data:
|
||||
entry["parent_account"] = parent_account_in_data.get("account")
|
||||
entry["indent"] = (parent_account_in_data.get("indent") or 0) + 1
|
||||
data.insert(data.index(parent_account_in_data) + 1, entry)
|
||||
else:
|
||||
entry["parent_account"] = None
|
||||
entry["indent"] = 0
|
||||
data.append(entry)
|
||||
|
||||
|
||||
def update_to_presentation_currency(data, from_currency, to_currency, date, ignore_reporting_currency):
|
||||
if from_currency == to_currency:
|
||||
return
|
||||
|
||||
exchange_rate = get_rate_as_at(date, from_currency, to_currency)
|
||||
|
||||
for d in data:
|
||||
if not ignore_reporting_currency:
|
||||
for field in value_fields:
|
||||
if d.get(field):
|
||||
d[field] = d[field] * flt(exchange_rate)
|
||||
d.update(currency=to_currency)
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Data",
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_name",
|
||||
"label": _("Account Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_number",
|
||||
"label": _("Account Number"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_debit",
|
||||
"label": _("Opening (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_credit",
|
||||
"label": _("Opening (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_debit",
|
||||
"label": _("Closing (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_credit",
|
||||
"label": _("Closing (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.consolidated_trial_balance.consolidated_trial_balance import execute
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class ForeignCurrencyTranslationReserveNotFoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TestConsolidatedTrialBalance(IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import create_company
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
# Group Company
|
||||
create_company(company_name="Parent Group Company India", is_group=1)
|
||||
|
||||
create_company(company_name="Child Company India", parent_company="Parent Group Company India")
|
||||
|
||||
# Child Company with different currency
|
||||
create_company(
|
||||
company_name="Child Company US",
|
||||
country="United States",
|
||||
currency="USD",
|
||||
parent_company="Parent Group Company India",
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Parent Group Company India",
|
||||
acc1="Marketing Expenses - PGCI",
|
||||
acc2="Cash - PGCI",
|
||||
amount=100000,
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company India", acc1="Cash - CCI", acc2="Secured Loans - CCI", amount=50000
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company US", acc1="Marketing Expenses - CCU", acc2="Cash - CCU", amount=1000
|
||||
)
|
||||
|
||||
cls.fiscal_year = get_fiscal_year(today(), company="Parent Group Company India")[0]
|
||||
|
||||
def test_single_company_report(self):
|
||||
filters = frappe._dict({"company": ["Parent Group Company India"], "fiscal_year": self.fiscal_year})
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertEqual(total_row["closing_credit"], 100000)
|
||||
|
||||
def test_child_company_report_with_same_default_currency_as_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company India"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
|
||||
def test_child_company_with_different_default_currency_from_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company US"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR")
|
||||
|
||||
fctr = [d for d in report[1] if d.get("account") == _("Foreign Currency Translation Reserve")]
|
||||
|
||||
if not fctr:
|
||||
raise ForeignCurrencyTranslationReserveNotFoundError
|
||||
|
||||
ccu_total_credit = 1000 * flt(exchange_rate)
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertNotEqual(total_row["closing_credit"], ccu_total_credit)
|
||||
|
||||
self.assertEqual(total_row["closing_credit"], flt(100000 + ccu_total_credit))
|
||||
|
||||
|
||||
def create_journal_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = args.posting_date or today()
|
||||
je.company = args.company
|
||||
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": args.acc1,
|
||||
"debit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": args.acc2,
|
||||
"credit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
je.save()
|
||||
je.submit()
|
||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
filter_out_zero_value_rows,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
||||
for dimension in dimension_list:
|
||||
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||
|
||||
if abs(row[frappe.scrub(dimension)]) >= 0.005:
|
||||
if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
|
||||
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
|
||||
|
||||
if (heading_ratios.includes(value)) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
|
||||
column.fieldtype = "Data";
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
|
||||
|
||||
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Liquidity Ratios"})
|
||||
data.append({"ratio": _("Liquidity Ratios")})
|
||||
|
||||
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||||
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
|
||||
|
||||
for d in ratio_data:
|
||||
row = {
|
||||
@@ -165,13 +165,13 @@ def add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Solvency Ratios"})
|
||||
data.append({"ratio": _("Solvency Ratios")})
|
||||
|
||||
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||||
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||||
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||||
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
|
||||
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
|
||||
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
|
||||
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
|
||||
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
@@ -195,7 +195,7 @@ def add_solvency_ratios(
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Turnover Ratios"})
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||||
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||||
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||||
["Inventory Turnover Ratio", cogs, avg_stock],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
]
|
||||
for ratio in ratio_data:
|
||||
row = {
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
</h5>
|
||||
{% } %}
|
||||
<hr>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff
|
||||
|
||||
|
||||
def get_period_list(
|
||||
@@ -212,7 +212,7 @@ def get_data(
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out and total:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
@@ -306,7 +306,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
row[period.key] = flt(d.get(period.key, 0.0), 3)
|
||||
|
||||
if abs(row[period.key]) >= 0.005:
|
||||
if abs(row[period.key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(row[period.key])
|
||||
@@ -325,18 +325,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
|
||||
def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
|
||||
def get_all_parents(account, parent_children_map):
|
||||
for parent, children in parent_children_map.items():
|
||||
for child in children:
|
||||
if child["name"] == account and parent:
|
||||
accounts_to_show.add(parent)
|
||||
get_all_parents(parent, parent_children_map)
|
||||
|
||||
data_with_value = []
|
||||
accounts_to_show = set()
|
||||
|
||||
for d in data:
|
||||
if show_zero_values or d.get("has_value"):
|
||||
accounts_to_show.add(d.get("account"))
|
||||
get_all_parents(d.get("account"), parent_children_map)
|
||||
|
||||
for d in data:
|
||||
if d.get("account") in accounts_to_show:
|
||||
data_with_value.append(d)
|
||||
else:
|
||||
# show group with zero balance, if there are balances against child
|
||||
children = [child.name for child in parent_children_map.get(d.get("account")) or []]
|
||||
if children:
|
||||
for row in data:
|
||||
if row.get("account") in children and row.get("has_value"):
|
||||
data_with_value.append(d)
|
||||
break
|
||||
|
||||
return data_with_value
|
||||
|
||||
@@ -437,6 +443,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
@@ -467,6 +474,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
@@ -482,9 +490,10 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
for entry in gl_entries:
|
||||
@@ -505,6 +514,7 @@ def get_accounting_entries(
|
||||
period_closing_voucher=None,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
gl_entry = frappe.qb.DocType(doctype)
|
||||
query = (
|
||||
@@ -524,12 +534,23 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
if not ignore_reporting_currency:
|
||||
query = query.select(
|
||||
gl_entry.debit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.debit_in_reporting_currency).as_("debit_in_reporting_currency"),
|
||||
gl_entry.credit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.credit_in_reporting_currency).as_("credit_in_reporting_currency"),
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.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)
|
||||
query = query.force_index("posting_date_company_index")
|
||||
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
|
||||
@@ -75,6 +75,12 @@
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table style="width:100%; font-size: 11px">
|
||||
<thead>
|
||||
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
|
||||
|
||||
@@ -667,7 +667,7 @@ def get_columns(filters):
|
||||
"options": "GL Entry",
|
||||
"hidden": 1,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
{
|
||||
"label": _("Account"),
|
||||
"fieldname": "account",
|
||||
|
||||
@@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name":
|
||||
del columns[4:6]
|
||||
else:
|
||||
del columns[5:7]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
@@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
"width": 120,
|
||||
},
|
||||
"posting_time": {
|
||||
"label": _("Posting Time"),
|
||||
@@ -677,7 +682,9 @@ class GrossProfitGenerator:
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
@@ -395,7 +395,6 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
@@ -642,3 +641,42 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, 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)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 0.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
@@ -5,31 +5,36 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
filter_out_zero_value_rows,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
value_fields = ("income", "expense", "gross_profit_loss")
|
||||
|
||||
@@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= 0.005:
|
||||
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ def create_company(**args):
|
||||
"company_name": args.company_name or "Trial Balance Company",
|
||||
"country": args.country or "India",
|
||||
"default_currency": args.currency or "INR",
|
||||
"parent_company": args.get("parent_company"),
|
||||
"is_group": args.get("is_group"),
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
value_fields = (
|
||||
"opening_debit",
|
||||
@@ -134,15 +135,21 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters, ignore_is_opening):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
|
||||
def get_opening_balances(filters, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(
|
||||
filters, "Balance Sheet", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
pl_opening = get_rootwise_opening_balances(
|
||||
filters, "Profit and Loss", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
|
||||
balance_sheet_opening.update(pl_opening)
|
||||
return balance_sheet_opening
|
||||
|
||||
|
||||
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
def get_rootwise_opening_balances(
|
||||
filters, report_type, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True
|
||||
):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = ""
|
||||
@@ -167,6 +174,7 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
@@ -179,24 +187,41 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
start_date=start_date,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
else:
|
||||
gle = get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
|
||||
"GL Entry",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening.setdefault(
|
||||
d.account,
|
||||
{
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
},
|
||||
)
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
opening_dr_cr = {
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
}
|
||||
|
||||
opening.setdefault(d.account, opening_dr_cr)
|
||||
|
||||
if ignore_reporting_currency:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
|
||||
else:
|
||||
if d.get("report_type") == "Balance Sheet" and not (
|
||||
d.get("root_type") == "Equity" or d.get("account_type") == "Equity"
|
||||
):
|
||||
opening[d.account]["opening_debit"] += flt(d.debit) * flt(exchange_rate)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit) * flt(exchange_rate)
|
||||
else:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit_in_reporting_currency)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit_in_reporting_currency)
|
||||
|
||||
return opening
|
||||
|
||||
@@ -209,9 +234,10 @@ def get_opening_balance(
|
||||
period_closing_voucher=None,
|
||||
start_date=None,
|
||||
ignore_is_opening=0,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
closing_balance = frappe.qb.DocType(doctype)
|
||||
account = frappe.qb.DocType("Account")
|
||||
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
|
||||
|
||||
opening_balance = (
|
||||
frappe.qb.from_(closing_balance)
|
||||
@@ -223,17 +249,16 @@ def get_opening_balance(
|
||||
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
)
|
||||
.where(
|
||||
(closing_balance.company == filters.company)
|
||||
& (
|
||||
closing_balance.account.isin(
|
||||
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
|
||||
)
|
||||
)
|
||||
)
|
||||
.where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts)))
|
||||
.groupby(closing_balance.account)
|
||||
)
|
||||
|
||||
if not ignore_reporting_currency:
|
||||
opening_balance = opening_balance.select(
|
||||
Sum(closing_balance.debit_in_reporting_currency).as_("debit_in_reporting_currency"),
|
||||
Sum(closing_balance.credit_in_reporting_currency).as_("credit_in_reporting_currency"),
|
||||
)
|
||||
|
||||
if period_closing_voucher:
|
||||
opening_balance = opening_balance.where(
|
||||
closing_balance.period_closing_voucher == period_closing_voucher
|
||||
@@ -285,21 +310,24 @@ def get_opening_balance(
|
||||
if filters.project:
|
||||
opening_balance = opening_balance.where(closing_balance.project == filters.project)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
if frappe.db.count("Finance Book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
|
||||
)
|
||||
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
@@ -318,13 +346,21 @@ def get_opening_balance(
|
||||
|
||||
gle = opening_balance.run(as_dict=1)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gle, get_currency(filters))
|
||||
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
|
||||
def calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
show_net_values,
|
||||
ignore_is_opening=0,
|
||||
exchange_rate=None,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -343,8 +379,18 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
|
||||
for entry in gl_entries_by_account.get(d.name, []):
|
||||
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
if ignore_reporting_currency:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
else:
|
||||
if d.report_type == "Balance Sheet" and not (
|
||||
d.root_type == "Equity" or d.account_type == "Equity"
|
||||
):
|
||||
d["debit"] += flt(entry.debit) * flt(exchange_rate)
|
||||
d["credit"] += flt(entry.credit) * flt(exchange_rate)
|
||||
else:
|
||||
d["debit"] += flt(entry.debit_in_reporting_currency)
|
||||
d["credit"] += flt(entry.credit_in_reporting_currency)
|
||||
|
||||
d["closing_debit"] = d["opening_debit"] + d["debit"]
|
||||
d["closing_credit"] = d["opening_credit"] + d["credit"]
|
||||
@@ -412,7 +458,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= 0.005:
|
||||
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"mandatory": 1,
|
||||
"options": "Company",
|
||||
"wildcard_filter": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2019-01-17 17:20:42.374958",
|
||||
"modified": "2025-08-28 19:06:54.273322",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Trial Balance (Simple)",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance (Simple)",
|
||||
"report_type": "Query Report",
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.party import get_party_shipping_address
|
||||
from erpnext.accounts.utils import (
|
||||
get_future_stock_vouchers,
|
||||
get_voucherwise_gl_entries,
|
||||
get_zero_cutoff,
|
||||
sort_stock_vouchers_by_posting_date,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -157,6 +158,11 @@ class TestUtils(IntegrationTestCase):
|
||||
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
|
||||
frappe.db.set_default("supp_master_name", "Supplier Name")
|
||||
|
||||
def test_get_zero_cutoff(self):
|
||||
self.assertEqual(get_zero_cutoff(None), 0.005)
|
||||
self.assertEqual(get_zero_cutoff("EUR"), 0.005)
|
||||
self.assertEqual(get_zero_cutoff("BHD"), 0.0005)
|
||||
|
||||
|
||||
ADDRESS_RECORDS = [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user