mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-13 18:28:48 +00:00
Compare commits
563 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e01a9b84d | ||
|
|
911ec37d13 | ||
|
|
33273faccb | ||
|
|
bd9c84d3b7 | ||
|
|
2466e4c2fd | ||
|
|
44f798c3e1 | ||
|
|
20186fb8fa | ||
|
|
282f133500 | ||
|
|
f88dc263d5 | ||
|
|
7e0ff851ab | ||
|
|
f3cf28a4f4 | ||
|
|
ef5a5643dc | ||
|
|
925eb4449e | ||
|
|
815c6b4234 | ||
|
|
5cd39941ee | ||
|
|
eae5996c4c | ||
|
|
98bd5f20c4 | ||
|
|
bdb60a51c3 | ||
|
|
04b07a9e1c | ||
|
|
243bf542c4 | ||
|
|
0bb7c889ae | ||
|
|
cce1cc6674 | ||
|
|
c6c5587cc6 | ||
|
|
2560beb695 | ||
|
|
80f2fc8e64 | ||
|
|
47e1ed1eff | ||
|
|
3c8abe41e7 | ||
|
|
70e44a4df1 | ||
|
|
971e474640 | ||
|
|
2a17ecdc39 | ||
|
|
608eaaa482 | ||
|
|
6f80af0355 | ||
|
|
0c3bf2daf1 | ||
|
|
11fc560a88 | ||
|
|
0cd495a128 | ||
|
|
3a98660739 | ||
|
|
97c91c2217 | ||
|
|
a204abcaa5 | ||
|
|
3b57767d36 | ||
|
|
dc74912f5d | ||
|
|
d860a5b35d | ||
|
|
b00cc83ae6 | ||
|
|
408a09233b | ||
|
|
a5cc307417 | ||
|
|
2a2d8da628 | ||
|
|
27b63beb18 | ||
|
|
3c50cfef4e | ||
|
|
0b268279cf | ||
|
|
03ae9e27be | ||
|
|
31c2b818bd | ||
|
|
f4cdf02e85 | ||
|
|
f06827c6fb | ||
|
|
672aa42d03 | ||
|
|
7a6cb0f157 | ||
|
|
7940feaa1c | ||
|
|
3bfb271ddc | ||
|
|
dd3a747af4 | ||
|
|
43fc8bf74e | ||
|
|
33d69222ca | ||
|
|
773e03f84f | ||
|
|
e6d1f1af91 | ||
|
|
6980d1e142 | ||
|
|
06ac21dd85 | ||
|
|
63b1df38a8 | ||
|
|
5ef063c634 | ||
|
|
99a327155e | ||
|
|
59c7b80a2f | ||
|
|
843ff18cf9 | ||
|
|
3771c6eeae | ||
|
|
f4aa4c9466 | ||
|
|
4f05e45698 | ||
|
|
d540c99934 | ||
|
|
71d8dfb401 | ||
|
|
71d53dd5ef | ||
|
|
ffdd7cfa86 | ||
|
|
3d6d560777 | ||
|
|
2139e6fba2 | ||
|
|
6c0949e6dd | ||
|
|
ceec5fdb6c | ||
|
|
af97d3e341 | ||
|
|
283043eba7 | ||
|
|
8733eda576 | ||
|
|
07df87ab69 | ||
|
|
539c5b7974 | ||
|
|
94a0725917 | ||
|
|
106671a414 | ||
|
|
1d158d58f6 | ||
|
|
cd57f5cd0a | ||
|
|
3e2bc139ab | ||
|
|
16d0d42afe | ||
|
|
63de576be6 | ||
|
|
d5a544ca69 | ||
|
|
4a713f6b5e | ||
|
|
f165e1732b | ||
|
|
ea57f2b292 | ||
|
|
c880476cbe | ||
|
|
0a9c92fce9 | ||
|
|
aa090beae0 | ||
|
|
ebdacc094c | ||
|
|
0c28726ce2 | ||
|
|
be0604f7cf | ||
|
|
435280d626 | ||
|
|
558d49b3d3 | ||
|
|
05795af471 | ||
|
|
38aa3769bb | ||
|
|
5c6d9c9812 | ||
|
|
85fda71835 | ||
|
|
7150aff520 | ||
|
|
c157978912 | ||
|
|
ae93f7f967 | ||
|
|
f5ddc9a543 | ||
|
|
4c5570ae7d | ||
|
|
d0356f81ba | ||
|
|
c8e2c9aa25 | ||
|
|
fdda86455a | ||
|
|
eb4a485df6 | ||
|
|
579d8e293e | ||
|
|
a807776f83 | ||
|
|
48059a7c74 | ||
|
|
0199bc127a | ||
|
|
17c2734042 | ||
|
|
a1643ad292 | ||
|
|
1f97979059 | ||
|
|
c81b5e3d9c | ||
|
|
0d41c23383 | ||
|
|
0e39aa349e | ||
|
|
e607795bae | ||
|
|
821f39203c | ||
|
|
35e365c263 | ||
|
|
1c50111371 | ||
|
|
5999a8e24f | ||
|
|
4b046160f8 | ||
|
|
9c4b5814a6 | ||
|
|
0d67c62f43 | ||
|
|
5f785ede16 | ||
|
|
5b2fce2df7 | ||
|
|
ae81bb3c1b | ||
|
|
48d6fcaab8 | ||
|
|
633be8d06b | ||
|
|
651d7e4cfc | ||
|
|
0084f45629 | ||
|
|
8ab9fc7f55 | ||
|
|
11c54d27f2 | ||
|
|
173d60fb7d | ||
|
|
5bbef90f08 | ||
|
|
c3bc724523 | ||
|
|
0fbc60a20e | ||
|
|
70b5b08d58 | ||
|
|
294ded2030 | ||
|
|
9f3ae08e3b | ||
|
|
1f683afa43 | ||
|
|
0ab0b4f716 | ||
|
|
4be6a78691 | ||
|
|
d0d97c26a0 | ||
|
|
81b9832917 | ||
|
|
8d8027d423 | ||
|
|
9a374ddbd4 | ||
|
|
3f577779be | ||
|
|
fd1cbf4b6f | ||
|
|
5e7cfeb514 | ||
|
|
223d30ecd8 | ||
|
|
0e1f5ff391 | ||
|
|
a2612d5f36 | ||
|
|
632412bd72 | ||
|
|
09c28760e4 | ||
|
|
7c85e4056f | ||
|
|
d073b005a8 | ||
|
|
45e41827c7 | ||
|
|
d6ef43858c | ||
|
|
66af7f4a14 | ||
|
|
f4518cac9a | ||
|
|
ca56709295 | ||
|
|
860350a5b3 | ||
|
|
6a34abefba | ||
|
|
a523c14fd5 | ||
|
|
6f798ab288 | ||
|
|
ef882de509 | ||
|
|
67809c781a | ||
|
|
c20def5d59 | ||
|
|
e0cb5f9ba8 | ||
|
|
7e61aca512 | ||
|
|
89bd4eba46 | ||
|
|
ea0f24aa57 | ||
|
|
069c763010 | ||
|
|
bc93de682b | ||
|
|
8aa4779191 | ||
|
|
cde19066fe | ||
|
|
465a26f714 | ||
|
|
ca8e7e9891 | ||
|
|
7423d7d337 | ||
|
|
db21def58b | ||
|
|
6a4058052b | ||
|
|
b65e16a91b | ||
|
|
49dad1a456 | ||
|
|
0d3802873b | ||
|
|
20d0e95d7c | ||
|
|
d77a880a62 | ||
|
|
7ccb2ead09 | ||
|
|
8cd455b050 | ||
|
|
072c5b7753 | ||
|
|
0469b0d1ec | ||
|
|
74af7d01a2 | ||
|
|
c545de7bc6 | ||
|
|
8928e062b1 | ||
|
|
97f2341b98 | ||
|
|
aa2ae5e408 | ||
|
|
6d10ccfc15 | ||
|
|
dc99e74ae3 | ||
|
|
fd9aa288c4 | ||
|
|
af74a3c32f | ||
|
|
db1bc8a3db | ||
|
|
4856a9633e | ||
|
|
a4398626f6 | ||
|
|
17e00b397f | ||
|
|
1261513ab2 | ||
|
|
5f752e29f9 | ||
|
|
c0e6f3f4df | ||
|
|
11deff98d9 | ||
|
|
7f8334f29a | ||
|
|
8b02402f62 | ||
|
|
81f1f1f1bb | ||
|
|
16ffee9992 | ||
|
|
723d10241b | ||
|
|
2d284de426 | ||
|
|
52e1551c23 | ||
|
|
08b896fc2c | ||
|
|
0ffeb9f6ad | ||
|
|
e0060f8ffe | ||
|
|
3518991ff0 | ||
|
|
b047425a6f | ||
|
|
d4f0512a10 | ||
|
|
8b15a965dd | ||
|
|
ccf99cf985 | ||
|
|
9b690e9ae6 | ||
|
|
d0e2b7c341 | ||
|
|
4afd4b4044 | ||
|
|
80f0d5b5ec | ||
|
|
234741f35f | ||
|
|
7483839418 | ||
|
|
9e7e6041ed | ||
|
|
9ce1c25c04 | ||
|
|
4335659905 | ||
|
|
514fe69b65 | ||
|
|
a0ea68499b | ||
|
|
76d6dd346c | ||
|
|
83b9680318 | ||
|
|
f3c3f170a7 | ||
|
|
bc03b68b13 | ||
|
|
ad0c65500a | ||
|
|
6bff9d39e3 | ||
|
|
704452a9fa | ||
|
|
7abcfca1cb | ||
|
|
610c483d83 | ||
|
|
608966158a | ||
|
|
efdbe93cf0 | ||
|
|
c2748e923e | ||
|
|
a17e1f6b6d | ||
|
|
0ea6691189 | ||
|
|
eca43916f0 | ||
|
|
8cc59e3be7 | ||
|
|
b130e2065b | ||
|
|
d7deed6c45 | ||
|
|
7cc31df587 | ||
|
|
c30a17cd7a | ||
|
|
c0d3f8cbbe | ||
|
|
b6524946bc | ||
|
|
c98a0ccd1d | ||
|
|
73a31cb395 | ||
|
|
08f6ceeb50 | ||
|
|
f04a934ed1 | ||
|
|
c6bfaa41be | ||
|
|
5848de76ea | ||
|
|
2a54cd5004 | ||
|
|
d61f696f85 | ||
|
|
725d107288 | ||
|
|
b6fe1f5842 | ||
|
|
5b3f0825af | ||
|
|
381101f552 | ||
|
|
1fe534290d | ||
|
|
5004b8fbc9 | ||
|
|
f4603910e4 | ||
|
|
50d15249fc | ||
|
|
5bd633b40f | ||
|
|
41c8cfac73 | ||
|
|
68f3dd848a | ||
|
|
c59a778503 | ||
|
|
8ee7e7d828 | ||
|
|
5d6451fca7 | ||
|
|
90b7ce2dd6 | ||
|
|
7b77128aab | ||
|
|
9598b1fc0f | ||
|
|
ba79560c0c | ||
|
|
9bfd5cdb2b | ||
|
|
8bec67cbcf | ||
|
|
7eb4b42280 | ||
|
|
da2f6a045a | ||
|
|
820692f246 | ||
|
|
186b646dee | ||
|
|
b28ff25180 | ||
|
|
9a3e9c4c9a | ||
|
|
8e6249d361 | ||
|
|
cc07402b5e | ||
|
|
7c78e0025d | ||
|
|
d8d8330123 | ||
|
|
825571ac98 | ||
|
|
836a05d07f | ||
|
|
b9ec43c354 | ||
|
|
c1983a4846 | ||
|
|
b65b57c054 | ||
|
|
8af005cef0 | ||
|
|
bce7acf9cc | ||
|
|
a833dd67f3 | ||
|
|
6e83fec5ca | ||
|
|
290bddea77 | ||
|
|
5fea0b5525 | ||
|
|
c2f7615eeb | ||
|
|
4ba07a40eb | ||
|
|
e3d74684d5 | ||
|
|
73661ac633 | ||
|
|
55f7f63e6e | ||
|
|
aca1577040 | ||
|
|
2754793ff9 | ||
|
|
5e196b9f8b | ||
|
|
61559be8a4 | ||
|
|
07dcf3fac2 | ||
|
|
471781a47e | ||
|
|
0afe893a83 | ||
|
|
92551751bf | ||
|
|
c8682d33d0 | ||
|
|
3423d3c13d | ||
|
|
f3ee439b33 | ||
|
|
ba6e068abc | ||
|
|
34f64f02bf | ||
|
|
762f3bac65 | ||
|
|
b314f3839b | ||
|
|
f387a8fceb | ||
|
|
9ac54f694c | ||
|
|
2bce735300 | ||
|
|
2de9292ac0 | ||
|
|
d0e5568010 | ||
|
|
9724cefce8 | ||
|
|
2affa60ea9 | ||
|
|
2183b99330 | ||
|
|
be07421ab7 | ||
|
|
363f15124e | ||
|
|
34b5639d1c | ||
|
|
4cfeb79355 | ||
|
|
65ec7c5604 | ||
|
|
593428b16d | ||
|
|
3c0623f593 | ||
|
|
7d098328d0 | ||
|
|
3e29ae8534 | ||
|
|
d648875681 | ||
|
|
865786e0b6 | ||
|
|
e6894b949c | ||
|
|
11745add18 | ||
|
|
705a26a2fa | ||
|
|
50fa77276e | ||
|
|
4cde77d8d8 | ||
|
|
f5610e29be | ||
|
|
56f25ae065 | ||
|
|
5958d0c257 | ||
|
|
6dcd015a39 | ||
|
|
489fde8220 | ||
|
|
487b5776e6 | ||
|
|
f397361ba7 | ||
|
|
23e9a4607e | ||
|
|
978a0078d8 | ||
|
|
6649d17b06 | ||
|
|
d7f91824c0 | ||
|
|
107d53b358 | ||
|
|
58ca4a2b99 | ||
|
|
4819535a52 | ||
|
|
7d8d9cfdfe | ||
|
|
ff4751c9e8 | ||
|
|
eeff0a1252 | ||
|
|
47a8fc28df | ||
|
|
5dca98a1cf | ||
|
|
318830c57d | ||
|
|
f4d2ba5bbd | ||
|
|
633997b1b0 | ||
|
|
36b22e290a | ||
|
|
c3e61aebd2 | ||
|
|
de0c6f2ca9 | ||
|
|
20033eef9b | ||
|
|
9ccdb987d9 | ||
|
|
a6c58a3542 | ||
|
|
ceccd8c2dc | ||
|
|
a38819cbd5 | ||
|
|
801940912a | ||
|
|
b665e4e24a | ||
|
|
e4d94af019 | ||
|
|
eaf6d0d7d8 | ||
|
|
1b8bd0e1f3 | ||
|
|
ce817cbc12 | ||
|
|
84a40c282b | ||
|
|
9f7afda4db | ||
|
|
ddd50167a5 | ||
|
|
7ad664d89a | ||
|
|
5eb252215c | ||
|
|
10d8cc9d66 | ||
|
|
64f616b8a7 | ||
|
|
9a2b0a965d | ||
|
|
ec465571d8 | ||
|
|
ce42d847b3 | ||
|
|
a689830bff | ||
|
|
9bfcad31fd | ||
|
|
426010e21a | ||
|
|
ba09ddcc3a | ||
|
|
d0a655d5ae | ||
|
|
91a276d4ed | ||
|
|
16c1fc75b5 | ||
|
|
7f9ae4e044 | ||
|
|
c8be4f3f78 | ||
|
|
d830ce1d88 | ||
|
|
07a394a1c5 | ||
|
|
68a95c7dbc | ||
|
|
164d7cc896 | ||
|
|
063cef576c | ||
|
|
c6bfdcf503 | ||
|
|
085e0455d8 | ||
|
|
cb36dcb382 | ||
|
|
ffd426d43d | ||
|
|
d84a3c4f29 | ||
|
|
a12df122a9 | ||
|
|
df25d33735 | ||
|
|
a6c26874c7 | ||
|
|
54f758c327 | ||
|
|
bf0b74bcbd | ||
|
|
0d02f8b5d1 | ||
|
|
b343334f69 | ||
|
|
4a749cec72 | ||
|
|
3648f3816f | ||
|
|
6da359a839 | ||
|
|
5ea498062c | ||
|
|
4dcaf42bc5 | ||
|
|
db3be4195c | ||
|
|
9a526611e0 | ||
|
|
9ac87bd3b1 | ||
|
|
0171af4604 | ||
|
|
3ab31dcb92 | ||
|
|
63ecf13058 | ||
|
|
8218ca990c | ||
|
|
ac121dd4e3 | ||
|
|
870709079b | ||
|
|
302f049025 | ||
|
|
63668eb855 | ||
|
|
9a93c892be | ||
|
|
1af2326a52 | ||
|
|
ab162070a6 | ||
|
|
d8e1a21bdc | ||
|
|
c2eb771c4d | ||
|
|
8a72845ee6 | ||
|
|
7189daba19 | ||
|
|
93d0db2910 | ||
|
|
50db0aca61 | ||
|
|
b5d2708f69 | ||
|
|
becfd980b2 | ||
|
|
be6cd6adc3 | ||
|
|
48939f25c8 | ||
|
|
94a03c6e17 | ||
|
|
cd4746ad2a | ||
|
|
e22d0a3406 | ||
|
|
1755006445 | ||
|
|
4c9e17fbd3 | ||
|
|
d06831ea94 | ||
|
|
603d2cf77d | ||
|
|
13557a2c10 | ||
|
|
218c51cdcf | ||
|
|
61a11c8f1f | ||
|
|
b21abf4d90 | ||
|
|
c70282663c | ||
|
|
87e0d2f7f4 | ||
|
|
8bfc212e26 | ||
|
|
a937e32989 | ||
|
|
935f2e11e8 | ||
|
|
7202939e0d | ||
|
|
73a8b6a7d7 | ||
|
|
303ae8321b | ||
|
|
d5e1a46588 | ||
|
|
27108874ea | ||
|
|
29aa5d6468 | ||
|
|
a826a894f4 | ||
|
|
9a5604c5bb | ||
|
|
b95dfcbce0 | ||
|
|
ed01b4c161 | ||
|
|
19db526fdd | ||
|
|
11dd1968c7 | ||
|
|
07aaef2af2 | ||
|
|
20478b632f | ||
|
|
18f32b8de6 | ||
|
|
81297ce168 | ||
|
|
f48ce90658 | ||
|
|
3df68e462f | ||
|
|
a79bc4d35a | ||
|
|
bf4fb53575 | ||
|
|
cf25f4c579 | ||
|
|
188645bfd6 | ||
|
|
1c4eef2ef6 | ||
|
|
a3bf320add | ||
|
|
d0db3b08d7 | ||
|
|
2fb441763a | ||
|
|
33fa1e45ad | ||
|
|
d392660d45 | ||
|
|
ced76ca5c0 | ||
|
|
9c0f17e13d | ||
|
|
e0a45a5a54 | ||
|
|
692de892ae | ||
|
|
879b2b778a | ||
|
|
12530616a7 | ||
|
|
41db040a60 | ||
|
|
40927c7413 | ||
|
|
58a3ef7aa6 | ||
|
|
9ddf1ccedd | ||
|
|
0495160f81 | ||
|
|
d2e5b2aa1d | ||
|
|
39387e9f54 | ||
|
|
2205ae8e54 | ||
|
|
29fe23bc0a | ||
|
|
8d97966662 | ||
|
|
c99d0535f8 | ||
|
|
fac27d9dff | ||
|
|
0519263882 | ||
|
|
71479ad47b | ||
|
|
88f5e3f160 | ||
|
|
71837ab400 | ||
|
|
853ca1fcee | ||
|
|
50dd8d9df7 | ||
|
|
aeaadb1e30 | ||
|
|
2e0cf36901 | ||
|
|
1d5345abc1 | ||
|
|
0a03076148 | ||
|
|
a3b8f9759d | ||
|
|
f51c511bcc | ||
|
|
9e56f213a3 | ||
|
|
1fa9030aee | ||
|
|
62226696aa | ||
|
|
605a30a7e7 | ||
|
|
0996aff79d | ||
|
|
501c53db05 | ||
|
|
f343d5a24d | ||
|
|
04fbcc64ff | ||
|
|
148d7e798b | ||
|
|
f7717c91bc | ||
|
|
c05382fa48 | ||
|
|
b673377b70 | ||
|
|
6bbc8e0544 | ||
|
|
ddb38db5c4 | ||
|
|
03b5d5a0e0 | ||
|
|
f70506fc92 | ||
|
|
a58ce52729 | ||
|
|
a5d9f5518f | ||
|
|
1281d9d21d | ||
|
|
52be45c5df | ||
|
|
60ffcd0574 | ||
|
|
601ea444ca | ||
|
|
ab20344141 | ||
|
|
a33d5535a7 | ||
|
|
e730b8c6e4 | ||
|
|
9f970189fe | ||
|
|
8db11d03ed | ||
|
|
2c21df2ad9 | ||
|
|
b80a5f27a9 |
16
CODEOWNERS
16
CODEOWNERS
@@ -4,21 +4,21 @@
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @phot0n
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.38.4"
|
||||
__version__ = "15.45.5"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += f"AND {deferred_account}='{account}'"
|
||||
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -191,7 +191,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-27 16:23:04.444354",
|
||||
"modified": "2024-08-19 15:19:11.095045",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
|
||||
@@ -60,6 +60,7 @@ class Account(NestedSet):
|
||||
"Payable",
|
||||
"Receivable",
|
||||
"Round Off",
|
||||
"Round Off for Opening",
|
||||
"Stock",
|
||||
"Stock Adjustment",
|
||||
"Stock Received But Not Billed",
|
||||
|
||||
@@ -113,9 +113,9 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = []
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
|
||||
filters={"docstatus": 1, "company": company, "period_end_date": ("<", closing_date)},
|
||||
fields=["name"],
|
||||
order_by="posting_date desc",
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ frappe.ui.form.on("Accounting Dimension", {
|
||||
},
|
||||
|
||||
label: function (frm) {
|
||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
|
||||
frm.set_value("fieldname", frm.doc.label.replace(/ /g, "_").replace(/-/g, "_").toLowerCase());
|
||||
},
|
||||
|
||||
document_type: function (frm) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.database.schema import validate_column_name
|
||||
from frappe.model import core_doctypes_list
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr
|
||||
@@ -60,6 +61,7 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
|
||||
@@ -74,12 +74,12 @@ def get_dimension_filter_map():
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -97,7 +97,6 @@ def get_dimension_filter_map():
|
||||
f.allow_or_restrict,
|
||||
f.is_mandatory,
|
||||
)
|
||||
|
||||
frappe.flags.dimension_filter_map = dimension_filter_map
|
||||
|
||||
return frappe.flags.dimension_filter_map
|
||||
|
||||
@@ -101,6 +101,8 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
date = doc.available_for_use_date
|
||||
elif doc.doctype == "Asset Repair":
|
||||
date = doc.completion_date
|
||||
elif doc.doctype == "Period Closing Voucher":
|
||||
date = doc.period_end_date
|
||||
else:
|
||||
date = doc.posting_date
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-10-16 16:57:12.085072",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Against Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Against Voucher No",
|
||||
"options": "against_voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "event",
|
||||
"fieldtype": "Data",
|
||||
"label": "Event",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase):
|
||||
"""
|
||||
Integration tests for AdvancePaymentLedgerEntry.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return so
|
||||
|
||||
def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
po = create_purchase_order(
|
||||
company=self.company,
|
||||
customer=self.supplier,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return po
|
||||
|
||||
def test_so_advance_paid_and_currency_with_payment(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from = self.debtors_usd
|
||||
pe.paid_from_account_currency = "USD"
|
||||
pe.source_exchange_rate = pe_exchange_rate
|
||||
pe.paid_amount = so.grand_total
|
||||
pe.received_amount = pe_exchange_rate * pe.paid_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_so_advance_paid_and_currency_with_journal(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": so.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debtors_usd,
|
||||
"party_type": "Customer",
|
||||
"party": so.customer,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": so.doctype,
|
||||
"reference_name": so.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_payment(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.target_exchange_rate = pe_exchange_rate
|
||||
pe.received_amount = po.grand_total
|
||||
pe.paid_amount = pe_exchange_rate * pe.received_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_journal(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": po.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.creditors_usd,
|
||||
"party_type": "Supplier",
|
||||
"party": po.supplier,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": po.doctype,
|
||||
"reference_name": po.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
@@ -224,11 +224,6 @@
|
||||
"link_doctype": "Bank Guarantee",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payroll Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Transaction",
|
||||
@@ -255,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-09-24 06:57:41.292970",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -168,7 +168,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
"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) as debit,
|
||||
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`
|
||||
|
||||
@@ -208,13 +208,17 @@ class BankTransaction(Document):
|
||||
if self.party_type and self.party:
|
||||
return
|
||||
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
result = None
|
||||
try:
|
||||
result = AutoMatchParty(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
bank_party_iban=self.bank_party_iban,
|
||||
bank_party_name=self.bank_party_name,
|
||||
description=self.description,
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
@@ -19,16 +19,6 @@
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"column_break_8",
|
||||
@@ -56,7 +46,21 @@
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"address_and_contact_tab",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break_vodj",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_xban",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"column_break_lqmf"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -178,10 +182,8 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
@@ -377,11 +379,28 @@
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vodj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xban",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqmf",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-15 15:46:53.865712",
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -435,4 +454,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def before_submit(self):
|
||||
self.remove_accounts_without_gain_loss()
|
||||
|
||||
def remove_accounts_without_gain_loss(self):
|
||||
self.accounts = [account for account in self.accounts if account.gain_loss]
|
||||
|
||||
if not self.accounts:
|
||||
frappe.throw(_("At least one account with exchange gain or loss is required"))
|
||||
|
||||
frappe.msgprint(
|
||||
_("Removing rows without exchange gain or loss"),
|
||||
alert=True,
|
||||
indicator="yellow",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@@ -248,23 +263,23 @@ class ExchangeRateRevaluation(Document):
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
@@ -288,23 +303,22 @@ class ExchangeRateRevaluation(Document):
|
||||
current_exchange_rate * d.balance_in_account_currency
|
||||
)
|
||||
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.source_exchange_rate = 84.2105
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
frappe.ui.form.on("Fiscal Year", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value(
|
||||
"year_start_date",
|
||||
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)
|
||||
);
|
||||
frm.set_value("year_start_date", frappe.datetime.year_start());
|
||||
}
|
||||
},
|
||||
year_start_date: function (frm) {
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Less than 12 months.",
|
||||
"description": "More/Less than 12 months.",
|
||||
"fieldname": "is_short_year",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Short Year",
|
||||
"label": "Is Short/Long Year",
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -6,38 +6,50 @@
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"dates_section",
|
||||
"posting_date",
|
||||
"transaction_date",
|
||||
"column_break_avko",
|
||||
"fiscal_year",
|
||||
"due_date",
|
||||
"account_details_section",
|
||||
"account",
|
||||
"account_currency",
|
||||
"column_break_ifvf",
|
||||
"against",
|
||||
"party_type",
|
||||
"party",
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
"against",
|
||||
"transaction_details_section",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"voucher_subtype",
|
||||
"transaction_currency",
|
||||
"column_break_dpsx",
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
"voucher_type",
|
||||
"voucher_subtype",
|
||||
"voucher_no",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
"project",
|
||||
"remarks",
|
||||
"more_info_section",
|
||||
"finance_book",
|
||||
"company",
|
||||
"is_opening",
|
||||
"is_advance",
|
||||
"fiscal_year",
|
||||
"company",
|
||||
"finance_book",
|
||||
"column_break_8abq",
|
||||
"to_rename",
|
||||
"due_date",
|
||||
"is_cancelled",
|
||||
"transaction_currency",
|
||||
"debit_in_transaction_currency",
|
||||
"credit_in_transaction_currency",
|
||||
"transaction_exchange_rate"
|
||||
"remarks"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -285,13 +297,67 @@
|
||||
"fieldname": "voucher_subtype",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Voucher Subtype"
|
||||
},
|
||||
{
|
||||
"fieldname": "dates_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dates"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_avko",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Account Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ifvf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transaction Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "amounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dpsx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bm1w",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lmnm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-02 14:31:51.496466",
|
||||
"modified": "2024-08-22 13:03:39.997475",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
|
||||
@@ -430,8 +430,9 @@ def update_against_account(voucher_type, voucher_no):
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
|
||||
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
|
||||
frappe.db.add_index("GL Entry", ["posting_date", "company"])
|
||||
frappe.db.add_index("GL Entry", ["party_type", "party"])
|
||||
|
||||
|
||||
def rename_gle_sle_docs():
|
||||
|
||||
@@ -127,9 +127,6 @@ class JournalEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
@@ -184,10 +181,16 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
def before_submit(self):
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
@@ -218,8 +221,10 @@ class JournalEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
@@ -259,7 +264,7 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, self.posting_date, self.company
|
||||
@@ -1668,6 +1673,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
"reference_type": "reference_type",
|
||||
"reference_name": "reference_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -515,6 +515,55 @@ class TestJournalEntry(unittest.TestCase):
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
def test_toggle_debit_credit_if_negative(self):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.extend(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit": 100 * -1,
|
||||
"debit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit": 100 * -1,
|
||||
"credit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
jv.flags.ignore_validate = True
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(len(jv.accounts), 2)
|
||||
|
||||
gl_map = jv.build_gl_map()
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit, 100 * -1)
|
||||
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
|
||||
|
||||
gl_map = process_gl_map(gl_map, False)
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.credit, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_transaction_currency, 100)
|
||||
|
||||
def test_transaction_exchange_rate_on_journals(self):
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
|
||||
|
||||
@@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
@@ -181,6 +185,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
company: doc.company,
|
||||
status: ["!=", "Paid"],
|
||||
outstanding_amount: [">", 0], // for compatibility with old data
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -316,11 +324,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"write_off_difference_amount",
|
||||
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
|
||||
);
|
||||
|
||||
frm.toggle_display(
|
||||
"set_exchange_gain_loss",
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount
|
||||
);
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
@@ -403,6 +406,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
payment_type: function (frm) {
|
||||
set_default_party_type(frm);
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
[
|
||||
@@ -1109,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
set_unallocated_amount: function (frm) {
|
||||
var unallocated_amount = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
let unallocated_amount = 0;
|
||||
let deductions_to_consider = 0;
|
||||
|
||||
for (const row of frm.doc.deductions || []) {
|
||||
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
|
||||
}
|
||||
const included_taxes = get_included_taxes(frm);
|
||||
|
||||
if (frm.doc.party) {
|
||||
if (
|
||||
frm.doc.payment_type == "Receive" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount +
|
||||
total_deductions -
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
frm.doc.base_total_allocated_amount) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_paid_amount +
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
(total_deductions + frm.doc.base_total_allocated_amount)) /
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount -
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@@ -1232,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
write_off_difference_amount: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "write_off_account");
|
||||
frm.events.set_write_off_deduction(frm);
|
||||
},
|
||||
|
||||
set_exchange_gain_loss: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
|
||||
base_paid_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
set_deductions_entry: function (frm, account) {
|
||||
if (frm.doc.difference_amount) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
|
||||
return t.account == r.message[account] ? t : null;
|
||||
});
|
||||
base_received_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
const difference_amount = flt(
|
||||
frm.doc.difference_amount,
|
||||
precision("difference_amount")
|
||||
);
|
||||
set_exchange_gain_loss_deduction: async function (frm) {
|
||||
// wait for allocate_party_amount_against_ref_docs to finish
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"));
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
},
|
||||
__(frappe.unscrub(account))
|
||||
);
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!exchange_gain_loss) {
|
||||
frm.events.delete_exchange_gain_loss(frm);
|
||||
return;
|
||||
}
|
||||
|
||||
const account_fieldname = "exchange_gain_loss_account";
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
row.amount = exchange_gain_loss;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
delete_exchange_gain_loss: function (frm) {
|
||||
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
|
||||
|
||||
if (!exchange_gain_loss_row) return;
|
||||
|
||||
exchange_gain_loss_row.amount = 0;
|
||||
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
|
||||
frm.refresh_field("deductions");
|
||||
},
|
||||
|
||||
set_write_off_deduction: async function (frm) {
|
||||
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
|
||||
if (!difference_amount) return;
|
||||
|
||||
const account_fieldname = "write_off_account";
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
const write_off_account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
if (!write_off_account) return;
|
||||
|
||||
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
|
||||
if (!row) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = write_off_account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
}
|
||||
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@@ -1768,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Deduction", {
|
||||
before_deductions_remove: function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (row.is_exchange_gain_loss && row.amount) {
|
||||
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
|
||||
}
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
@@ -1776,3 +1794,66 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
|
||||
function set_default_party_type(frm) {
|
||||
if (frm.doc.party) return;
|
||||
|
||||
let party_type;
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
party_type = "Customer";
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
party_type = "Supplier";
|
||||
}
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
function get_included_taxes(frm) {
|
||||
let included_taxes = 0;
|
||||
for (const tax of frm.doc.taxes) {
|
||||
if (!tax.included_in_paid_amount) continue;
|
||||
|
||||
if (tax.add_deduct_tax == "Add") {
|
||||
included_taxes += tax.base_tax_amount;
|
||||
} else {
|
||||
included_taxes -= tax.base_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
return included_taxes;
|
||||
}
|
||||
|
||||
function get_company_defaults(company) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: company,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prompt_for_missing_account(frm, account) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = frappe.prompt(
|
||||
{
|
||||
label: __(frappe.unscrub(account)),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
function get_deduction_amount_precision() {
|
||||
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
"base_total_allocated_amount",
|
||||
"set_exchange_gain_loss",
|
||||
"column_break_36",
|
||||
"unallocated_amount",
|
||||
"difference_amount",
|
||||
@@ -390,11 +389,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_exchange_gain_loss",
|
||||
"fieldtype": "Button",
|
||||
"label": "Set Exchange Gain / Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -801,7 +795,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified": "2024-11-07 11:19:19.320883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -110,6 +110,7 @@ class PaymentEntry(AccountsController):
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
@@ -190,6 +191,7 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -197,6 +199,7 @@ class PaymentEntry(AccountsController):
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
@@ -890,6 +893,7 @@ class PaymentEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_total_allocated_amount()
|
||||
self.set_unallocated_amount()
|
||||
self.set_exchange_gain_loss()
|
||||
self.set_difference_amount()
|
||||
|
||||
def validate_amounts(self):
|
||||
@@ -985,10 +989,10 @@ class PaymentEntry(AccountsController):
|
||||
if d.exchange_rate is None:
|
||||
d.exchange_rate = 1
|
||||
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
allocated_amount_in_ref_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -1006,29 +1010,80 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if (
|
||||
self.payment_type == "Receive"
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
||||
and self.total_allocated_amount
|
||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
||||
) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif (
|
||||
self.payment_type == "Pay"
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
||||
and self.total_allocated_amount
|
||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
||||
) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
if not self.party:
|
||||
return
|
||||
|
||||
deductions_to_consider = sum(
|
||||
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
|
||||
)
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
|
||||
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
|
||||
|
||||
for row in exchange_gain_loss_rows:
|
||||
self.remove(row)
|
||||
|
||||
if not exchange_gain_loss:
|
||||
if exchange_gain_loss_row:
|
||||
self.remove(exchange_gain_loss_row)
|
||||
|
||||
return
|
||||
|
||||
if not exchange_gain_loss_row:
|
||||
values = frappe.get_cached_value(
|
||||
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
|
||||
)
|
||||
|
||||
for fieldname, value in values.items():
|
||||
if value:
|
||||
continue
|
||||
|
||||
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||
return frappe.msgprint(
|
||||
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||
label, get_link_to_form("Company", self.company)
|
||||
),
|
||||
title=_("Missing Default in Company"),
|
||||
indicator="red" if self.docstatus.is_submitted() else "yellow",
|
||||
raise_exception=self.docstatus.is_submitted(),
|
||||
)
|
||||
|
||||
exchange_gain_loss_row = self.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": values.exchange_gain_loss_account,
|
||||
"cost_center": values.cost_center,
|
||||
"is_exchange_gain_loss": 1,
|
||||
},
|
||||
)
|
||||
|
||||
exchange_gain_loss_row.amount = exchange_gain_loss
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (
|
||||
@@ -1056,11 +1111,13 @@ class PaymentEntry(AccountsController):
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get("taxes"):
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
if not tax.included_in_paid_amount:
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -1143,6 +1200,12 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.currency = self.paid_from_account_currency
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.currency = self.paid_to_account_currency
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
@@ -1210,11 +1273,19 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
@@ -1245,13 +1316,22 @@ class PaymentEntry(AccountsController):
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
@@ -1728,7 +1808,7 @@ class PaymentEntry(AccountsController):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
_("Cannot {0} from {1} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
@@ -1886,8 +1966,8 @@ class PaymentEntry(AccountsController):
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
@@ -1989,7 +2069,7 @@ def get_outstanding_of_references_with_payment_term(references=None):
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
@@ -2486,7 +2566,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
@@ -2801,9 +2883,6 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
@@ -2813,7 +2892,8 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_difference_amount()
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
@@ -2825,7 +2905,7 @@ def get_payment_entry(
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
@@ -2849,6 +2929,7 @@ def get_open_payment_requests_for_references(references=None):
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -3159,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -479,16 +479,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[-1].amount, 300.0)
|
||||
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -552,16 +545,10 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
self.assertEqual(pe.difference_amount, 100)
|
||||
self.assertEqual(pe.deductions[0].amount, 100)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -654,16 +641,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(pe.difference_amount, 500)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 500,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[0].amount, 500)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -956,6 +936,53 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
|
||||
|
||||
def test_gl_of_multi_currency_payment_transaction(self):
|
||||
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||
save_new_records,
|
||||
test_records,
|
||||
)
|
||||
|
||||
save_new_records(test_records)
|
||||
paid_from = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="_Test Cash USD",
|
||||
company="_Test Company",
|
||||
account_type="Cash",
|
||||
account_currency="USD",
|
||||
)
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD",
|
||||
paid_from=paid_from,
|
||||
paid_to="_Test Payable USD - _TC",
|
||||
paid_amount=100,
|
||||
save=True,
|
||||
)
|
||||
payment_entry.source_exchange_rate = 84.4
|
||||
payment_entry.target_exchange_rate = 84.4
|
||||
payment_entry.save()
|
||||
payment_entry = payment_entry.submit()
|
||||
gle = qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
gle.debit_in_transaction_currency,
|
||||
gle.credit_in_transaction_currency,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
expected_gl_entries = (
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||
)
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_multi_currency_payment_entry_with_taxes(self):
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"cost_center",
|
||||
"amount",
|
||||
"column_break_2",
|
||||
"is_exchange_gain_loss",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -45,12 +46,20 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_exchange_gain_loss",
|
||||
"fieldname": "is_exchange_gain_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Exchange Gain / Loss?",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link
|
||||
description: DF.SmallText | None
|
||||
is_exchange_gain_loss: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -153,10 +153,7 @@ class PaymentReconciliation(Document):
|
||||
self.add_payment_entries(non_reconciled_payments)
|
||||
|
||||
def get_payment_entries(self):
|
||||
if self.default_advance_account:
|
||||
party_account = [self.receivable_payable_account, self.default_advance_account]
|
||||
else:
|
||||
party_account = [self.receivable_payable_account]
|
||||
party_account = [self.receivable_payable_account]
|
||||
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = frappe._dict(
|
||||
@@ -187,6 +184,7 @@ class PaymentReconciliation(Document):
|
||||
self.party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
default_advance_account=self.default_advance_account,
|
||||
against_all_orders=True,
|
||||
limit=self.payment_limit,
|
||||
condition=condition,
|
||||
@@ -211,12 +209,14 @@ class PaymentReconciliation(Document):
|
||||
if self.get("cost_center"):
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
account_type = erpnext.get_party_account_type(self.party_type)
|
||||
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||
|
||||
conditions.append(dr_or_cr.gt(0))
|
||||
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
@@ -231,7 +231,7 @@ class PaymentReconciliation(Document):
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
dr_or_cr.as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
@@ -323,6 +323,7 @@ class PaymentReconciliation(Document):
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
"cost_center": inv.cost_center,
|
||||
"remarks": inv.remarks,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -370,6 +371,10 @@ class PaymentReconciliation(Document):
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
non_reconciled_invoices = sorted(
|
||||
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
|
||||
)
|
||||
|
||||
self.add_invoice_entries(non_reconciled_invoices)
|
||||
|
||||
def add_invoice_entries(self, non_reconciled_invoices):
|
||||
|
||||
@@ -632,6 +632,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.accounts[1].credit_in_account_currency = 0
|
||||
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
@@ -954,6 +990,100 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = -8000
|
||||
je1.accounts[0].credit = -8000
|
||||
je1.accounts[0].debit_in_account_currency = 0
|
||||
je1.accounts[0].debit = 0
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = -16000
|
||||
je2.accounts[0].credit = -16000
|
||||
je2.accounts[0].debit_in_account_currency = 0
|
||||
je2.accounts[0].debit = 0
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
@@ -1986,13 +2116,15 @@ def make_period_closing_voucher(company, cost_center, posting_date=None, submit=
|
||||
parent_account=parent_account,
|
||||
doctype="Account",
|
||||
)
|
||||
fy = get_fiscal_year(posting_date, company=company)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": fy[2],
|
||||
"company": company,
|
||||
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"amount",
|
||||
"difference_amount",
|
||||
"sec_break1",
|
||||
"remark",
|
||||
"remarks",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"cost_center"
|
||||
@@ -74,12 +74,6 @@
|
||||
"fieldname": "sec_break1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remark",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
@@ -105,12 +99,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remarks",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:34.818530",
|
||||
"modified": "2024-10-29 16:24:43.021230",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
|
||||
@@ -27,7 +27,7 @@ class PaymentReconciliationPayment(Document):
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_row: DF.Data | None
|
||||
reference_type: DF.Link | None
|
||||
remark: DF.SmallText | None
|
||||
remarks: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"party_details",
|
||||
"party_type",
|
||||
"party",
|
||||
"party_name",
|
||||
"column_break_4",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
@@ -422,13 +423,19 @@
|
||||
"label": "Party Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 17:50:54.440090",
|
||||
"modified": "2024-10-23 12:23:40.117336",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -467,4 +474,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
@@ -20,6 +20,15 @@ from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"POS Invoice",
|
||||
"Fees",
|
||||
]
|
||||
|
||||
|
||||
def _get_payment_gateway_controller(*args, **kwargs):
|
||||
with payment_app_import_guard():
|
||||
@@ -62,6 +71,7 @@ class PaymentRequest(Document):
|
||||
outstanding_amount: DF.Currency
|
||||
party: DF.DynamicLink | None
|
||||
party_account_currency: DF.Link | None
|
||||
party_name: DF.Data | None
|
||||
party_type: DF.Link | None
|
||||
payment_account: DF.ReadOnly | None
|
||||
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
|
||||
@@ -294,12 +304,12 @@ class PaymentRequest(Document):
|
||||
return controller.get_payment_url(
|
||||
**{
|
||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||
"title": data.company.encode("utf-8"),
|
||||
"description": self.subject.encode("utf-8"),
|
||||
"title": data.company,
|
||||
"description": self.subject,
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": self.name,
|
||||
"payer_email": self.email_to or frappe.session.user,
|
||||
"payer_name": frappe.safe_encode(data.customer_name),
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
}
|
||||
@@ -525,6 +535,9 @@ def make_payment_request(**args):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
@@ -551,11 +564,35 @@ def make_payment_request(**args):
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||
grand_total -= existing_payment_request_amount
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Request is already created"))
|
||||
return grand_total
|
||||
|
||||
if existing_payment_request_amount:
|
||||
if args.order_type == "Shopping Cart":
|
||||
# If Payment Request is in an advanced stage, then create for remaining amount.
|
||||
if get_existing_payment_request_amount(
|
||||
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||
):
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
else:
|
||||
# If PR's are processed, cancel all of them.
|
||||
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
|
||||
else:
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
|
||||
if existing_paid_amount:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
if ref_doc.conversion_rate:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
@@ -597,6 +634,7 @@ def make_payment_request(**args):
|
||||
"party_type": party_type,
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -664,21 +702,88 @@ def get_amount(ref_doc, payment_account=None):
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
def get_irequest_status(payment_requests: None | list = None) -> list:
|
||||
IR = frappe.qb.DocType("Integration Request")
|
||||
res = []
|
||||
if payment_requests:
|
||||
res = (
|
||||
frappe.qb.from_(IR)
|
||||
.select(IR.name)
|
||||
.where(IR.reference_doctype.eq("Payment Request"))
|
||||
.where(IR.reference_docname.isin(payment_requests))
|
||||
.where(IR.status.isin(["Authorized", "Completed"]))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def cancel_old_payment_requests(ref_dt, ref_dn):
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
if res := (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name)
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.status.isin(["Draft", "Requested"]))
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if get_irequest_status([x.name for x in res]):
|
||||
frappe.throw(_("Another Payment Request is already processed"))
|
||||
else:
|
||||
for x in res:
|
||||
doc = frappe.get_doc("Payment Request", x.name)
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.cancel()
|
||||
|
||||
if ireqs := get_irequests_of_payment_request(doc.name):
|
||||
for ireq in ireqs:
|
||||
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
|
||||
"""
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
query = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(Sum(PR.grand_total))
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.run()
|
||||
)
|
||||
|
||||
if statuses:
|
||||
query = query.where(PR.status.isin(statuses))
|
||||
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_existing_paid_amount(doctype, name):
|
||||
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
.where(PL.against_voucher_no.eq(name))
|
||||
.where(PL.amount < 0)
|
||||
.where(PL.delinked == 0)
|
||||
.where(PER.docstatus == 1)
|
||||
.where(PER.payment_request.isnull())
|
||||
)
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
@@ -874,21 +979,17 @@ def validate_payment(doc, method=None):
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
if not filters.reference_doctype or not filters.reference_name:
|
||||
return []
|
||||
|
||||
if txt:
|
||||
filters.name = ["like", f"%{txt}%"]
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
filters=filters,
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
@@ -901,3 +1002,17 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len,
|
||||
)
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
|
||||
def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||
res = []
|
||||
if doc:
|
||||
res = frappe.db.get_all(
|
||||
"Integration Request",
|
||||
{
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": doc,
|
||||
"status": "Queued",
|
||||
},
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "payment_request",
|
||||
"internal_links": {
|
||||
"Payment Entry": ["references", "payment_request"],
|
||||
"Payment Order": ["references", "payment_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||
],
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "red",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
Requested: "green",
|
||||
};
|
||||
|
||||
frappe.listview_settings["Payment Request"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Draft") {
|
||||
return [__("Draft"), "gray", "status,=,Draft"];
|
||||
}
|
||||
if (doc.status == "Requested") {
|
||||
return [__("Requested"), "green", "status,=,Requested"];
|
||||
} else if (doc.status == "Initiated") {
|
||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
||||
} else if (doc.status == "Partially Paid") {
|
||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
||||
} else if (doc.status == "Paid") {
|
||||
return [__("Paid"), "blue", "status,=,Paid"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||
|
||||
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -524,3 +525,48 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
def test_partial_paid_invoice_with_payment_request(self):
|
||||
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PAYEE0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
si.load_from_db()
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -19,6 +19,24 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
});
|
||||
},
|
||||
|
||||
fiscal_year: function (frm) {
|
||||
if (frm.doc.fiscal_year) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.get_period_start_end_date",
|
||||
args: {
|
||||
fiscal_year: frm.doc.fiscal_year,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("period_start_date", r.message[0]);
|
||||
frm.set_value("period_end_date", r.message[1]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
|
||||
@@ -6,39 +6,32 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_date",
|
||||
"posting_date",
|
||||
"fiscal_year",
|
||||
"year_start_date",
|
||||
"amended_from",
|
||||
"company",
|
||||
"fiscal_year",
|
||||
"period_start_date",
|
||||
"period_end_date",
|
||||
"amended_from",
|
||||
"column_break1",
|
||||
"closing_account_head",
|
||||
"remarks",
|
||||
"gle_processing_status",
|
||||
"remarks",
|
||||
"error_message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Transaction Date",
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Closing Fiscal Year",
|
||||
"label": "Fiscal Year",
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
@@ -103,16 +96,25 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "year_start_date",
|
||||
"fieldname": "period_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Year Start Date"
|
||||
"label": "Period End Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "period_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Period Start Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-11 20:19:11.810533",
|
||||
"modified": "2024-09-15 17:22:45.291628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
@@ -148,7 +150,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"search_fields": "fiscal_year, period_start_date, period_end_date",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, flt
|
||||
from frappe.utils import add_days, flt, formatdate, getdate
|
||||
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
|
||||
@@ -29,36 +34,395 @@ class PeriodClosingVoucher(AccountsController):
|
||||
error_message: DF.Text | None
|
||||
fiscal_year: DF.Link
|
||||
gle_processing_status: DF.Literal["In Progress", "Completed", "Failed"]
|
||||
posting_date: DF.Date
|
||||
period_end_date: DF.Date
|
||||
period_start_date: DF.Date
|
||||
remarks: DF.SmallText
|
||||
transaction_date: DF.Date | None
|
||||
year_start_date: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account_head()
|
||||
self.validate_posting_date()
|
||||
self.validate_start_and_end_date()
|
||||
self.check_if_previous_year_closed()
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.check_closing_account_type()
|
||||
self.check_closing_account_currency()
|
||||
|
||||
def validate_start_and_end_date(self):
|
||||
self.fy_start_date, self.fy_end_date = frappe.db.get_value(
|
||||
"Fiscal Year", self.fiscal_year, ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
prev_closed_period_end_date = get_previous_closed_period_in_current_year(
|
||||
self.fiscal_year, self.company
|
||||
)
|
||||
valid_start_date = (
|
||||
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else self.fy_start_date
|
||||
)
|
||||
|
||||
if getdate(self.period_start_date) != getdate(valid_start_date):
|
||||
frappe.throw(_("Period Start Date must be {0}").format(formatdate(valid_start_date)))
|
||||
|
||||
if getdate(self.period_start_date) > getdate(self.period_end_date):
|
||||
frappe.throw(_("Period Start Date cannot be greater than Period End Date"))
|
||||
|
||||
if getdate(self.period_end_date) > getdate(self.fy_end_date):
|
||||
frappe.throw(_("Period End Date cannot be greater than Fiscal Year End Date"))
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.fy_start_date, -1)
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
gle_exists_in_previous_year = frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
)
|
||||
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"))
|
||||
|
||||
def block_if_future_closing_voucher_exists(self):
|
||||
future_closing_voucher = self.get_future_closing_voucher()
|
||||
if future_closing_voucher and future_closing_voucher[0][0]:
|
||||
action = "cancel" if self.docstatus == 2 else "create"
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
|
||||
).format(action, future_closing_voucher[0][0], self.period_end_date)
|
||||
)
|
||||
|
||||
def get_future_closing_voucher(self):
|
||||
return frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
{"period_end_date": (">", self.period_end_date), "docstatus": 1, "company": self.company},
|
||||
"name",
|
||||
)
|
||||
|
||||
def check_closing_account_type(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
if closing_account_type not in ["Liability", "Equity"]:
|
||||
frappe.throw(
|
||||
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
||||
)
|
||||
|
||||
def check_closing_account_currency(self):
|
||||
account_currency = get_account_currency(self.closing_account_head)
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if account_currency != company_currency:
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
get_opening_entries = False
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
|
||||
):
|
||||
get_opening_entries = True
|
||||
|
||||
self.make_gl_entries(get_opening_entries=get_opening_entries)
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_future_closing_vouchers()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
gle_count = frappe.db.count(
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.cancel_gl_entries()
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.get_gle_count_in_selected_period() > 5000:
|
||||
frappe.enqueue(
|
||||
process_gl_and_closing_entries,
|
||||
doc=self,
|
||||
timeout=1800,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"The GL Entries and closing balances will be processed in the background, it can take a few minutes."
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_and_closing_entries(self)
|
||||
|
||||
def get_gle_count_in_selected_period(self):
|
||||
return frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
{
|
||||
"posting_date": ["between", [self.period_start_date, self.period_end_date]],
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
)
|
||||
if gle_count > 5000:
|
||||
|
||||
def get_pcv_gl_entries(self):
|
||||
self.pl_accounts_reverse_gle = []
|
||||
self.closing_account_gle = []
|
||||
|
||||
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||
for dimensions, account_balances in pl_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if balance_in_company_currency and acc != "balances":
|
||||
self.pl_accounts_reverse_gle.append(
|
||||
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||
)
|
||||
|
||||
# closing liability account
|
||||
self.closing_account_gle.append(
|
||||
self.get_gle_for_closing_account(account_balances["balances"], dimensions)
|
||||
)
|
||||
|
||||
return self.pl_accounts_reverse_gle + self.closing_account_gle
|
||||
|
||||
def get_gle_for_pl_account(self, acc, balances, dimensions):
|
||||
balance_in_account_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"posting_date": self.period_end_date,
|
||||
"account": acc,
|
||||
"account_currency": balances.account_currency,
|
||||
"debit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": self.name,
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"remarks": self.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"posting_date": self.period_end_date,
|
||||
"account": self.closing_account_head,
|
||||
"account_currency": frappe.db.get_value(
|
||||
"Account", self.closing_account_head, "account_currency"
|
||||
),
|
||||
"debit_in_account_currency": balance_in_account_currency
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": self.name,
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"remarks": self.remarks,
|
||||
"is_opening": "No",
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, dimensions)
|
||||
return gl_entry
|
||||
|
||||
def update_default_dimensions(self, gl_entry, dimensions):
|
||||
for i, dimension in enumerate(self.accounting_dimension_fields):
|
||||
gl_entry[dimension] = dimensions[i]
|
||||
|
||||
def get_account_balances_based_on_dimensions(self, report_type):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
self.get_accounting_dimension_fields()
|
||||
acc_bal_dict = frappe._dict()
|
||||
gl_entries = []
|
||||
|
||||
with frappe.db.unbuffered_cursor():
|
||||
gl_entries = self.get_gl_entries_for_current_period(report_type, as_iterator=True)
|
||||
for gle in gl_entries:
|
||||
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||
|
||||
if report_type == "Balance Sheet" and self.is_first_period_closing_voucher():
|
||||
opening_entries = self.get_gl_entries_for_current_period(report_type, only_opening_entries=True)
|
||||
for gle in opening_entries:
|
||||
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||
|
||||
return acc_bal_dict
|
||||
|
||||
def get_accounting_dimension_fields(self):
|
||||
default_dimensions = ["cost_center", "finance_book", "project"]
|
||||
self.accounting_dimension_fields = default_dimensions + get_accounting_dimensions()
|
||||
|
||||
def get_gl_entries_for_current_period(self, report_type, only_opening_entries=False, as_iterator=False):
|
||||
date_condition = ""
|
||||
if only_opening_entries:
|
||||
date_condition = "is_opening = 'Yes'"
|
||||
else:
|
||||
date_condition = f"posting_date BETWEEN '{self.period_start_date}' AND '{self.period_end_date}' and is_opening = 'No'"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name,
|
||||
posting_date,
|
||||
account,
|
||||
account_currency,
|
||||
debit_in_account_currency,
|
||||
credit_in_account_currency,
|
||||
debit,
|
||||
credit,
|
||||
{}
|
||||
FROM `tabGL Entry`
|
||||
WHERE
|
||||
{}
|
||||
AND company = %s
|
||||
AND voucher_type != 'Period Closing Voucher'
|
||||
AND EXISTS(SELECT name FROM `tabAccount` WHERE name = account AND report_type = %s)
|
||||
AND is_cancelled = 0
|
||||
""".format(
|
||||
", ".join(self.accounting_dimension_fields),
|
||||
date_condition,
|
||||
),
|
||||
(self.company, report_type),
|
||||
as_dict=1,
|
||||
as_iterator=as_iterator,
|
||||
)
|
||||
|
||||
def set_account_balance_dict(self, gle, acc_bal_dict):
|
||||
key = self.get_key(gle)
|
||||
|
||||
acc_bal_dict.setdefault(key, frappe._dict()).setdefault(
|
||||
gle.account,
|
||||
frappe._dict(
|
||||
{
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"credit": 0,
|
||||
"account_currency": gle.account_currency,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
acc_bal_dict[key][gle.account].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||
acc_bal_dict[key][gle.account].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||
acc_bal_dict[key][gle.account].debit += flt(gle.debit)
|
||||
acc_bal_dict[key][gle.account].credit += flt(gle.credit)
|
||||
|
||||
# dimension-wise total balances
|
||||
acc_bal_dict[key].setdefault(
|
||||
"balances",
|
||||
frappe._dict(
|
||||
{
|
||||
"balance_in_account_currency": 0,
|
||||
"balance_in_company_currency": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
balance_in_account_currency = flt(gle.debit_in_account_currency) - flt(gle.credit_in_account_currency)
|
||||
balance_in_company_currency = flt(gle.debit) - flt(gle.credit)
|
||||
|
||||
acc_bal_dict[key]["balances"].balance_in_account_currency += balance_in_account_currency
|
||||
acc_bal_dict[key]["balances"].balance_in_company_currency += balance_in_company_currency
|
||||
|
||||
return acc_bal_dict
|
||||
|
||||
def get_key(self, gle):
|
||||
return tuple([gle.get(dimension) for dimension in self.accounting_dimension_fields])
|
||||
|
||||
def get_account_closing_balances(self):
|
||||
pl_closing_entries = self.get_closing_entries_for_pl_accounts()
|
||||
bs_closing_entries = self.get_closing_entries_for_balance_sheet_accounts()
|
||||
closing_entries_for_closing_account = self.get_closing_entries_for_closing_account()
|
||||
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entries_for_pl_accounts(self):
|
||||
closing_entries = copy.deepcopy(self.pl_accounts_reverse_gle)
|
||||
for d in self.pl_accounts_reverse_gle:
|
||||
# reverse debit and credit
|
||||
gle_copy = copy.deepcopy(d)
|
||||
gle_copy.debit = d.credit
|
||||
gle_copy.credit = d.debit
|
||||
gle_copy.debit_in_account_currency = d.credit_in_account_currency
|
||||
gle_copy.credit_in_account_currency = d.debit_in_account_currency
|
||||
gle_copy.is_period_closing_voucher_entry = 0
|
||||
gle_copy.period_closing_voucher = self.name
|
||||
closing_entries.append(gle_copy)
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entries_for_balance_sheet_accounts(self):
|
||||
closing_entries = []
|
||||
balance_sheet_account_balances = self.get_account_balances_based_on_dimensions(
|
||||
report_type="Balance Sheet"
|
||||
)
|
||||
|
||||
for dimensions, account_balances in balance_sheet_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if acc != "balances" and balance_in_company_currency:
|
||||
closing_entries.append(self.get_closing_entry(acc, balances, dimensions))
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_closing_entry(self, account, balances, dimensions):
|
||||
closing_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.period_end_date,
|
||||
"period_closing_voucher": self.name,
|
||||
"account": account,
|
||||
"account_currency": balances.account_currency,
|
||||
"debit_in_account_currency": flt(balances.debit_in_account_currency),
|
||||
"debit": flt(balances.debit),
|
||||
"credit_in_account_currency": flt(balances.credit_in_account_currency),
|
||||
"credit": flt(balances.credit),
|
||||
"is_period_closing_voucher_entry": 0,
|
||||
}
|
||||
)
|
||||
self.update_default_dimensions(closing_entry, dimensions)
|
||||
return closing_entry
|
||||
|
||||
def get_closing_entries_for_closing_account(self):
|
||||
closing_entries = copy.deepcopy(self.closing_account_gle)
|
||||
for d in closing_entries:
|
||||
d.period_closing_voucher = self.name
|
||||
|
||||
return closing_entries
|
||||
|
||||
def is_first_period_closing_voucher(self):
|
||||
first_pcv = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
{"company": self.company, "docstatus": 1},
|
||||
"name",
|
||||
order_by="period_end_date asc",
|
||||
)
|
||||
|
||||
if not first_pcv or first_pcv == self.name:
|
||||
return True
|
||||
|
||||
def cancel_gl_entries(self):
|
||||
if self.get_gle_count_against_current_pcv() > 5000:
|
||||
frappe.enqueue(
|
||||
process_cancellation,
|
||||
voucher_type="Period Closing Voucher",
|
||||
@@ -73,308 +437,29 @@ class PeriodClosingVoucher(AccountsController):
|
||||
else:
|
||||
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
def validate_future_closing_vouchers(self):
|
||||
if frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
|
||||
)
|
||||
)
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
if closing_account_type not in ["Liability", "Equity"]:
|
||||
frappe.throw(
|
||||
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(self.closing_account_head)
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if account_currency != company_currency:
|
||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||
|
||||
def validate_posting_date(self):
|
||||
validate_fiscal_year(
|
||||
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
|
||||
)
|
||||
|
||||
self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
existing_entry = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(pcv.name)
|
||||
.where(
|
||||
(pcv.posting_date >= self.posting_date)
|
||||
& (pcv.fiscal_year == self.fiscal_year)
|
||||
& (pcv.docstatus == 1)
|
||||
& (pcv.company == self.company)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if existing_entry and existing_entry[0][0]:
|
||||
frappe.throw(
|
||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
||||
existing_entry[0][0], self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.year_start_date, -1)
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
if not frappe.db.exists(
|
||||
def get_gle_count_against_current_pcv(self):
|
||||
return frappe.db.count(
|
||||
"GL Entry",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
):
|
||||
return
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def make_gl_entries(self, get_opening_entries=False):
|
||||
gl_entries = self.get_gl_entries()
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if len(gl_entries + closing_entries) > 3000:
|
||||
frappe.enqueue(
|
||||
process_gl_and_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
company=self.company,
|
||||
closing_date=self.posting_date,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_and_closing_entries(
|
||||
gl_entries, closing_entries, self.name, self.company, self.posting_date
|
||||
)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
|
||||
):
|
||||
closing_entries.append(self.get_closing_entries(acc))
|
||||
|
||||
return closing_entries
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
|
||||
# pl account
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=True, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
# closing liability account
|
||||
for acc in self.get_balances_based_on_dimensions(
|
||||
group_by_account=False, report_type="Profit and Loss"
|
||||
):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_gle_for_pl_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
def get_closing_entries(self, acc):
|
||||
closing_entry = self.get_gl_dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"closing_date": self.posting_date,
|
||||
"period_closing_voucher": self.name,
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": flt(acc.debit_in_account_currency),
|
||||
"debit": flt(acc.debit),
|
||||
"credit_in_account_currency": flt(acc.credit_in_account_currency),
|
||||
"credit": flt(acc.credit),
|
||||
},
|
||||
item=acc,
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
)
|
||||
|
||||
for dimension in self.accounting_dimensions:
|
||||
closing_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
return closing_entry
|
||||
|
||||
def update_default_dimensions(self, gl_entry, acc):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
def get_balances_based_on_dimensions(
|
||||
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
|
||||
):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
qb_dimension_fields = ["cost_center", "finance_book", "project"]
|
||||
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
qb_dimension_fields.append(dimension)
|
||||
|
||||
if group_by_account:
|
||||
qb_dimension_fields.append("account")
|
||||
|
||||
account_filters = {
|
||||
"company": self.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
|
||||
if report_type:
|
||||
account_filters.update({"report_type": report_type})
|
||||
|
||||
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
|
||||
|
||||
if not for_aggregation:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
|
||||
"bal_in_account_currency"
|
||||
),
|
||||
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
|
||||
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
|
||||
(Sum(gl_entry.debit)).as_("debit"),
|
||||
(Sum(gl_entry.credit)).as_("credit"),
|
||||
)
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.select(gl_entry[dimension])
|
||||
|
||||
query = query.where(
|
||||
(gl_entry.company == self.company)
|
||||
& (gl_entry.is_cancelled == 0)
|
||||
& (gl_entry.account.isin(accounts))
|
||||
)
|
||||
|
||||
if get_opening_entries:
|
||||
query = query.where(
|
||||
( # noqa: UP034
|
||||
(gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date))
|
||||
| (gl_entry.is_opening == "Yes")
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
& gl_entry.is_opening
|
||||
== "No"
|
||||
)
|
||||
|
||||
if for_aggregation:
|
||||
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
|
||||
|
||||
for dimension in qb_dimension_fields:
|
||||
query = query.groupby(gl_entry[dimension])
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_and_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
def process_gl_and_closing_entries(doc):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
gl_entries = doc.get_pcv_gl_entries()
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
|
||||
closing_entries = doc.get_account_closing_balances()
|
||||
make_closing_entries(closing_entries, doc.name, doc.company, doc.period_end_date)
|
||||
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_cancellation(voucher_type, voucher_no):
|
||||
@@ -395,3 +480,29 @@ def delete_closing_entries(voucher_no):
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == voucher_no
|
||||
).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_period_start_end_date(fiscal_year, company):
|
||||
fy_start_date, fy_end_date = frappe.db.get_value(
|
||||
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
|
||||
)
|
||||
prev_closed_period_end_date = get_previous_closed_period_in_current_year(fiscal_year, company)
|
||||
period_start_date = (
|
||||
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else fy_start_date
|
||||
)
|
||||
return period_start_date, fy_end_date
|
||||
|
||||
|
||||
def get_previous_closed_period_in_current_year(fiscal_year, company):
|
||||
prev_closed_period_end_date = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"company": company,
|
||||
"fiscal_year": fiscal_year,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname=["period_end_date"],
|
||||
order_by="period_end_date desc",
|
||||
)
|
||||
return prev_closed_period_end_date
|
||||
|
||||
@@ -317,16 +317,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
repost_doc.posting_date = today()
|
||||
repost_doc.save()
|
||||
|
||||
def make_period_closing_voucher(self, posting_date=None, submit=True):
|
||||
def make_period_closing_voucher(self, posting_date, submit=True):
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
fy = get_fiscal_year(posting_date, company="Test PCV Company")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": fy[2],
|
||||
"company": "Test PCV Company",
|
||||
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
) {
|
||||
reset_values(frm);
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||
() => frm.trigger("set_opening_amounts"),
|
||||
() => frm.trigger("get_pos_invoices"),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
}
|
||||
},
|
||||
@@ -145,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_payments(doc, frm, false);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
@@ -170,7 +172,7 @@ function set_form_data(data, frm) {
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
frm.doc.net_total += flt(d.net_total);
|
||||
frm.doc.total_quantity += flt(d.total_qty);
|
||||
refresh_payments(d, frm);
|
||||
refresh_payments(d, frm, true);
|
||||
refresh_taxes(d, frm);
|
||||
});
|
||||
}
|
||||
@@ -184,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm) {
|
||||
function refresh_payments(d, frm, is_new) {
|
||||
d.payments.forEach((p) => {
|
||||
const payment = frm.doc.payment_reconciliation.find(
|
||||
(pay) => pay.mode_of_payment === p.mode_of_payment
|
||||
@@ -194,9 +196,7 @@ function refresh_payments(d, frm) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
if (payment.closing_amount === 0) {
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
}
|
||||
if (is_new) payment.closing_amount = payment.expected_amount;
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -57,6 +57,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
}
|
||||
|
||||
onload_post_render(frm) {
|
||||
super.onload_post_render();
|
||||
this.pos_profile(frm);
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
super.refresh();
|
||||
|
||||
if (doc.docstatus == 1 && !doc.is_return) {
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create"));
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1558,12 +1559,19 @@
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:00:34.268756",
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -32,12 +32,8 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
||||
SalesInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
||||
SalesInvoicePayment,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||
SalesInvoiceTimesheet,
|
||||
)
|
||||
@@ -75,6 +71,7 @@ class POSInvoice(SalesInvoice):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
inv.save()
|
||||
|
||||
self.assertEqual(inv.net_total, 4298.25)
|
||||
self.assertEqual(inv.net_total, 4298.24)
|
||||
self.assertEqual(inv.grand_total, 4900.00)
|
||||
|
||||
def test_tax_calculation_with_multiple_items(self):
|
||||
|
||||
@@ -438,7 +438,9 @@ def split_invoices(invoices):
|
||||
if not item.serial_no and not item.serial_and_batch_bundle:
|
||||
continue
|
||||
|
||||
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
|
||||
return_against_is_added = any(
|
||||
d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against
|
||||
)
|
||||
if return_against_is_added:
|
||||
break
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.status, "Return")
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -446,7 +446,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
if isinstance(pricing_rule, str):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
|
||||
pricing_rule.apply_rule_on_other_items = (
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
if args.coupon_code != coupon_code:
|
||||
continue
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -473,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
if pricing_rule.price_or_product_discount == "Price":
|
||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||
|
||||
@@ -1137,6 +1137,45 @@ class TestPricingRule(FrappeTestCase):
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_pricing_rule_for_product_free_item_round_free_qty(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate": 0,
|
||||
"min_qty": 100,
|
||||
"max_qty": 0,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 1,
|
||||
"free_qty": 10,
|
||||
"round_free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 100,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With pricing rule
|
||||
so = make_sales_order(item_code="_Test Item", qty=100)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=150)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
@@ -651,11 +651,20 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
qty = pricing_rule.free_qty or 1
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over
|
||||
if transaction_qty:
|
||||
transaction_qty = sum(
|
||||
[
|
||||
row.qty
|
||||
for row in doc.items
|
||||
if not row.is_free_item
|
||||
and row.item_code == args.item_code
|
||||
and row.pricing_rules == args.pricing_rules
|
||||
]
|
||||
)
|
||||
transaction_qty = transaction_qty - pricing_rule.apply_recursion_over
|
||||
if transaction_qty and transaction_qty > 0:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
@@ -728,14 +737,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
def validate_coupon_code(coupon_name):
|
||||
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
||||
|
||||
if coupon.valid_from:
|
||||
if coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto:
|
||||
if coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.used >= coupon.maximum_use:
|
||||
if coupon.valid_from and coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto and coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
|
||||
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"payment_terms_template",
|
||||
"sales_partner",
|
||||
"sales_person",
|
||||
"show_remarks",
|
||||
"based_on_payment_terms",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
@@ -390,10 +391,16 @@
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -70,6 +70,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
sales_person: DF.Link | None
|
||||
sender: DF.Link | None
|
||||
show_net_values_in_party_account: DF.Check
|
||||
show_remarks: DF.Check
|
||||
start_date: DF.Date | None
|
||||
subject: DF.Data | None
|
||||
terms_and_conditions: DF.Link | None
|
||||
@@ -187,6 +188,7 @@ def get_common_filters(doc):
|
||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||
"show_remarks": doc.show_remarks,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -472,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
reference_doctype="Process Statement Of Accounts",
|
||||
reference_name=document_name,
|
||||
attachments=attachments,
|
||||
expose_recipients="header",
|
||||
)
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
|
||||
@@ -31,6 +31,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onload() {
|
||||
@@ -335,7 +342,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
party_type: "Supplier",
|
||||
account: this.frm.doc.credit_to,
|
||||
price_list: this.frm.doc.buying_price_list,
|
||||
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template),
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
@@ -506,13 +515,6 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.set_query("expense_account", "items", function (doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
|
||||
cur_frm.set_query("wip_composite_asset", "items", function () {
|
||||
return {
|
||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||
|
||||
@@ -1134,12 +1134,14 @@
|
||||
"label": "Payment Terms"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_terms_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_schedule",
|
||||
"fieldtype": "Table",
|
||||
"label": "Payment Schedule",
|
||||
@@ -1631,7 +1633,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified": "2024-10-25 18:13:01.944477",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -863,6 +863,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
self.make_gl_entries_for_tax_withholding(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
@@ -896,32 +897,37 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
self.party_account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
def add_supplier_gl_entry(
|
||||
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
|
||||
):
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl = {
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": against_account or self.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
"_skip_merge": skip_merge,
|
||||
}
|
||||
|
||||
if remarks:
|
||||
gl["remarks"] = remarks
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
# item gl entries
|
||||
@@ -1413,6 +1419,31 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries_for_tax_withholding(self, gl_entries):
|
||||
"""
|
||||
Tax withholding amount is not part of supplier invoice.
|
||||
Separate supplier GL Entry for correct reporting.
|
||||
"""
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
for row in self.get("taxes"):
|
||||
if not row.is_tax_withholding_account or not row.tax_amount:
|
||||
continue
|
||||
|
||||
base_tds_amount = row.base_tax_amount_after_discount_amount
|
||||
tds_amount = row.tax_amount_after_discount_amount
|
||||
|
||||
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
|
||||
self.add_supplier_gl_entry(
|
||||
gl_entries,
|
||||
-base_tds_amount,
|
||||
-tds_amount,
|
||||
against_account=row.account_head,
|
||||
remarks=_("TDS Deducted"),
|
||||
skip_merge=True,
|
||||
)
|
||||
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
# Make Cash GL Entries
|
||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||
@@ -1506,10 +1537,29 @@ class PurchaseInvoice(BuyingController):
|
||||
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
|
||||
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
|
||||
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(self.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", self.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1593,7 +1643,11 @@ class PurchaseInvoice(BuyingController):
|
||||
for proj, value in projects.items():
|
||||
res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
||||
current_purchase_cost = res and res[0][0] or 0
|
||||
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
# frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
project_doc = frappe.get_doc("Project", proj)
|
||||
project_doc.total_purchase_cost = current_purchase_cost + value
|
||||
project_doc.calculate_gross_margin()
|
||||
project_doc.db_update()
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
|
||||
@@ -1544,6 +1544,61 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
payment_entry.load_from_db()
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_purchase_gl_with_tax_withholding_tax(self):
|
||||
company = "_Test Company"
|
||||
|
||||
tds_account_args = {
|
||||
"doctype": "Account",
|
||||
"account_name": "TDS Payable",
|
||||
"account_type": "Tax",
|
||||
"parent_account": frappe.db.get_value(
|
||||
"Account", {"account_name": "Duties and Taxes", "company": company}
|
||||
),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
tds_account = create_account(**tds_account_args)
|
||||
tax_withholding_category = "Test TDS - 194 - Dividends - Individual"
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
create_tax_witholding_category(tax_withholding_category, company, tds_account)
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test TDS Advance Supplier",
|
||||
tax_withholding_category=tax_withholding_category,
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
supplier=supplier.name,
|
||||
rate=3000,
|
||||
qty=1,
|
||||
item="_Test Non Stock Item",
|
||||
do_not_submit=1,
|
||||
)
|
||||
pi.apply_tds = 1
|
||||
pi.tax_withholding_category = tax_withholding_category
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pi.taxes[0].tax_amount, 300)
|
||||
self.assertEqual(pi.taxes[0].account_head, tds_account)
|
||||
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"},
|
||||
fields=["account", "against", "debit", "credit"],
|
||||
)
|
||||
|
||||
for gle in gl_entries:
|
||||
if gle.debit:
|
||||
# GL Entry with TDS Amount
|
||||
self.assertEqual(gle.against, tds_account)
|
||||
self.assertEqual(gle.debit, 300)
|
||||
else:
|
||||
# GL Entry with Purchase Invoice Amount
|
||||
self.assertEqual(gle.credit, 3000)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
@@ -1680,6 +1735,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
@@ -2310,6 +2389,65 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
pi.items[0].qty = 1
|
||||
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||
pi.is_opening = "Yes"
|
||||
pi.save()
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
def _create_opening_roundoff_account(self, company_name):
|
||||
liability_root = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||
order_by="lft",
|
||||
limit=1,
|
||||
)[0]
|
||||
|
||||
# setup round off account
|
||||
if acc := frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Round Off for Opening",
|
||||
"account_type": "Round Off for Opening",
|
||||
"company": company_name,
|
||||
},
|
||||
):
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||
else:
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.company = company_name
|
||||
acc.parent_account = liability_root.name
|
||||
acc.account_name = "Round Off for Opening"
|
||||
acc.account_type = "Round Off for Opening"
|
||||
acc.save()
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||
|
||||
def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
pi.items[0].qty = 1
|
||||
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||
pi.is_opening = "Yes"
|
||||
pi.save()
|
||||
self._create_opening_roundoff_account(pi.company)
|
||||
pi.submit()
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
order_by="account,debit",
|
||||
)
|
||||
expected = [
|
||||
{"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"},
|
||||
{"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"},
|
||||
{"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"},
|
||||
]
|
||||
self.assertEqual(len(actual), 3)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -505,7 +505,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -974,7 +975,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified": "2024-10-28 15:06:19.246141",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -45,9 +45,9 @@ class RepostAccountingLedger(Document):
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
order_by="posting_date desc",
|
||||
pluck="posting_date",
|
||||
filters={"company": self.company, "docstatus": 1},
|
||||
order_by="period_end_date desc",
|
||||
pluck="period_end_date",
|
||||
limit=1,
|
||||
)
|
||||
or None
|
||||
|
||||
@@ -129,13 +129,15 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
fy = get_fiscal_year(today(), company=self.company)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"posting_date": today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": today(),
|
||||
"company": self.company,
|
||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"remarks": "test",
|
||||
|
||||
@@ -339,6 +339,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
account: this.frm.doc.debit_to,
|
||||
price_list: this.frm.doc.selling_price_list,
|
||||
pos_profile: pos_profile,
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
@@ -738,20 +741,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
};
|
||||
|
||||
frm.set_query("company_address", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: {
|
||||
link_doctype: "Company",
|
||||
link_name: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("pos_profile", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
|
||||
@@ -159,8 +159,9 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_addr_col_break",
|
||||
"company_address_display",
|
||||
"company_addr_col_break",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -2166,6 +2167,13 @@
|
||||
"label": "Update Outstanding for Self",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2178,7 +2186,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2233,4 +2241,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ class SalesInvoice(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
company_tax_id: DF.Data | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -297,8 +298,11 @@ class SalesInvoice(SellingController):
|
||||
self.update_current_stock()
|
||||
self.validate_delivery_note()
|
||||
|
||||
is_deferred_invoice = any(d.get("enable_deferred_revenue") for d in self.get("items"))
|
||||
|
||||
# validate service stop date to lie in between start and end date
|
||||
validate_service_stop_date(self)
|
||||
if is_deferred_invoice:
|
||||
validate_service_stop_date(self)
|
||||
|
||||
if not self.is_opening:
|
||||
self.is_opening = "No"
|
||||
@@ -1359,14 +1363,15 @@ class SalesInvoice(SellingController):
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
if not asset.status == "Fully Depreciated":
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
@@ -1629,10 +1634,29 @@ class SalesInvoice(SellingController):
|
||||
and self.base_rounding_adjustment
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(self.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", self.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1730,9 +1754,14 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
def update_project(self):
|
||||
if self.project:
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
unique_projects.append(self.project)
|
||||
|
||||
for p in unique_projects:
|
||||
project = frappe.get_doc("Project", p)
|
||||
project.update_billed_amount()
|
||||
project.calculate_gross_margin()
|
||||
project.db_update()
|
||||
|
||||
def verify_payment_amount_is_positive(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Draft: "grey",
|
||||
Draft: "red",
|
||||
Unpaid: "orange",
|
||||
Paid: "green",
|
||||
Return: "gray",
|
||||
|
||||
@@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.insert()
|
||||
|
||||
# with inclusive tax
|
||||
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
|
||||
self.assertEqual(si.items[0].net_amount, 3947.37)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 3947.37)
|
||||
self.assertEqual(si.grand_total, 5000)
|
||||
|
||||
@@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
62.5,
|
||||
625.0,
|
||||
50,
|
||||
499.97600115194473,
|
||||
499.98,
|
||||
],
|
||||
"_Test Item Home Desktop 200": [
|
||||
190.66,
|
||||
@@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
190.66,
|
||||
953.3,
|
||||
150,
|
||||
749.9968530500239,
|
||||
750,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||
|
||||
# check net total
|
||||
self.assertEqual(si.net_total, 1249.97)
|
||||
self.assertEqual(si.base_net_total, si.net_total)
|
||||
self.assertEqual(si.net_total, 1249.98)
|
||||
self.assertEqual(si.total, 1578.3)
|
||||
|
||||
# check tax calculation
|
||||
expected_values = {
|
||||
"keys": ["tax_amount", "total"],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 2500,
|
||||
"base_amount": 25000,
|
||||
"net_rate": 40,
|
||||
"net_amount": 399.9808009215558,
|
||||
"net_amount": 399.98,
|
||||
"base_net_rate": 2000,
|
||||
"base_net_amount": 19999,
|
||||
},
|
||||
@@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 7500,
|
||||
"base_amount": 37500,
|
||||
"net_rate": 118.01,
|
||||
"net_amount": 590.0531205155963,
|
||||
"net_amount": 590.05,
|
||||
"base_net_rate": 5900.5,
|
||||
"base_net_amount": 29502.5,
|
||||
},
|
||||
@@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(si.base_grand_total, 60795)
|
||||
self.assertEqual(si.grand_total, 1215.90)
|
||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
||||
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
|
||||
self.assertEqual(si.rounding_adjustment, 0.10)
|
||||
self.assertEqual(si.base_rounding_adjustment, 5.0)
|
||||
else:
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.0)
|
||||
|
||||
def test_outstanding(self):
|
||||
w = self.make()
|
||||
@@ -1995,7 +2002,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(sales_order.advance_paid, 0.0)
|
||||
self.assertEqual(sales_order.advance_paid, 300.0)
|
||||
|
||||
# check outstanding after advance allocation
|
||||
self.assertEqual(
|
||||
@@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
def test_rounding_adjustment_2(self):
|
||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
||||
for rate in [400, 600, 100]:
|
||||
for rate in [400.25, 600.30, 100.65]:
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 1271.19)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 1272.20)
|
||||
self.assertEqual(si.grand_total, 1501.20)
|
||||
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = {
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
||||
si.debit_to: [1500, 0.0],
|
||||
round_off_account: [0.01, 0.01],
|
||||
"Sales - _TC": [0.0, 1271.18],
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.50],
|
||||
"_Test Account VAT - _TC": [0.0, 114.50],
|
||||
si.debit_to: [1501, 0.0],
|
||||
round_off_account: [0.20, 0.0],
|
||||
"Sales - _TC": [0.0, 1272.20],
|
||||
}
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 4007.15)
|
||||
self.assertEqual(si.grand_total, 4488.02)
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
@@ -2280,7 +2289,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
[round_off_account, 0.02, 0.01],
|
||||
[round_off_account, 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -4005,6 +4014,223 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.submit()
|
||||
self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}")
|
||||
|
||||
def test_gl_voucher_subtype(self):
|
||||
si = create_sales_invoice()
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||
pluck="voucher_subtype",
|
||||
)
|
||||
|
||||
self.assertTrue(all([x == "Sales Invoice" for x in gl_entries]))
|
||||
|
||||
si = create_sales_invoice(is_return=1, qty=-1)
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||
pluck="voucher_subtype",
|
||||
)
|
||||
|
||||
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
|
||||
|
||||
def test_validation_on_opening_invoice_with_rounding(self):
|
||||
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
si.save()
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def _create_opening_roundoff_account(self, company_name):
|
||||
liability_root = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||
order_by="lft",
|
||||
limit=1,
|
||||
)[0]
|
||||
|
||||
# setup round off account
|
||||
if acc := frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Round Off for Opening",
|
||||
"account_type": "Round Off for Opening",
|
||||
"company": company_name,
|
||||
},
|
||||
):
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||
else:
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.company = company_name
|
||||
acc.parent_account = liability_root.name
|
||||
acc.account_name = "Round Off for Opening"
|
||||
acc.account_type = "Round Off for Opening"
|
||||
acc.save()
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||
|
||||
def test_opening_invoice_with_rounding_adjustment(self):
|
||||
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
si.save()
|
||||
|
||||
self._create_opening_roundoff_account(si.company)
|
||||
|
||||
si.reload()
|
||||
si.submit()
|
||||
res = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": si.name, "is_opening": "Yes"},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
)
|
||||
self.assertEqual(len(res), 3)
|
||||
|
||||
def _create_opening_invoice_with_inclusive_tax(self):
|
||||
si = create_sales_invoice(qty=1, rate=90, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
item_template = si.items[0].as_dict()
|
||||
item_template.name = None
|
||||
item_template.rate = 55
|
||||
si.append("items", item_template)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Testing...",
|
||||
"rate": 5,
|
||||
"included_in_print_rate": True,
|
||||
},
|
||||
)
|
||||
# there will be 0.01 precision loss between Dr and Cr
|
||||
# caused by 'included_in_print_tax' option
|
||||
si.save()
|
||||
return si
|
||||
|
||||
def test_rounding_validation_for_opening_with_inclusive_tax(self):
|
||||
si = self._create_opening_invoice_with_inclusive_tax()
|
||||
# 'Round Off for Opening' not set in Company master
|
||||
# Ledger level validation must be thrown
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self):
|
||||
si = self._create_opening_invoice_with_inclusive_tax()
|
||||
# 'Round Off for Opening' is set in Company master
|
||||
self._create_opening_roundoff_account(si.company)
|
||||
|
||||
si.submit()
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
order_by="account,debit",
|
||||
)
|
||||
expected = [
|
||||
{"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"},
|
||||
{"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"},
|
||||
{"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"},
|
||||
{"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"},
|
||||
]
|
||||
self.assertEqual(len(actual), 4)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors INR",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="INR",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party INR").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "INR"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
def test_total_billed_amount(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -89,11 +89,14 @@
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_eoec",
|
||||
"serial_no",
|
||||
"column_break_ytgd",
|
||||
"batch_no",
|
||||
"available_quantity_section",
|
||||
"actual_qty",
|
||||
"column_break_ogff",
|
||||
"company_total_stock",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@@ -675,7 +678,8 @@
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Available Qty at Warehouse",
|
||||
"label": "Qty (Warehouse)",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "actual_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
@@ -812,7 +816,8 @@
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"options": "Project",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
@@ -922,12 +927,30 @@
|
||||
{
|
||||
"fieldname": "column_break_ytgd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_quantity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Available Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ogff",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "company_total_stock",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty (Company)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified": "2024-11-25 16:27:33.287341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -28,6 +28,7 @@ class SalesInvoiceItem(Document):
|
||||
base_rate_with_margin: DF.Currency
|
||||
batch_no: DF.Link | None
|
||||
brand: DF.Data | None
|
||||
company_total_stock: DF.Float
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link
|
||||
customer_item_code: DF.Data | None
|
||||
|
||||
@@ -514,7 +514,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
@@ -538,7 +538,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
group_by="payment_type",
|
||||
)
|
||||
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
@@ -556,19 +556,19 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
|
||||
or 0.0
|
||||
)
|
||||
net_total += inv.tax_withholding_net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -74,11 +74,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
self.assertEqual(pi.grand_total, 18000)
|
||||
|
||||
# check gl entry for the purchase invoice
|
||||
gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"])
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name},
|
||||
fields=["account", "sum(debit) as debit", "sum(credit) as credit"],
|
||||
group_by="account",
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 3)
|
||||
for d in gl_entries:
|
||||
if d.account == pi.credit_to:
|
||||
self.assertEqual(d.credit, 18000)
|
||||
self.assertEqual(d.credit, 20000)
|
||||
self.assertEqual(d.debit, 2000)
|
||||
elif d.account == pi.items[0].get("expense_account"):
|
||||
self.assertEqual(d.debit, 20000)
|
||||
elif d.account == pi.taxes[0].get("account_head"):
|
||||
@@ -121,6 +127,85 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self):
|
||||
invoices = []
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance TDS Category"
|
||||
)
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=1000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=6000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 800)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_tax_on_excess_amount(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertTrue(len(pi1.taxes) > 0)
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
|
||||
@@ -262,6 +262,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe1.paid_from = self.debtors_usd
|
||||
pe1.paid_from_account_currency = "USD"
|
||||
pe1.source_exchange_rate = 75
|
||||
pe1.paid_amount = 100
|
||||
pe1.received_amount = 75 * 100
|
||||
pe1.save()
|
||||
# Allocate payment against both invoices
|
||||
@@ -279,6 +280,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe2.paid_from = self.debtors_usd
|
||||
pe2.paid_from_account_currency = "USD"
|
||||
pe2.source_exchange_rate = 75
|
||||
pe2.paid_amount = 100
|
||||
pe2.received_amount = 75 * 100
|
||||
pe2.save()
|
||||
# Allocate payment against both invoices
|
||||
@@ -362,10 +364,14 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
pe.cancel()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
@@ -411,7 +417,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 0)
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
@@ -459,6 +465,6 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
@@ -7,7 +7,7 @@ import copy
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||
from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -37,13 +37,14 @@ def make_gl_entries(
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@@ -116,17 +117,16 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
||||
def validate_disabled_accounts(gl_map):
|
||||
accounts = [d.account for d in gl_map if d.account]
|
||||
|
||||
Account = frappe.qb.DocType("Account")
|
||||
disabled_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
disabled_accounts = (
|
||||
frappe.qb.from_(Account)
|
||||
.where(Account.name.isin(accounts) & Account.disabled == 1)
|
||||
.select(Account.name, Account.disabled)
|
||||
).run(as_dict=True)
|
||||
|
||||
if disabled_accounts:
|
||||
used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts]))
|
||||
if used_disabled_accounts:
|
||||
account_list = "<br>"
|
||||
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
|
||||
account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts])
|
||||
frappe.throw(
|
||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||
title=_("Disabled Account Selected"),
|
||||
@@ -234,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
merge_properties = get_merge_properties(accounting_dimensions)
|
||||
|
||||
for entry in gl_map:
|
||||
if entry._skip_merge:
|
||||
merged_gl_map.append(entry)
|
||||
continue
|
||||
|
||||
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||
# if there is already an entry in this account then just add it
|
||||
# to that entry
|
||||
@@ -311,66 +315,48 @@ def check_if_in_list(gle, gl_map):
|
||||
|
||||
|
||||
def toggle_debit_credit_if_negative(gl_map):
|
||||
debit_credit_field_map = {
|
||||
"debit": "credit",
|
||||
"debit_in_account_currency": "credit_in_account_currency",
|
||||
"debit_in_transaction_currency": "credit_in_transaction_currency",
|
||||
}
|
||||
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
|
||||
entry.credit *= -1
|
||||
entry.debit *= -1
|
||||
for debit_field, credit_field in debit_credit_field_map.items():
|
||||
debit = flt(entry.get(debit_field))
|
||||
credit = flt(entry.get(credit_field))
|
||||
|
||||
if (
|
||||
flt(entry.debit_in_account_currency) < 0
|
||||
and flt(entry.credit_in_account_currency) < 0
|
||||
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
|
||||
):
|
||||
entry.credit_in_account_currency *= -1
|
||||
entry.debit_in_account_currency *= -1
|
||||
if debit < 0 and credit < 0 and debit == credit:
|
||||
debit *= -1
|
||||
credit *= -1
|
||||
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
if debit < 0:
|
||||
credit = credit - debit
|
||||
debit = 0.0
|
||||
|
||||
if flt(entry.debit_in_account_currency) < 0:
|
||||
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
|
||||
entry.debit_in_account_currency
|
||||
)
|
||||
entry.debit_in_account_currency = 0.0
|
||||
if credit < 0:
|
||||
debit = debit - credit
|
||||
credit = 0.0
|
||||
|
||||
if flt(entry.credit) < 0:
|
||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
||||
entry.credit = 0.0
|
||||
# update net values
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and debit and credit:
|
||||
if debit > credit:
|
||||
debit = debit - credit
|
||||
credit = 0.0
|
||||
|
||||
if flt(entry.credit_in_account_currency) < 0:
|
||||
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
|
||||
entry.credit_in_account_currency
|
||||
)
|
||||
entry.credit_in_account_currency = 0.0
|
||||
else:
|
||||
credit = credit - debit
|
||||
debit = 0.0
|
||||
|
||||
update_net_values(entry)
|
||||
entry[debit_field] = debit
|
||||
entry[credit_field] = credit
|
||||
|
||||
return gl_map
|
||||
|
||||
|
||||
def update_net_values(entry):
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and entry.debit and entry.credit:
|
||||
if entry.debit > entry.credit:
|
||||
entry.debit = entry.debit - entry.credit
|
||||
entry.debit_in_account_currency = (
|
||||
entry.debit_in_account_currency - entry.credit_in_account_currency
|
||||
)
|
||||
entry.credit = 0
|
||||
entry.credit_in_account_currency = 0
|
||||
else:
|
||||
entry.credit = entry.credit - entry.debit
|
||||
entry.credit_in_account_currency = (
|
||||
entry.credit_in_account_currency - entry.debit_in_account_currency
|
||||
)
|
||||
|
||||
entry.debit = 0
|
||||
entry.debit_in_account_currency = 0
|
||||
|
||||
|
||||
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
if not from_repost:
|
||||
validate_cwip_accounts(gl_map)
|
||||
@@ -492,16 +478,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
|
||||
)
|
||||
|
||||
|
||||
def has_opening_entries(gl_map: list) -> bool:
|
||||
for x in gl_map:
|
||||
if x.is_opening == "Yes":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
round_off_gle = frappe._dict()
|
||||
round_off_account_exists = False
|
||||
has_opening_entry = has_opening_entries(gl_map)
|
||||
|
||||
if has_opening_entry:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_("Please set '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company)
|
||||
)
|
||||
)
|
||||
|
||||
account = round_off_for_opening
|
||||
else:
|
||||
account = round_off_account
|
||||
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
for d in gl_map:
|
||||
if d.account == round_off_account:
|
||||
if d.account == account:
|
||||
round_off_gle = d
|
||||
if d.debit:
|
||||
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
||||
@@ -519,7 +525,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
|
||||
round_off_gle.update(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"account": account,
|
||||
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
@@ -533,6 +539,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
}
|
||||
)
|
||||
|
||||
if has_opening_entry:
|
||||
round_off_gle.update({"is_opening": "Yes"})
|
||||
|
||||
update_accounting_dimensions(round_off_gle)
|
||||
if not round_off_account_exists:
|
||||
gl_map.append(round_off_gle)
|
||||
@@ -557,9 +566,9 @@ def update_accounting_dimensions(round_off_gle):
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||
) or [None, None]
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"]
|
||||
) or [None, None, None]
|
||||
|
||||
# Use expense account as fallback
|
||||
if not round_off_account:
|
||||
@@ -574,12 +583,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use
|
||||
round_off_cost_center = parent_cost_center
|
||||
|
||||
if not round_off_account:
|
||||
frappe.throw(_("Please mention Round Off Account in Company"))
|
||||
frappe.throw(
|
||||
_("Please mention '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off Account"), get_link_to_form("Company", company)
|
||||
)
|
||||
)
|
||||
|
||||
if not round_off_cost_center:
|
||||
frappe.throw(_("Please mention Round Off Cost Center in Company"))
|
||||
frappe.throw(
|
||||
_("Please mention '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company)
|
||||
)
|
||||
)
|
||||
|
||||
return round_off_account, round_off_cost_center
|
||||
return round_off_account, round_off_cost_center, round_off_for_opening
|
||||
|
||||
|
||||
def make_reverse_gl_entries(
|
||||
@@ -708,7 +725,7 @@ def validate_against_pcv(is_opening, posting_date, company):
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Invoice",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Bills",
|
||||
"modified": "2020-07-22 13:06:46.045344",
|
||||
"modified": "2024-11-20 19:08:37.043777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Bills",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Invoice",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render_address
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render_address
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
@@ -881,16 +887,17 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
|
||||
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.party, Abs(Sum(ple.amount).as_("amount")))
|
||||
.where(
|
||||
(ple.party_type.isin(party_type))
|
||||
& (ple.amount < 0)
|
||||
& (ple.against_voucher_no == ple.voucher_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
.inner_join(acc)
|
||||
.on(ple.account == acc.name)
|
||||
.select(ple.party)
|
||||
.where((ple.party_type.isin(party_type)) & (acc.account_type == account_type) & (ple.delinked == 0))
|
||||
.groupby(ple.party)
|
||||
)
|
||||
|
||||
@@ -909,9 +916,32 @@ def get_partywise_advanced_payment_amount(
|
||||
if invoice_doctypes := frappe.get_hooks("invoice_doctypes"):
|
||||
query = query.where(ple.voucher_type.notin(invoice_doctypes))
|
||||
|
||||
data = query.run()
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
# Get advance amount from Receivable / Payable Account
|
||||
party_ledger = query.select(Abs(Sum(ple.amount).as_("amount")))
|
||||
party_ledger = party_ledger.where(ple.amount < 0)
|
||||
party_ledger = party_ledger.where(ple.against_voucher_no == ple.voucher_no)
|
||||
party_ledger = party_ledger.where(
|
||||
acc.root_type == ("Liability" if account_type == "Payable" else "Asset")
|
||||
)
|
||||
|
||||
data = party_ledger.run()
|
||||
data = frappe._dict(data or {})
|
||||
|
||||
# Get advance amount from Advance Account
|
||||
advance_ledger = query.select(Sum(ple.amount).as_("amount"), ple.account)
|
||||
advance_ledger = advance_ledger.where(
|
||||
acc.root_type == ("Asset" if account_type == "Payable" else "Liability")
|
||||
)
|
||||
advance_ledger = advance_ledger.groupby(ple.account)
|
||||
advance_ledger = advance_ledger.having(Sum(ple.amount) < 0)
|
||||
|
||||
advance_data = advance_ledger.run()
|
||||
|
||||
for row in advance_data:
|
||||
data.setdefault(row[0], 0)
|
||||
data[row[0]] += abs(row[1])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_default_contact(doctype: str, name: str) -> str | None:
|
||||
@@ -958,10 +988,4 @@ def add_party_account(party_type, party, company, account):
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
return frappe.call(_render_address, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -385,6 +385,7 @@ class ReceivablePayableReport:
|
||||
self.delivery_notes = frappe._dict()
|
||||
|
||||
# delivery note link inside sales invoice
|
||||
# nosemgrep
|
||||
si_against_dn = frappe.db.sql(
|
||||
"""
|
||||
select parent, delivery_note
|
||||
@@ -400,6 +401,7 @@ class ReceivablePayableReport:
|
||||
if d.delivery_note:
|
||||
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
|
||||
|
||||
# nosemgrep
|
||||
dn_against_si = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, against_sales_invoice
|
||||
@@ -417,13 +419,16 @@ class ReceivablePayableReport:
|
||||
def get_invoice_details(self):
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.account_type == "Receivable":
|
||||
# nosemgrep
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
from `tabSales Invoice`
|
||||
where posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
)
|
||||
for d in si_list:
|
||||
@@ -431,6 +436,7 @@ class ReceivablePayableReport:
|
||||
|
||||
# Get Sales Team
|
||||
if self.filters.show_sales_person:
|
||||
# nosemgrep
|
||||
sales_team = frappe.db.sql(
|
||||
"""
|
||||
select parent, sales_person
|
||||
@@ -445,25 +451,33 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
if self.account_type == "Payable":
|
||||
# nosemgrep
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabPurchase Invoice`
|
||||
where posting_date <= %s
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
):
|
||||
self.invoice_details.setdefault(pi.name, pi)
|
||||
|
||||
# Invoices booked via Journal Entries
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
from `tabJournal Entry`
|
||||
where posting_date <= %s
|
||||
where
|
||||
posting_date <= %s
|
||||
and company = %s
|
||||
and docstatus = 1
|
||||
""",
|
||||
self.filters.report_date,
|
||||
(self.filters.report_date, self.filters.company),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -472,6 +486,8 @@ class ReceivablePayableReport:
|
||||
self.invoice_details.setdefault(je.name, je)
|
||||
|
||||
def set_party_details(self, row):
|
||||
if not row.party:
|
||||
return
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
@@ -496,6 +512,7 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_payment_terms(self, row):
|
||||
# build payment_terms for row
|
||||
# nosemgrep
|
||||
payment_terms_details = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -505,7 +522,8 @@ class ReceivablePayableReport:
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = %s
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
""",
|
||||
row.voucher_no,
|
||||
@@ -708,6 +726,7 @@ class ReceivablePayableReport:
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
filters = {
|
||||
"posting_date": ("<=", self.filters.report_date),
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"company": self.filters.company,
|
||||
@@ -815,6 +834,7 @@ class ReceivablePayableReport:
|
||||
if self.filters.get("sales_person"):
|
||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||
|
||||
# nosemgrep
|
||||
records = frappe.db.sql(
|
||||
"""
|
||||
select distinct parent, parenttype
|
||||
@@ -993,22 +1013,29 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label="Party Type",
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
if self.account_type == "Receivable":
|
||||
label = _("Receivable Account")
|
||||
elif self.account_type == "Payable":
|
||||
label = _("Payable Account")
|
||||
else:
|
||||
label = _("Party Account")
|
||||
|
||||
self.add_column(
|
||||
label=self.account_type + " Account",
|
||||
label=label,
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -1017,10 +1044,10 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
label = _("Supplier Name")
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
label = _("Customer Name")
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
label=label,
|
||||
@@ -1046,7 +1073,7 @@ class ReceivablePayableReport:
|
||||
width=180,
|
||||
)
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
|
||||
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
|
||||
@@ -89,7 +89,9 @@ def get_data(filters):
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
asset_data.accumulated_depreciation_amount = (
|
||||
query[0]["accumulated_depreciation_amount"] if query else 0
|
||||
)
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -101,6 +102,9 @@ def execute(filters=None):
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
return columns, data, message, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
@@ -122,13 +126,13 @@ def get_provisional_profit_loss(
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
total_assets = flt(asset[0].get(key))
|
||||
total_assets = flt(asset[-2].get(key))
|
||||
effective_liability = 0.00
|
||||
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
if liability and liability[-1] == {}:
|
||||
effective_liability += flt(liability[-2].get(key))
|
||||
if equity and equity[-1] == {}:
|
||||
effective_liability += flt(equity[-2].get(key))
|
||||
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||
@@ -195,9 +199,9 @@ def get_report_summary(
|
||||
key = period if consolidated else period.key
|
||||
if asset:
|
||||
net_asset += asset[-2].get(key)
|
||||
if liability:
|
||||
if liability and liability[-1] == {}:
|
||||
net_liability += liability[-2].get(key)
|
||||
if equity:
|
||||
if equity and equity[-1] == {}:
|
||||
net_equity += equity[-2].get(key)
|
||||
if provisional_profit_loss:
|
||||
net_provisional_profit_loss += provisional_profit_loss.get(key)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements);
|
||||
frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, {
|
||||
name_field: "section",
|
||||
parent_field: "parent_section",
|
||||
});
|
||||
|
||||
erpnext.utils.add_dimensions("Cash Flow", 10);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def execute(filters=None):
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
cash_flow_accounts = get_cash_flow_accounts()
|
||||
cash_flow_sections = get_cash_flow_accounts()
|
||||
|
||||
# compute net profit / loss
|
||||
income = get_data(
|
||||
@@ -60,14 +60,14 @@ def execute(filters=None):
|
||||
summary_data = {}
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
|
||||
for cash_flow_account in cash_flow_accounts:
|
||||
for cash_flow_section in cash_flow_sections:
|
||||
section_data = []
|
||||
data.append(
|
||||
{
|
||||
"account_name": cash_flow_account["section_header"],
|
||||
"parent_account": None,
|
||||
"section_name": "'" + cash_flow_section["section_header"] + "'",
|
||||
"parent_section": None,
|
||||
"indent": 0.0,
|
||||
"account": cash_flow_account["section_header"],
|
||||
"section": cash_flow_section["section_header"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,31 +75,40 @@ def execute(filters=None):
|
||||
# add first net income in operations section
|
||||
if net_profit_loss:
|
||||
net_profit_loss.update(
|
||||
{"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]}
|
||||
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]}
|
||||
)
|
||||
data.append(net_profit_loss)
|
||||
section_data.append(net_profit_loss)
|
||||
|
||||
for account in cash_flow_account["account_types"]:
|
||||
account_data = get_account_type_based_data(
|
||||
filters.company, account["account_type"], period_list, filters.accumulated_values, filters
|
||||
for row in cash_flow_section["account_types"]:
|
||||
row_data = get_account_type_based_data(
|
||||
filters.company, row["account_type"], period_list, filters.accumulated_values, filters
|
||||
)
|
||||
account_data.update(
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={
|
||||
"account_type": row["account_type"],
|
||||
"is_group": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
row_data.update(
|
||||
{
|
||||
"account_name": account["label"],
|
||||
"account": account["label"],
|
||||
"section_name": row["label"],
|
||||
"section": row["label"],
|
||||
"indent": 1,
|
||||
"parent_account": cash_flow_account["section_header"],
|
||||
"accounts": accounts,
|
||||
"parent_section": cash_flow_section["section_header"],
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
data.append(account_data)
|
||||
section_data.append(account_data)
|
||||
data.append(row_data)
|
||||
section_data.append(row_data)
|
||||
|
||||
add_total_row_account(
|
||||
data,
|
||||
section_data,
|
||||
cash_flow_account["section_footer"],
|
||||
cash_flow_section["section_footer"],
|
||||
period_list,
|
||||
company_currency,
|
||||
summary_data,
|
||||
@@ -109,7 +118,7 @@ def execute(filters=None):
|
||||
add_total_row_account(
|
||||
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
@@ -217,8 +226,8 @@ def get_start_date(period, accumulated_values, company):
|
||||
|
||||
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
|
||||
total_row = {
|
||||
"account_name": "'" + _("{0}").format(label) + "'",
|
||||
"account": "'" + _("{0}").format(label) + "'",
|
||||
"section_name": "'" + _("{0}").format(label) + "'",
|
||||
"section": "'" + _("{0}").format(label) + "'",
|
||||
"currency": currency,
|
||||
}
|
||||
|
||||
@@ -229,7 +238,7 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for row in data:
|
||||
if row.get("parent_account"):
|
||||
if row.get("parent_section"):
|
||||
for period in period_list:
|
||||
key = period if consolidated else period["key"]
|
||||
total_row.setdefault(key, 0.0)
|
||||
@@ -254,13 +263,14 @@ def get_report_summary(summary_data, currency):
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
print(data)
|
||||
datasets = [
|
||||
{
|
||||
"name": account.get("account").replace("'", ""),
|
||||
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
|
||||
"name": section.get("section").replace("'", ""),
|
||||
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
|
||||
}
|
||||
for account in data
|
||||
if account.get("parent_account") is None and account.get("currency")
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
]
|
||||
datasets = datasets[:-1]
|
||||
|
||||
|
||||
@@ -122,21 +122,24 @@ class Deferred_Item:
|
||||
"""
|
||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||
"""
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
if (
|
||||
self.service_start_date != self.service_end_date
|
||||
and add_days(self.last_entry_date, 1) < self.service_end_date
|
||||
):
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.service_end_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
@@ -9,6 +10,7 @@ import re
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -181,12 +183,12 @@ def get_data(
|
||||
company,
|
||||
period_list[0]["year_start_date"] if only_current_fiscal_year else None,
|
||||
period_list[-1]["to_date"],
|
||||
root.lft,
|
||||
root.rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root.lft,
|
||||
root.rgt,
|
||||
root_type=root_type,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
@@ -333,8 +335,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False
|
||||
|
||||
def add_total_row(out, root_type, balance_must_be, period_list, company_currency):
|
||||
total_row = {
|
||||
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"currency": company_currency,
|
||||
"opening_balance": 0.0,
|
||||
}
|
||||
@@ -419,93 +421,78 @@ def set_gl_entries_by_account(
|
||||
company,
|
||||
from_date,
|
||||
to_date,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
root_type=None,
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
|
||||
account_filters = {
|
||||
"company": company,
|
||||
"is_group": 0,
|
||||
"lft": (">=", root_lft),
|
||||
"rgt": ("<=", root_rgt),
|
||||
}
|
||||
|
||||
if root_type:
|
||||
account_filters.update(
|
||||
{
|
||||
"root_type": root_type,
|
||||
}
|
||||
# For balance sheet
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
if not from_date and not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": filters.company,
|
||||
"period_end_date": ("<", filters["period_start_date"]),
|
||||
},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_period_closing_voucher:
|
||||
gl_entries += get_accounting_entries(
|
||||
"Account Closing Balance",
|
||||
from_date,
|
||||
to_date,
|
||||
filters,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
root_type,
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
|
||||
accounts_list = frappe.db.get_all(
|
||||
"Account",
|
||||
filters=account_filters,
|
||||
pluck="name",
|
||||
gl_entries += get_accounting_entries(
|
||||
"GL Entry",
|
||||
from_date,
|
||||
to_date,
|
||||
filters,
|
||||
root_lft,
|
||||
root_rgt,
|
||||
root_type,
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
)
|
||||
|
||||
if accounts_list:
|
||||
# For balance sheet
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
if not from_date and not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": filters.company,
|
||||
"posting_date": ("<", filters["period_start_date"]),
|
||||
},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)
|
||||
if last_period_closing_voucher:
|
||||
gl_entries += get_accounting_entries(
|
||||
"Account Closing Balance",
|
||||
from_date,
|
||||
to_date,
|
||||
accounts_list,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].posting_date, 1)
|
||||
ignore_opening_entries = True
|
||||
if filters and filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
gl_entries += get_accounting_entries(
|
||||
"GL Entry",
|
||||
from_date,
|
||||
to_date,
|
||||
accounts_list,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
)
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
for entry in gl_entries:
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
return gl_entries_by_account
|
||||
return gl_entries_by_account
|
||||
|
||||
|
||||
def get_accounting_entries(
|
||||
doctype,
|
||||
from_date,
|
||||
to_date,
|
||||
accounts,
|
||||
filters,
|
||||
ignore_closing_entries,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
root_type=None,
|
||||
ignore_closing_entries=None,
|
||||
period_closing_voucher=None,
|
||||
ignore_opening_entries=False,
|
||||
):
|
||||
@@ -535,13 +522,30 @@ def get_accounting_entries(
|
||||
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
|
||||
|
||||
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
|
||||
query = query.where(gl_entry.account.isin(accounts))
|
||||
|
||||
if (root_lft and root_rgt) or root_type:
|
||||
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
entries = query.run(as_dict=True)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
acc = frappe.qb.DocType("Account")
|
||||
exists_query = (
|
||||
frappe.qb.from_(acc).select(acc.name).where(acc.name == gl_entry.account).where(acc.is_group == 0)
|
||||
)
|
||||
if root_lft and root_rgt:
|
||||
exists_query = exists_query.where(acc.lft >= root_lft).where(acc.rgt <= root_rgt)
|
||||
|
||||
if root_type:
|
||||
exists_query = exists_query.where(acc.root_type == root_type)
|
||||
|
||||
return exists_query
|
||||
|
||||
|
||||
def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
|
||||
gl_entry = frappe.qb.DocType(doctype)
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
@@ -613,11 +617,11 @@ def get_cost_centers_with_children(cost_centers):
|
||||
return list(set(all_cost_centers))
|
||||
|
||||
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"label": _("Account") if not cash_flow else _("Section"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300,
|
||||
@@ -665,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
|
||||
filtered_summary_list.append(period)
|
||||
|
||||
return filtered_summary_list
|
||||
|
||||
|
||||
def compute_growth_view_data(data, columns):
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
for column_idx in range(1, len(columns)):
|
||||
previous_period_key = columns[column_idx - 1].get("key")
|
||||
current_period_key = columns[column_idx].get("key")
|
||||
current_period_value = data_copy[row_idx].get(current_period_key)
|
||||
previous_period_value = data_copy[row_idx].get(previous_period_key)
|
||||
annual_growth = 0
|
||||
|
||||
if current_period_value is None:
|
||||
data[row_idx][current_period_key] = None
|
||||
continue
|
||||
|
||||
if previous_period_value == 0 and current_period_value > 0:
|
||||
annual_growth = 1
|
||||
|
||||
elif previous_period_value > 0:
|
||||
annual_growth = (current_period_value - previous_period_value) / previous_period_value
|
||||
|
||||
growth_percent = round(annual_growth * 100, 2)
|
||||
|
||||
data[row_idx][current_period_key] = growth_percent
|
||||
|
||||
|
||||
def compute_margin_view_data(data, columns, accumulated_values):
|
||||
if not columns:
|
||||
return
|
||||
|
||||
if not accumulated_values:
|
||||
columns.append({"key": "total"})
|
||||
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
base_row = None
|
||||
for row in data_copy:
|
||||
if row.get("account_name") == _("Income"):
|
||||
base_row = row
|
||||
break
|
||||
|
||||
if not base_row:
|
||||
return
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
# Taking the total income from each column (for all the financial years) as the base (100%)
|
||||
row = data_copy[row_idx]
|
||||
if not row:
|
||||
continue
|
||||
|
||||
for column in columns:
|
||||
curr_period = column.get("key")
|
||||
base_value = base_row[curr_period]
|
||||
curr_value = row[curr_period]
|
||||
|
||||
if curr_value is None or base_value <= 0:
|
||||
data[row_idx][curr_period] = None
|
||||
continue
|
||||
|
||||
margin_percent = round((curr_value / base_value) * 100, 2)
|
||||
|
||||
data[row_idx][curr_period] = margin_percent
|
||||
|
||||
@@ -35,9 +35,6 @@ def execute(filters=None):
|
||||
if filters.get("party"):
|
||||
filters.party = frappe.parse_json(filters.get("party"))
|
||||
|
||||
if filters.get("voucher_no") and not filters.get("group_by"):
|
||||
filters.group_by = "Group by Voucher (Consolidated)"
|
||||
|
||||
validate_filters(filters, account_details)
|
||||
|
||||
validate_party(filters)
|
||||
@@ -348,10 +345,18 @@ def get_accounts_with_children(accounts):
|
||||
return frappe.qb.from_(doctype).select(doctype.name).where(Criterion.any(conditions)).run(pluck=True)
|
||||
|
||||
|
||||
def set_bill_no(gl_entries):
|
||||
inv_details = get_supplier_invoice_details()
|
||||
for gl in gl_entries:
|
||||
gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "")
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
totals_dict = get_totals_dict()
|
||||
|
||||
set_bill_no(gl_entries)
|
||||
|
||||
gle_map = initialize_gle_map(gl_entries, filters, totals_dict)
|
||||
|
||||
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict)
|
||||
@@ -365,16 +370,21 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
|
||||
# totals
|
||||
data.append(acc_dict.totals.total)
|
||||
if filters.get("group_by") or not filters.voucher_no:
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.closing)
|
||||
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
@@ -539,7 +549,6 @@ def get_account_type_map(company):
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
inv_details = get_supplier_invoice_details()
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
@@ -549,7 +558,6 @@ def get_result_as_list(data, filters):
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
d["bill_no"] = inv_details.get(d.get("against_voucher"), "")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -421,10 +421,10 @@ class GrossProfitGenerator:
|
||||
self.load_invoice_items()
|
||||
self.get_delivery_notes()
|
||||
|
||||
self.load_product_bundle()
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
self.get_returned_invoice_items()
|
||||
self.process()
|
||||
@@ -440,6 +440,7 @@ class GrossProfitGenerator:
|
||||
|
||||
if grouped_by_invoice:
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.filters.get("group_by") == "Monthly":
|
||||
@@ -480,12 +481,11 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
|
||||
|
||||
if grouped_by_invoice:
|
||||
if row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
elif row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
buying_amount = 0
|
||||
if grouped_by_invoice and row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
row.base_amount = base_amount
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
# get buying rate
|
||||
if flt(row.qty):
|
||||
@@ -495,11 +495,19 @@ class GrossProfitGenerator:
|
||||
if self.is_not_invoice_row(row):
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
|
||||
if self.is_not_invoice_row(row):
|
||||
self.update_return_invoices(row)
|
||||
|
||||
if grouped_by_invoice and row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
row.gross_profit_percent = 0.0
|
||||
@@ -510,33 +518,29 @@ class GrossProfitGenerator:
|
||||
if self.grouped:
|
||||
self.get_average_rate_based_on_group_by()
|
||||
|
||||
def update_return_invoices(self, row):
|
||||
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
returned_item_row.qty = 0
|
||||
returned_item_row.base_amount = 0
|
||||
|
||||
else:
|
||||
row.qty = 0
|
||||
row.base_amount = 0
|
||||
returned_item_row.qty += row.qty
|
||||
returned_item_row.base_amount += row.base_amount
|
||||
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") == "Invoice":
|
||||
for row in self.grouped[key]:
|
||||
if row.indent == 1.0:
|
||||
if (
|
||||
row.parent in self.returned_invoices
|
||||
and row.item_code in self.returned_invoices[row.parent]
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(
|
||||
flt(row.qty) * flt(row.buying_rate), self.currency_precision
|
||||
)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
elif self.filters.get("group_by") == "Payment Term":
|
||||
if self.filters.get("group_by") == "Payment Term":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
invoice_portion = 0
|
||||
|
||||
@@ -556,7 +560,7 @@ class GrossProfitGenerator:
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
elif self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
@@ -632,6 +636,7 @@ class GrossProfitGenerator:
|
||||
if packed_item.get("parent_detail_docname") == row.item_row:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -664,7 +669,9 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
if (row.update_stock or row.dn_detail) and my_sle:
|
||||
parenttype, parent = row.parenttype, row.parent
|
||||
parenttype = row.parenttype
|
||||
parent = row.invoice or row.parent
|
||||
|
||||
if row.dn_detail:
|
||||
parenttype, parent = "Delivery Note", row.delivery_note
|
||||
|
||||
@@ -847,6 +854,7 @@ class GrossProfitGenerator:
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
@@ -907,6 +915,7 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
grouped = OrderedDict()
|
||||
product_bundles = self.product_bundles.get("Sales Invoice", {})
|
||||
|
||||
for row in self.si_list:
|
||||
# initialize list with a header row for each new parent
|
||||
@@ -917,8 +926,7 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
@@ -954,47 +962,40 @@ class GrossProfitGenerator:
|
||||
"item_row": None,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"),
|
||||
"base_net_amount": row.invoice_base_net_total,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
)
|
||||
|
||||
def get_bundle_item_row(self, product_bundle, item):
|
||||
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
|
||||
|
||||
def get_bundle_item_row(self, row, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": product_bundle.item_code,
|
||||
"indent": product_bundle.indent + 1,
|
||||
"parent_invoice": row.item_code,
|
||||
"parenttype": row.parenttype,
|
||||
"indent": row.indent + 1,
|
||||
"parent": None,
|
||||
"invoice_or_item": item.item_code,
|
||||
"posting_date": product_bundle.posting_date,
|
||||
"posting_time": product_bundle.posting_time,
|
||||
"project": product_bundle.project,
|
||||
"customer": product_bundle.customer,
|
||||
"customer_group": product_bundle.customer_group,
|
||||
"posting_date": row.posting_date,
|
||||
"posting_time": row.posting_time,
|
||||
"project": row.project,
|
||||
"customer": row.customer,
|
||||
"customer_group": row.customer_group,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item_name,
|
||||
"description": description,
|
||||
"warehouse": product_bundle.warehouse,
|
||||
"item_group": item_group,
|
||||
"brand": brand,
|
||||
"dn_detail": product_bundle.dn_detail,
|
||||
"delivery_note": product_bundle.delivery_note,
|
||||
"qty": (flt(product_bundle.qty) * flt(item.qty)),
|
||||
"item_row": None,
|
||||
"is_return": product_bundle.is_return,
|
||||
"cost_center": product_bundle.cost_center,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"warehouse": item.warehouse or row.warehouse,
|
||||
"update_stock": row.update_stock,
|
||||
"item_group": "",
|
||||
"brand": "",
|
||||
"dn_detail": row.dn_detail,
|
||||
"delivery_note": row.delivery_note,
|
||||
"qty": item.total_qty * -1,
|
||||
"item_row": row.item_row,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_item_details(self, item_code):
|
||||
return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"])
|
||||
|
||||
def get_stock_ledger_entries(self, item_code, warehouse):
|
||||
if item_code and warehouse:
|
||||
if (item_code, warehouse) not in self.sle:
|
||||
|
||||
@@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase):
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit": 0.0,
|
||||
"gross_profit_%": 0.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
|
||||
@@ -130,6 +130,7 @@ class PaymentLedger:
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
@@ -194,7 +195,7 @@ class PaymentLedger:
|
||||
label=_("Amount"),
|
||||
fieldname="amount",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
options=company_currency,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
compute_margin_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -68,6 +70,12 @@ def execute(filters=None):
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
if filters.get("selected_view") == "Margin":
|
||||
compute_margin_view_data(data, period_list, filters.accumulated_values)
|
||||
|
||||
return columns, data, None, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -526,7 +526,8 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
|
||||
tax_details = frappe.db.sql(
|
||||
"""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head"""
|
||||
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
|
||||
group by parent, account_head"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
|
||||
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal file
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.sales_register.sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_child_cost_center()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_child_cost_center(self):
|
||||
cc_name = "South Wing"
|
||||
if frappe.db.exists("Cost Center", cc_name):
|
||||
cc = frappe.get_doc("Cost Center", cc_name)
|
||||
else:
|
||||
parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center")
|
||||
cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"company": self.company,
|
||||
"is_group": False,
|
||||
"parent_cost_center": parent,
|
||||
"cost_center_name": cc_name,
|
||||
}
|
||||
)
|
||||
cc = cc.save()
|
||||
self.south_cc = cc.name
|
||||
|
||||
def create_sales_invoice(self, rate=100, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=rate,
|
||||
price_list_rate=rate,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice(rate=98)
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
res = [x for x in report[1] if x.get("voucher_no") == si.name]
|
||||
|
||||
expected_result = {
|
||||
"voucher_type": si.doctype,
|
||||
"voucher_no": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 98.0,
|
||||
"grand_total": 98.0,
|
||||
"debit": 98.0,
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in res[0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_journal_with_cost_center_filter(self):
|
||||
je1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": "Journal Entry",
|
||||
"company": self.company,
|
||||
"posting_date": getdate(),
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"credit_in_account_currency": 77,
|
||||
"credit": 77,
|
||||
"is_advance": "Yes",
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 77,
|
||||
"debit": 77,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je1.submit()
|
||||
|
||||
je2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": "Journal Entry",
|
||||
"company": self.company,
|
||||
"posting_date": getdate(),
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"credit_in_account_currency": 98,
|
||||
"credit": 98,
|
||||
"is_advance": "Yes",
|
||||
"cost_center": self.south_cc,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 98,
|
||||
"debit": 98,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je2.submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"include_payments": True,
|
||||
"customer": self.customer,
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
)
|
||||
report_output = execute(filters)[1]
|
||||
filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name]
|
||||
self.assertEqual(len(filtered_output), 1)
|
||||
expected_result = {
|
||||
"voucher_type": je1.doctype,
|
||||
"voucher_no": je1.name,
|
||||
"posting_date": je1.posting_date,
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 77.0,
|
||||
"credit": 77.0,
|
||||
}
|
||||
result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result}
|
||||
self.assertDictEqual(result_fields, expected_result)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"include_payments": True,
|
||||
"customer": self.customer,
|
||||
"cost_center": self.south_cc,
|
||||
}
|
||||
)
|
||||
report_output = execute(filters)[1]
|
||||
filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name]
|
||||
self.assertEqual(len(filtered_output), 1)
|
||||
expected_result = {
|
||||
"voucher_type": je2.doctype,
|
||||
"voucher_no": je2.name,
|
||||
"posting_date": je2.posting_date,
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 98.0,
|
||||
"credit": 98.0,
|
||||
}
|
||||
result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result}
|
||||
self.assertDictEqual(result_output, expected_result)
|
||||
@@ -94,12 +94,6 @@ def get_data(filters):
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
min_lft, max_rgt = frappe.db.sql(
|
||||
"""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""",
|
||||
(filters.company,),
|
||||
)[0]
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(filters)
|
||||
@@ -112,10 +106,10 @@ def get_data(filters):
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
min_lft,
|
||||
max_rgt,
|
||||
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,
|
||||
)
|
||||
@@ -150,9 +144,9 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)},
|
||||
fields=["period_end_date", "name"],
|
||||
order_by="period_end_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
@@ -168,8 +162,8 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
if getdate(last_period_closing_voucher[0].posting_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].posting_date, 1)
|
||||
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
gle += get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
|
||||
)
|
||||
|
||||
@@ -255,7 +255,9 @@ def get_journal_entries(filters, args):
|
||||
)
|
||||
.orderby(je.posting_date, je.name, order=Order.desc)
|
||||
)
|
||||
query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
|
||||
query = apply_common_conditions(
|
||||
filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True
|
||||
)
|
||||
|
||||
journal_entries = query.run(as_dict=True)
|
||||
return journal_entries
|
||||
@@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
|
||||
query = query.where(parent_doc.posting_date <= filters.to_date)
|
||||
|
||||
if payments:
|
||||
if filters.get("cost_center"):
|
||||
if doctype == "Journal Entry" and filters.get("cost_center"):
|
||||
query = query.where(child_doc.cost_center == filters.cost_center)
|
||||
elif filters.get("cost_center"):
|
||||
query = query.where(parent_doc.cost_center == filters.cost_center)
|
||||
else:
|
||||
if filters.get("cost_center"):
|
||||
|
||||
@@ -92,14 +92,14 @@ class TestUtils(unittest.TestCase):
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
# below is the difference between base_received_amount and base_paid_amount
|
||||
self.assertEqual(payment_entry.difference_amount, -4855.0)
|
||||
# below is the difference between base_paid_amount and base_received_amount (exchange gain)
|
||||
self.assertEqual(payment_entry.deductions[0].amount, -4855.0)
|
||||
|
||||
payment_entry.target_exchange_rate = 62.9
|
||||
payment_entry.save()
|
||||
|
||||
# below is due to change in exchange rate
|
||||
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
|
||||
# after changing the exchange rate, there is no exchange gain / loss
|
||||
self.assertEqual(payment_entry.deductions, [])
|
||||
|
||||
payment_entry.references = []
|
||||
self.assertEqual(payment_entry.difference_amount, 0.0)
|
||||
|
||||
@@ -630,6 +630,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
rev_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d["dr_or_cr"] == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if jv_detail.get(rev_dr_or_cr):
|
||||
d["dr_or_cr"] = rev_dr_or_cr
|
||||
d["allocated_amount"] = d["allocated_amount"] * -1
|
||||
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||
@@ -1547,12 +1557,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
return matched
|
||||
|
||||
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None, accounts=None):
|
||||
stock_accounts = [
|
||||
d.name
|
||||
for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0})
|
||||
]
|
||||
if voucher_type and voucher_no:
|
||||
|
||||
if accounts:
|
||||
stock_accounts = [row.account for row in accounts if row.account in stock_accounts]
|
||||
|
||||
elif voucher_type and voucher_no:
|
||||
if voucher_type == "Journal Entry":
|
||||
stock_accounts = [
|
||||
d.account
|
||||
@@ -1965,6 +1979,7 @@ class QueryPaymentLedger:
|
||||
ple.cost_center.as_("cost_center"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
ple.remarks,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
@@ -2027,6 +2042,7 @@ class QueryPaymentLedger:
|
||||
Table("vouchers").due_date,
|
||||
Table("vouchers").currency,
|
||||
Table("vouchers").cost_center.as_("cost_center"),
|
||||
Table("vouchers").remarks,
|
||||
)
|
||||
.where(Criterion.all(filter_on_outstanding_amount))
|
||||
)
|
||||
|
||||
@@ -416,7 +416,7 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
|
||||
frm.dashboard.render_graph({
|
||||
title: "Asset Value",
|
||||
title: __("Asset Value"),
|
||||
data: {
|
||||
labels: x_intervals,
|
||||
datasets: [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user