Compare commits
672 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c79088f46a | ||
|
|
d5896cd8a0 | ||
|
|
79edcb2a94 | ||
|
|
bd2f408f00 | ||
|
|
3b02f80a4e | ||
|
|
bebd1a0426 | ||
|
|
f6f341ad58 | ||
|
|
752f93c630 | ||
|
|
d14e386e18 | ||
|
|
9f687eee4e | ||
|
|
c98ab15a44 | ||
|
|
54815b44e7 | ||
|
|
ed568fb01d | ||
|
|
a520d06c3f | ||
|
|
2796aa8b57 | ||
|
|
a5ca5b5081 | ||
|
|
695297b917 | ||
|
|
8882b85888 | ||
|
|
1cddb4ff39 | ||
|
|
aaa6d66666 | ||
|
|
fa44b0d745 | ||
|
|
54735469c1 | ||
|
|
714a432c1c | ||
|
|
367b4177c6 | ||
|
|
2d0dca943a | ||
|
|
456f5b644b | ||
|
|
cca5aa8a96 | ||
|
|
366f383d1f | ||
|
|
7752de3a1c | ||
|
|
f0f663e552 | ||
|
|
f35bc43242 | ||
|
|
b0b5b25a53 | ||
|
|
25096185c9 | ||
|
|
a5b5b5e62c | ||
|
|
e421e16fdc | ||
|
|
cef3573d79 | ||
|
|
1ebcc33cdd | ||
|
|
a9871e379a | ||
|
|
34662e6e3c | ||
|
|
1844cb60a4 | ||
|
|
71416902f6 | ||
|
|
e289793308 | ||
|
|
365ef6b88a | ||
|
|
e44daf73fb | ||
|
|
a43136da5d | ||
|
|
d0fbb4d532 | ||
|
|
29cfb7cf25 | ||
|
|
c4c001b6f9 | ||
|
|
93d8e3f3b0 | ||
|
|
db27dd8702 | ||
|
|
cc97d4e869 | ||
|
|
eb26610d54 | ||
|
|
29449cbaf9 | ||
|
|
f29cad695a | ||
|
|
8f19832aa5 | ||
|
|
8d7e150afb | ||
|
|
4a77653e94 | ||
|
|
fa74efc1fc | ||
|
|
b73df8f5f0 | ||
|
|
a8a8a033a1 | ||
|
|
f0230b2dfd | ||
|
|
f4811c3846 | ||
|
|
a9fa9e86ea | ||
|
|
c67be05280 | ||
|
|
186701b54f | ||
|
|
7cd6debd0e | ||
|
|
f8fd354d75 | ||
|
|
bc9b46126b | ||
|
|
f0f8a2f01b | ||
|
|
83928bbf78 | ||
|
|
bc50512c97 | ||
|
|
b7731c8fd7 | ||
|
|
8667bcd86b | ||
|
|
7086a96462 | ||
|
|
2a38e14cca | ||
|
|
f4ba879203 | ||
|
|
5c5349ed16 | ||
|
|
8df1079618 | ||
|
|
38739bec45 | ||
|
|
6ceb3476a6 | ||
|
|
6cba69c7da | ||
|
|
ffd3aea07e | ||
|
|
989ef52f59 | ||
|
|
baa36c6d5e | ||
|
|
f7f191fe50 | ||
|
|
703f58ceac | ||
|
|
e1da019fe7 | ||
|
|
f0bdc41a94 | ||
|
|
392ba36dcf | ||
|
|
1b6539c3c5 | ||
|
|
1da744dc4d | ||
|
|
ac763f8c19 | ||
|
|
0a83c8b00c | ||
|
|
b20b15af74 | ||
|
|
4089af5a7b | ||
|
|
06c73ef2da | ||
|
|
cccfbf193e | ||
|
|
3fb6f97f66 | ||
|
|
35f6657bae | ||
|
|
e82c441326 | ||
|
|
c8d85364b9 | ||
|
|
64234a571e | ||
|
|
a5e14324da | ||
|
|
37a7da3371 | ||
|
|
81c362dbe4 | ||
|
|
53034c332b | ||
|
|
0441984405 | ||
|
|
3ba6f40063 | ||
|
|
8e8d0c7bd0 | ||
|
|
f42f1bb35f | ||
|
|
0256c64634 | ||
|
|
e4f583d25a | ||
|
|
7259c0fe30 | ||
|
|
02547115e5 | ||
|
|
0bc9825238 | ||
|
|
bb66126dfa | ||
|
|
0e2abbd08e | ||
|
|
ff78fab176 | ||
|
|
04840762dd | ||
|
|
e7432fc60d | ||
|
|
97f2e88f4c | ||
|
|
37f24ae763 | ||
|
|
25b9127bae | ||
|
|
6e74e6f314 | ||
|
|
240118ee8b | ||
|
|
c1fd95ac66 | ||
|
|
8e340bb7fd | ||
|
|
96a6172999 | ||
|
|
d0d587432d | ||
|
|
07509b5e99 | ||
|
|
99d5b6dc71 | ||
|
|
56b1582027 | ||
|
|
149109649d | ||
|
|
0284328e2c | ||
|
|
a243873ab0 | ||
|
|
197e043fc9 | ||
|
|
925a164101 | ||
|
|
8f1a4b9717 | ||
|
|
67d4020241 | ||
|
|
fe4b2e36cc | ||
|
|
6759b90f85 | ||
|
|
99bc8e849c | ||
|
|
7a25d33547 | ||
|
|
2466e28bf5 | ||
|
|
1096528bb9 | ||
|
|
6ecb064264 | ||
|
|
92300b27c9 | ||
|
|
4fa9626de0 | ||
|
|
9b828b829a | ||
|
|
2936988cc6 | ||
|
|
9fca232578 | ||
|
|
fac22e93d0 | ||
|
|
3109efaf09 | ||
|
|
90ee21f868 | ||
|
|
36c46bb344 | ||
|
|
8c1f6196b8 | ||
|
|
12a31de25a | ||
|
|
3b9400755e | ||
|
|
2ce7300c3c | ||
|
|
b96b3b51b6 | ||
|
|
8f03769bf2 | ||
|
|
d20f3ab492 | ||
|
|
980ca1d8c5 | ||
|
|
4668a2d7d8 | ||
|
|
1a7efbb654 | ||
|
|
ccc2a47e73 | ||
|
|
f98716cc2a | ||
|
|
7903e8d669 | ||
|
|
9a50a0a129 | ||
|
|
1646517dc4 | ||
|
|
38811e792c | ||
|
|
edfb408464 | ||
|
|
ac48c3d4e7 | ||
|
|
1d66b7e5a3 | ||
|
|
ab9bde86f9 | ||
|
|
5c75bb8775 | ||
|
|
115a0123ed | ||
|
|
fdf1dfe46e | ||
|
|
106c154a16 | ||
|
|
00e8b862dd | ||
|
|
4195c50f02 | ||
|
|
fdb8e5b379 | ||
|
|
51cbbee4ca | ||
|
|
5000c09759 | ||
|
|
49e50662b6 | ||
|
|
f2f1f32826 | ||
|
|
fcf6500144 | ||
|
|
f2d5a69af4 | ||
|
|
d5c1c62622 | ||
|
|
e2f8e02c73 | ||
|
|
30cba7ee2c | ||
|
|
21a60c9927 | ||
|
|
0f275a9ff0 | ||
|
|
18402677da | ||
|
|
e9357c193d | ||
|
|
13895fa060 | ||
|
|
64f8498576 | ||
|
|
62ad466a3b | ||
|
|
45899b3017 | ||
|
|
d92a042bf7 | ||
|
|
4d6a71ab4b | ||
|
|
d5fa968078 | ||
|
|
71cbebd31b | ||
|
|
99317768f6 | ||
|
|
9fde7330e0 | ||
|
|
49fb6bec6a | ||
|
|
0f1f5b6f3d | ||
|
|
454e147592 | ||
|
|
701dd9e19b | ||
|
|
2cf82561f6 | ||
|
|
f2d113dcf2 | ||
|
|
97e4495f1f | ||
|
|
26c99351fd | ||
|
|
4d99449aa8 | ||
|
|
72b93805fd | ||
|
|
f31ed75578 | ||
|
|
003d7e9f3e | ||
|
|
650b25fc23 | ||
|
|
d5366c5873 | ||
|
|
bf36b4fa11 | ||
|
|
f4fc26b52e | ||
|
|
952a7b46d5 | ||
|
|
cc9857affd | ||
|
|
6de7a8de10 | ||
|
|
b593f57637 | ||
|
|
21bf7fd1f8 | ||
|
|
a2ee4631cc | ||
|
|
92ca6c6538 | ||
|
|
6028a19e6b | ||
|
|
87dc586543 | ||
|
|
22216b275c | ||
|
|
0e6edf763e | ||
|
|
e77534ba2b | ||
|
|
ae14b86a8c | ||
|
|
9665212607 | ||
|
|
5ae9ec2657 | ||
|
|
890289563a | ||
|
|
ee49e83020 | ||
|
|
598c581623 | ||
|
|
8f5278e3d4 | ||
|
|
aa38f69a67 | ||
|
|
57a81c4012 | ||
|
|
2fabcb0c50 | ||
|
|
f833923f2f | ||
|
|
077cb9e983 | ||
|
|
bcfc83d8d5 | ||
|
|
a5d1feef02 | ||
|
|
b050110544 | ||
|
|
e8f5c45751 | ||
|
|
eae5f27ec8 | ||
|
|
d48455393e | ||
|
|
7c699c8a38 | ||
|
|
20b8ee1e90 | ||
|
|
f9a7bb0544 | ||
|
|
6441bc7862 | ||
|
|
a5df0f2c94 | ||
|
|
e33416a4a7 | ||
|
|
245c6d8672 | ||
|
|
32fba94b2a | ||
|
|
a6ed10b712 | ||
|
|
7234625d65 | ||
|
|
0c4a2af9ab | ||
|
|
5ba0082bc7 | ||
|
|
47b3e96a37 | ||
|
|
810378c899 | ||
|
|
bdf198c94f | ||
|
|
e8286d7b3c | ||
|
|
fd73a8a348 | ||
|
|
91de46922d | ||
|
|
3823e0e494 | ||
|
|
625aa4ef6b | ||
|
|
e9b7d00afb | ||
|
|
e79e9e94d0 | ||
|
|
e94e15259b | ||
|
|
ab90b815e3 | ||
|
|
ee47440063 | ||
|
|
bc24e75c24 | ||
|
|
5d97c7cff9 | ||
|
|
e38383a757 | ||
|
|
a5bda0180e | ||
|
|
b6648eebfa | ||
|
|
faa3c7c3a4 | ||
|
|
5ab5bd138f | ||
|
|
dc3265751c | ||
|
|
cf8f0e4096 | ||
|
|
8c09968e1b | ||
|
|
39885b2b01 | ||
|
|
6a6a6971ba | ||
|
|
13dfbe3d80 | ||
|
|
7e3c15e0b6 | ||
|
|
570985f40e | ||
|
|
eb418e8659 | ||
|
|
014486de39 | ||
|
|
a53a80f01d | ||
|
|
b1a911aa9c | ||
|
|
18b6d50a31 | ||
|
|
fcb2f4f084 | ||
|
|
308f200793 | ||
|
|
4fcfbe6e9f | ||
|
|
5b2c13dacf | ||
|
|
76b7884fb7 | ||
|
|
518dad8ecb | ||
|
|
220ae118e8 | ||
|
|
f1f2f6e338 | ||
|
|
c16c41ee59 | ||
|
|
dcffb3886b | ||
|
|
7035969db7 | ||
|
|
0cf97f2559 | ||
|
|
459d136368 | ||
|
|
a2bfe31876 | ||
|
|
75b021dc23 | ||
|
|
b2e2e951da | ||
|
|
0f9b5074a6 | ||
|
|
190900cd1b | ||
|
|
3512f7d528 | ||
|
|
0dc2f78a2e | ||
|
|
c834a9de85 | ||
|
|
c910b8ab03 | ||
|
|
b9ebb50a02 | ||
|
|
38cc28a4c3 | ||
|
|
bbb9b9e3b6 | ||
|
|
94c3ee645d | ||
|
|
4d8c35aa5d | ||
|
|
5230d411bf | ||
|
|
a6bf7c1ebd | ||
|
|
50f6afd588 | ||
|
|
649c192abe | ||
|
|
b7d6a54bed | ||
|
|
05e4dae1b8 | ||
|
|
97fdda8a7c | ||
|
|
9802333397 | ||
|
|
82d206b709 | ||
|
|
80810c2ebb | ||
|
|
331a743d69 | ||
|
|
32a1fea8f0 | ||
|
|
dfb4c47089 | ||
|
|
0cbf049608 | ||
|
|
3f0cb47464 | ||
|
|
89d507e07e | ||
|
|
26595351cc | ||
|
|
dfaca93292 | ||
|
|
40de3f3481 | ||
|
|
fc2614612b | ||
|
|
e69e5404d3 | ||
|
|
14ec6351ae | ||
|
|
a8be5f0789 | ||
|
|
42312c5bba | ||
|
|
7b28d7d2b8 | ||
|
|
0aa246c39e | ||
|
|
5468a3b0b7 | ||
|
|
5366356400 | ||
|
|
7a380f584d | ||
|
|
256d6a47ac | ||
|
|
67e06615a3 | ||
|
|
1078a98cce | ||
|
|
d87ffae03f | ||
|
|
0404941fb2 | ||
|
|
c4dfcbec96 | ||
|
|
c9e7f450c5 | ||
|
|
690278042d | ||
|
|
754e193c76 | ||
|
|
9d5e4b3b3a | ||
|
|
d7709cf4e4 | ||
|
|
4647ec8892 | ||
|
|
339256bc71 | ||
|
|
c3d567b291 | ||
|
|
e068bec212 | ||
|
|
d2ce927891 | ||
|
|
a26ae64385 | ||
|
|
cd33199da2 | ||
|
|
27100401aa | ||
|
|
93b30d9f11 | ||
|
|
904f369e99 | ||
|
|
7b9c22775c | ||
|
|
6aa8d5fb4b | ||
|
|
e82ea12cbc | ||
|
|
d727c52421 | ||
|
|
5ae29655f9 | ||
|
|
81a99309d8 | ||
|
|
9aa054c400 | ||
|
|
82b2675aa8 | ||
|
|
8318286865 | ||
|
|
f87411f40d | ||
|
|
45c4167c86 | ||
|
|
e0d1f2f6eb | ||
|
|
ba45ea42f8 | ||
|
|
58b68b7597 | ||
|
|
4d56c46446 | ||
|
|
eb22fb9326 | ||
|
|
54313b5db9 | ||
|
|
866b0c6ac7 | ||
|
|
1e1319351d | ||
|
|
6b2874694e | ||
|
|
ff37706bef | ||
|
|
949aa9346c | ||
|
|
2f6fee9877 | ||
|
|
abe64aa1ab | ||
|
|
1a7b3c437d | ||
|
|
da69b1e71b | ||
|
|
d160f5b61a | ||
|
|
3fcdcef178 | ||
|
|
40ece3f5da | ||
|
|
c93840eb56 | ||
|
|
7656fe4bfc | ||
|
|
59010c9a61 | ||
|
|
113351e850 | ||
|
|
2026c986ba | ||
|
|
91c202f172 | ||
|
|
7e52f72bed | ||
|
|
b9d28fc1ad | ||
|
|
33d38ba3a7 | ||
|
|
ff8dba1cb7 | ||
|
|
c3244f009b | ||
|
|
30b2cac423 | ||
|
|
67be2ba9dc | ||
|
|
3e81f0f578 | ||
|
|
bb48440591 | ||
|
|
35230a9692 | ||
|
|
df3ff1097c | ||
|
|
521bf31fe3 | ||
|
|
8868cb147d | ||
|
|
0dfae12f14 | ||
|
|
0df80ad923 | ||
|
|
c71d1118a9 | ||
|
|
6017e7ac3e | ||
|
|
d5784cc629 | ||
|
|
a2e1d132df | ||
|
|
3e912993e1 | ||
|
|
5aef9d2ef2 | ||
|
|
f1b75e8c54 | ||
|
|
5b1c5e3fea | ||
|
|
f087ec8df5 | ||
|
|
ec4f07fd60 | ||
|
|
a22be6f9b9 | ||
|
|
e8accd0256 | ||
|
|
de3a423618 | ||
|
|
03ce9ee321 | ||
|
|
9d0c1dc46f | ||
|
|
5d05bf8d4e | ||
|
|
84789b7407 | ||
|
|
cd70c6c1b2 | ||
|
|
4cc0a5851b | ||
|
|
6e9da0a729 | ||
|
|
97e7b3f3d3 | ||
|
|
e956dbbf68 | ||
|
|
ebf5f13447 | ||
|
|
9404dea97e | ||
|
|
ee06448f0d | ||
|
|
c2b6b64e2e | ||
|
|
04938726d9 | ||
|
|
153e0ba81b | ||
|
|
0bee921d40 | ||
|
|
9d3c861766 | ||
|
|
e8d05517c5 | ||
|
|
2ebdd93a83 | ||
|
|
50a74ee7fe | ||
|
|
da1c317c6c | ||
|
|
79e23dad2c | ||
|
|
b47e224a1c | ||
|
|
3310358d36 | ||
|
|
7a6c0e5283 | ||
|
|
4672f59599 | ||
|
|
c2c4548cc0 | ||
|
|
cc925ae938 | ||
|
|
f0ecc627fb | ||
|
|
cf3c26d80c | ||
|
|
59cea9f4dd | ||
|
|
cc6bacb190 | ||
|
|
cebde2662d | ||
|
|
b4ed2d2c16 | ||
|
|
2425119b5e | ||
|
|
b087fb3d54 | ||
|
|
4d34b1ead7 | ||
|
|
c28d19cf7f | ||
|
|
73d2595aa6 | ||
|
|
9745724d41 | ||
|
|
a31b2e17f5 | ||
|
|
46c76b166d | ||
|
|
6079c5636e | ||
|
|
5808feaccf | ||
|
|
98ad034b21 | ||
|
|
277717a2e0 | ||
|
|
0591f72e01 | ||
|
|
1af6b4256f | ||
|
|
6273a31b8c | ||
|
|
f86c035f88 | ||
|
|
9981a900b2 | ||
|
|
7a6ffccecc | ||
|
|
58698c9aa4 | ||
|
|
bbe4ca7d74 | ||
|
|
153fc91478 | ||
|
|
b667a02470 | ||
|
|
5896e755bf | ||
|
|
dd6c53df96 | ||
|
|
3165682d7a | ||
|
|
aff71970e5 | ||
|
|
2b5ae1dbae | ||
|
|
24709ab400 | ||
|
|
0172880fd3 | ||
|
|
b76c00de68 | ||
|
|
ae1858f465 | ||
|
|
22b16a6b0e | ||
|
|
8d8feadeae | ||
|
|
5629ad21d2 | ||
|
|
1eb720a7b0 | ||
|
|
c4e950f6b5 | ||
|
|
d4313f7109 | ||
|
|
60ba62e882 | ||
|
|
26d7354973 | ||
|
|
10d760089e | ||
|
|
7e1ab75b38 | ||
|
|
0f9af4b82c | ||
|
|
a6145fa13c | ||
|
|
cc2f861bca | ||
|
|
8f2fd5dec9 | ||
|
|
09cda60bdf | ||
|
|
9dea6d3393 | ||
|
|
fb076d34b1 | ||
|
|
fba888fe94 | ||
|
|
b55d8597e8 | ||
|
|
81081663c4 | ||
|
|
2f66542d71 | ||
|
|
abc0294cbf | ||
|
|
515f933362 | ||
|
|
cfe5f009f6 | ||
|
|
cf74f8312e | ||
|
|
803ed904a9 | ||
|
|
72db656054 | ||
|
|
44b72e7fc8 | ||
|
|
31017774f6 | ||
|
|
9b8bb4d6a2 | ||
|
|
ceac42dfaa | ||
|
|
998cc0a966 | ||
|
|
e722c9a34a | ||
|
|
8dff18eab4 | ||
|
|
7363884ab3 | ||
|
|
f66b53b2c1 | ||
|
|
c1a95c05bc | ||
|
|
db56d259cc | ||
|
|
0322c1a36e | ||
|
|
bb35c334c0 | ||
|
|
4d6fad26f3 | ||
|
|
b5629d2d4e | ||
|
|
b1105fb0ab | ||
|
|
e778d7e690 | ||
|
|
5827bdcbc2 | ||
|
|
94f15797f5 | ||
|
|
af6dd3bcfb | ||
|
|
e947d65cb2 | ||
|
|
4f4470b9d2 | ||
|
|
4ea3876f30 | ||
|
|
bd3b1bbbb8 | ||
|
|
4a1cd943bd | ||
|
|
fb252ec29a | ||
|
|
2834d25ce3 | ||
|
|
208a46fbe4 | ||
|
|
2723e1f614 | ||
|
|
4485121255 | ||
|
|
cf770e784d | ||
|
|
fda9c03d4d | ||
|
|
adec93de18 | ||
|
|
73c534cf1a | ||
|
|
0ec1a88748 | ||
|
|
91400e1371 | ||
|
|
39711df27c | ||
|
|
03eb4d77ce | ||
|
|
c3f9338430 | ||
|
|
dbb4391f1e | ||
|
|
22cec21c6f | ||
|
|
ab7f80625b | ||
|
|
a4ddc4ee1b | ||
|
|
07929d20aa | ||
|
|
2f839fbf6d | ||
|
|
9f3b6d21c3 | ||
|
|
4eb1fbfe42 | ||
|
|
3a38aacc30 | ||
|
|
48bfb9d315 | ||
|
|
5d90b0fd1d | ||
|
|
3cab242e9f | ||
|
|
54e7f303e9 | ||
|
|
cd0a7fed41 | ||
|
|
84609abd65 | ||
|
|
a76d19fe8f | ||
|
|
ebea0fd203 | ||
|
|
f9c30968bf | ||
|
|
a9a510547e | ||
|
|
c2f6f9d37f | ||
|
|
9d5c010071 | ||
|
|
3cfc778bdb | ||
|
|
f06014afc3 | ||
|
|
bee5744309 | ||
|
|
bc1f25b897 | ||
|
|
6afb6ff4df | ||
|
|
609ecbe804 | ||
|
|
674d822985 | ||
|
|
66fae6466f | ||
|
|
81f456bdc7 | ||
|
|
a177137d6d | ||
|
|
d81be8d855 | ||
|
|
0399acdc8e | ||
|
|
50963d9ad5 | ||
|
|
1639974948 | ||
|
|
57ec0d6b41 | ||
|
|
5ad3918232 | ||
|
|
9e15ecfe34 | ||
|
|
da17496e3d | ||
|
|
99faafb5be | ||
|
|
6d39a7890e | ||
|
|
390d3a6a9f | ||
|
|
5117ba5abf | ||
|
|
9b3c4ac575 | ||
|
|
d3a6153077 | ||
|
|
2b311131ba | ||
|
|
c072cd941d | ||
|
|
757cf8a76f | ||
|
|
82b613353b | ||
|
|
5791c50fdf | ||
|
|
67481ad062 | ||
|
|
1515bb7f0b | ||
|
|
33e0371c4d | ||
|
|
1df54114a8 | ||
|
|
cf3b0ee41e | ||
|
|
957f55f6b1 | ||
|
|
77349a0af6 | ||
|
|
c36a5d1ba3 | ||
|
|
44d4096ba1 | ||
|
|
a694a92df9 | ||
|
|
1c7128e77b | ||
|
|
c147ec168f | ||
|
|
717442c01f | ||
|
|
ce7a53f810 | ||
|
|
1767dada8d | ||
|
|
5e68ebd030 | ||
|
|
87fdb4e720 | ||
|
|
e5722a772a | ||
|
|
35fcd032ad | ||
|
|
1fe14334f3 | ||
|
|
b429f75d65 | ||
|
|
fab5c1170d | ||
|
|
d496a1e58e | ||
|
|
6ae9eb250f | ||
|
|
5e590389f3 | ||
|
|
1110dd93ff | ||
|
|
0ea9ce7a5a | ||
|
|
cf6cb80d18 | ||
|
|
e908003358 | ||
|
|
e142daca7f | ||
|
|
962105bc87 | ||
|
|
e600109872 | ||
|
|
97ed905627 | ||
|
|
c38cfd14f3 | ||
|
|
341e467056 | ||
|
|
833df2c76b | ||
|
|
a303788c71 | ||
|
|
0d791f594f | ||
|
|
b1367e839c | ||
|
|
e56138ddb0 | ||
|
|
c9d77044b0 | ||
|
|
5542985e25 | ||
|
|
52d22d8b55 | ||
|
|
7280a76f73 | ||
|
|
07c98146c5 | ||
|
|
e922ac7c31 | ||
|
|
bba1ac5735 | ||
|
|
d0a0a35d72 | ||
|
|
3b2044dcd7 | ||
|
|
7f2a54d95b | ||
|
|
ee7bd98878 | ||
|
|
fe3bee44e2 | ||
|
|
c3b8c00070 | ||
|
|
f9ab763cc8 | ||
|
|
819ced4cb3 |
@@ -28,4 +28,7 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
|
||||
494bd9ef78313436f0424b918f200dab8fc7c20b
|
||||
|
||||
# bulk format python code with black
|
||||
baec607ff5905b1c67531096a9cf50ec7ff00a5d
|
||||
baec607ff5905b1c67531096a9cf50ec7ff00a5d
|
||||
|
||||
# ruff
|
||||
4d34b1ead73baf4c5430a2ecbe44b9e8468d7626
|
||||
|
||||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep==0.97.0
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
||||
@@ -65,28 +65,18 @@ repos:
|
||||
.*/loan_write_off.js
|
||||
)$
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-bugbear',
|
||||
]
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
- id: ruff
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.65.7"
|
||||
__version__ = "14.72.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -36,10 +36,8 @@ def get_default_cost_center(company):
|
||||
|
||||
if not frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center = {}
|
||||
if not company in frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value(
|
||||
"Company", company, "cost_center"
|
||||
)
|
||||
if company not in frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value("Company", company, "cost_center")
|
||||
return frappe.flags.company_cost_center[company]
|
||||
|
||||
|
||||
@@ -47,7 +45,7 @@ def get_company_currency(company):
|
||||
"""Returns the default company currency"""
|
||||
if not frappe.flags.company_currency:
|
||||
frappe.flags.company_currency = {}
|
||||
if not company in frappe.flags.company_currency:
|
||||
if company not in frappe.flags.company_currency:
|
||||
frappe.flags.company_currency[company] = frappe.db.get_value(
|
||||
"Company", company, "default_currency", cache=True
|
||||
)
|
||||
@@ -81,7 +79,7 @@ def is_perpetual_inventory_enabled(company):
|
||||
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
if not company in frappe.local.enable_perpetual_inventory:
|
||||
if company not in frappe.local.enable_perpetual_inventory:
|
||||
frappe.local.enable_perpetual_inventory[company] = (
|
||||
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
|
||||
)
|
||||
@@ -96,7 +94,7 @@ def get_default_finance_book(company=None):
|
||||
if not hasattr(frappe.local, "default_finance_book"):
|
||||
frappe.local.default_finance_book = {}
|
||||
|
||||
if not company in frappe.local.default_finance_book:
|
||||
if company not in frappe.local.default_finance_book:
|
||||
frappe.local.default_finance_book[company] = frappe.get_cached_value(
|
||||
"Company", company, "default_finance_book"
|
||||
)
|
||||
@@ -108,7 +106,7 @@ def get_party_account_type(party_type):
|
||||
if not hasattr(frappe.local, "party_account_types"):
|
||||
frappe.local.party_account_types = {}
|
||||
|
||||
if not party_type in frappe.local.party_account_types:
|
||||
if party_type not in frappe.local.party_account_types:
|
||||
frappe.local.party_account_types[party_type] = (
|
||||
frappe.db.get_value("Party Type", party_type, "account_type") or ""
|
||||
)
|
||||
|
||||
@@ -11,14 +11,14 @@ class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_compnay_address()
|
||||
super(ERPNextAddress, self).validate()
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
if self.is_your_company_address:
|
||||
return
|
||||
|
||||
return super(ERPNextAddress, self).link_address()
|
||||
return super().link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
for link in self.get("links"):
|
||||
@@ -26,11 +26,11 @@ class ERPNextAddress(Address):
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.is_your_company_address and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
]:
|
||||
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
|
||||
frappe.throw(
|
||||
_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
|
||||
_(
|
||||
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
|
||||
),
|
||||
title=_("Company Not Linked"),
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def get(
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
|
||||
|
||||
account = filters.get("account")
|
||||
company = filters.get("company")
|
||||
filters.get("company")
|
||||
|
||||
if not account and chart_name:
|
||||
frappe.throw(
|
||||
@@ -83,7 +83,6 @@ def build_result(account, dates, gl_entries):
|
||||
|
||||
# get balances in debit
|
||||
for entry in gl_entries:
|
||||
|
||||
# entry date is after the current pointer, so move the pointer forward
|
||||
while getdate(entry.posting_date) > result[date_index][0]:
|
||||
date_index += 1
|
||||
@@ -133,8 +132,6 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
|
||||
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
while getdate(dates[-1]) < getdate(to_date):
|
||||
date = get_period_ending(
|
||||
add_to_date(dates[-1], years=years, months=months, days=days), timegrain
|
||||
)
|
||||
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
|
||||
dates.append(date)
|
||||
return dates
|
||||
|
||||
@@ -24,14 +24,10 @@ from erpnext.accounts.utils import get_account_currency
|
||||
def validate_service_stop_date(doc):
|
||||
"""Validates service_stop_date for Purchase Invoice and Sales Invoice"""
|
||||
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
old_stop_dates = {}
|
||||
old_doc = frappe.db.get_all(
|
||||
"{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
|
||||
)
|
||||
old_doc = frappe.db.get_all(f"{doc.doctype} Item", {"parent": doc.name}, ["name", "service_stop_date"])
|
||||
|
||||
for d in old_doc:
|
||||
old_stop_dates[d.name] = d.service_stop_date or ""
|
||||
@@ -62,16 +58,14 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += "AND %s='%s'" % (deferred_account, account)
|
||||
conditions += f"AND {deferred_account}='{account}'"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def convert_deferred_expense_to_expense(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=""):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@@ -81,16 +75,14 @@ def convert_deferred_expense_to_expense(
|
||||
|
||||
# check for the purchase invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
f"""
|
||||
select distinct item.parent
|
||||
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_expense = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
{conditions}
|
||||
""",
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
@@ -103,9 +95,7 @@ def convert_deferred_expense_to_expense(
|
||||
send_mail(deferred_process)
|
||||
|
||||
|
||||
def convert_deferred_revenue_to_income(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=""):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@@ -115,16 +105,14 @@ def convert_deferred_revenue_to_income(
|
||||
|
||||
# check for the sales invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
f"""
|
||||
select distinct item.parent
|
||||
from `tabSales Invoice Item` item, `tabSales Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_revenue = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
{conditions}
|
||||
""",
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
@@ -243,9 +231,7 @@ def calculate_monthly_amount(
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
@@ -265,17 +251,13 @@ def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, a
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(
|
||||
item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
|
||||
)
|
||||
amount = flt(item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount"))
|
||||
else:
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
@@ -296,26 +278,22 @@ def get_already_booked_amount(doc, item):
|
||||
|
||||
gl_entries_details = frappe.db.sql(
|
||||
"""
|
||||
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
|
||||
select sum({}) as total_credit, sum({}) as total_credit_in_account_currency, voucher_detail_no
|
||||
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
group by voucher_detail_no
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
""".format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
journal_entry_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
|
||||
SELECT sum(c.{}) as total_credit, sum(c.{}) as total_credit_in_account_currency, reference_detail_no
|
||||
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
|
||||
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
|
||||
and p.docstatus < 2 group by reference_detail_no
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
""".format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -337,9 +315,7 @@ def get_already_booked_amount(doc, item):
|
||||
|
||||
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
@@ -384,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
|
||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
@@ -440,9 +416,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
via_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
|
||||
)
|
||||
submit_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
|
||||
)
|
||||
submit_journal_entry = cint(frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries"))
|
||||
book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
@@ -462,9 +436,7 @@ def process_deferred_accounting(posting_date=None):
|
||||
posting_date = today()
|
||||
|
||||
if not cint(
|
||||
frappe.db.get_singles_value(
|
||||
"Accounts Settings", "automatically_process_deferred_accounting_entry"
|
||||
)
|
||||
frappe.db.get_singles_value("Accounts Settings", "automatically_process_deferred_accounting_entry")
|
||||
):
|
||||
return
|
||||
|
||||
@@ -587,16 +559,13 @@ def book_revenue_via_journal_entry(
|
||||
deferred_process=None,
|
||||
submit="No",
|
||||
):
|
||||
|
||||
if amount == 0:
|
||||
return
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.posting_date = posting_date
|
||||
journal_entry.company = doc.company
|
||||
journal_entry.voucher_type = (
|
||||
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
)
|
||||
journal_entry.voucher_type = "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
journal_entry.process_deferred_accounting = deferred_process
|
||||
|
||||
debit_entry = {
|
||||
@@ -645,7 +614,6 @@ def book_revenue_via_journal_entry(
|
||||
|
||||
|
||||
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
credit_account, debit_account = frappe.db.get_value(
|
||||
"Sales Invoice Item",
|
||||
|
||||
@@ -29,7 +29,7 @@ class Account(NestedSet):
|
||||
if frappe.local.flags.ignore_update_nsm:
|
||||
return
|
||||
else:
|
||||
super(Account, self).on_update()
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
@@ -87,9 +87,7 @@ class Account(NestedSet):
|
||||
|
||||
def set_root_and_report_type(self):
|
||||
if self.parent_account:
|
||||
par = frappe.db.get_value(
|
||||
"Account", self.parent_account, ["report_type", "root_type"], as_dict=1
|
||||
)
|
||||
par = frappe.db.get_value("Account", self.parent_account, ["report_type", "root_type"], as_dict=1)
|
||||
|
||||
if par.report_type:
|
||||
self.report_type = par.report_type
|
||||
@@ -144,9 +142,7 @@ class Account(NestedSet):
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
# ignore validation while creating new compnay or while syncing to child companies
|
||||
if (
|
||||
frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
|
||||
):
|
||||
if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
|
||||
return
|
||||
ancestors = get_root_company(self.company)
|
||||
if ancestors:
|
||||
@@ -341,7 +337,7 @@ class Account(NestedSet):
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction can not be deleted"))
|
||||
|
||||
super(Account, self).on_trash(True)
|
||||
super().on_trash(True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -349,9 +345,8 @@ class Account(NestedSet):
|
||||
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = %s
|
||||
and %s like %s order by name limit %s offset %s"""
|
||||
% ("%s", searchfield, "%s", "%s", "%s"),
|
||||
where is_group = 1 and docstatus != 2 and company = {}
|
||||
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
)
|
||||
@@ -409,9 +404,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
if not account:
|
||||
return
|
||||
|
||||
old_acc_name, old_acc_number = frappe.db.get_value(
|
||||
"Account", name, ["account_name", "account_number"]
|
||||
)
|
||||
old_acc_name, old_acc_number = frappe.db.get_value("Account", name, ["account_name", "account_number"])
|
||||
|
||||
# check if account exists in parent company
|
||||
ancestors = get_ancestors_of("Company", account.company)
|
||||
@@ -519,7 +512,5 @@ def sync_update_account_number_in_child(
|
||||
if old_acc_number:
|
||||
filters["account_number"] = old_acc_number
|
||||
|
||||
for d in frappe.db.get_values(
|
||||
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
|
||||
):
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
@@ -31,7 +31,6 @@ def create_charts(
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
@@ -39,7 +38,9 @@ def create_charts(
|
||||
|
||||
is_group = identify_is_group(child)
|
||||
report_type = (
|
||||
"Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
|
||||
"Balance Sheet"
|
||||
if root_type in ["Asset", "Liability", "Equity"]
|
||||
else "Profit and Loss"
|
||||
)
|
||||
|
||||
account = frappe.get_doc(
|
||||
@@ -141,7 +142,7 @@ def get_chart(chart_template, existing_company=None):
|
||||
for fname in os.listdir(path):
|
||||
fname = frappe.as_unicode(fname)
|
||||
if fname.endswith(".json"):
|
||||
with open(os.path.join(path, fname), "r") as f:
|
||||
with open(os.path.join(path, fname)) as f:
|
||||
chart = f.read()
|
||||
if chart and json.loads(chart).get("name") == chart_template:
|
||||
return json.loads(chart).get("tree")
|
||||
@@ -173,7 +174,7 @@ def get_charts_for_country(country, with_standard=False):
|
||||
for fname in os.listdir(path):
|
||||
fname = frappe.as_unicode(fname)
|
||||
if (fname.startswith(country_code) or fname.startswith(country)) and fname.endswith(".json"):
|
||||
with open(os.path.join(path, fname), "r") as f:
|
||||
with open(os.path.join(path, fname)) as f:
|
||||
_get_chart_name(f.read())
|
||||
|
||||
# if more than one charts, returned then add the standard
|
||||
@@ -247,7 +248,13 @@ def validate_bank_account(coa, bank_account):
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
|
||||
if account_name not in [
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
|
||||
@@ -26,7 +26,7 @@ def go():
|
||||
default_account_types = get_default_account_types()
|
||||
|
||||
country_dirs = []
|
||||
for basepath, folders, files in os.walk(path):
|
||||
for basepath, _folders, _files in os.walk(path):
|
||||
basename = os.path.basename(basepath)
|
||||
if basename.startswith("l10n_"):
|
||||
country_dirs.append(basename)
|
||||
@@ -35,9 +35,7 @@ def go():
|
||||
accounts, charts = {}, {}
|
||||
country_path = os.path.join(path, country_dir)
|
||||
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
|
||||
data_files = (
|
||||
manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
|
||||
)
|
||||
data_files = manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
|
||||
files_path = [os.path.join(country_path, d) for d in data_files]
|
||||
xml_roots = get_xml_roots(files_path)
|
||||
csv_content = get_csv_contents(files_path)
|
||||
@@ -90,10 +88,10 @@ def get_csv_contents(files_path):
|
||||
fname = os.path.basename(filepath)
|
||||
for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
|
||||
if fname.startswith(file_type) and fname.endswith(".csv"):
|
||||
with open(filepath, "r") as csvfile:
|
||||
with open(filepath) as csvfile:
|
||||
try:
|
||||
csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
continue
|
||||
return csv_content
|
||||
|
||||
@@ -138,7 +136,7 @@ def get_account_types(root_list, csv_content, prefix=None):
|
||||
|
||||
if csv_content and csv_content[0][0] == "id":
|
||||
for row in csv_content[1:]:
|
||||
row_dict = dict(zip(csv_content[0], row))
|
||||
row_dict = dict(zip(csv_content[0], row, strict=False))
|
||||
data = {}
|
||||
if row_dict.get("code") and account_type_map.get(row_dict["code"]):
|
||||
data["account_type"] = account_type_map[row_dict["code"]]
|
||||
@@ -150,7 +148,7 @@ def get_account_types(root_list, csv_content, prefix=None):
|
||||
|
||||
def make_maps_for_xml(xml_roots, account_types, country_dir):
|
||||
"""make maps for `charts` and `accounts`"""
|
||||
for model, root_list in xml_roots.items():
|
||||
for _model, root_list in xml_roots.items():
|
||||
for root in root_list:
|
||||
for node in root[0].findall("record"):
|
||||
if node.get("model") == "account.account.template":
|
||||
@@ -186,7 +184,7 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
|
||||
def make_maps_for_csv(csv_content, account_types, country_dir):
|
||||
for content in csv_content.get("account.account.template", []):
|
||||
for row in content[1:]:
|
||||
data = dict(zip(content[0], row))
|
||||
data = dict(zip(content[0], row, strict=False))
|
||||
account = {
|
||||
"name": data.get("name"),
|
||||
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
|
||||
@@ -206,7 +204,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
|
||||
for content in csv_content.get("account.chart.template", []):
|
||||
for row in content[1:]:
|
||||
if row:
|
||||
data = dict(zip(content[0], row))
|
||||
data = dict(zip(content[0], row, strict=False))
|
||||
charts.setdefault(data.get("id"), {}).update(
|
||||
{
|
||||
"account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
|
||||
@@ -241,7 +239,7 @@ def make_charts():
|
||||
if not src.get("name") or not src.get("account_root_id"):
|
||||
continue
|
||||
|
||||
if not src["account_root_id"] in accounts:
|
||||
if src["account_root_id"] not in accounts:
|
||||
continue
|
||||
|
||||
filename = src["id"][5:] + "_" + chart_id
|
||||
@@ -255,14 +253,20 @@ def make_charts():
|
||||
for key, val in chart["tree"].items():
|
||||
if key in ["name", "parent_id"]:
|
||||
chart["tree"].pop(key)
|
||||
if type(val) == dict:
|
||||
if isinstance(val, dict):
|
||||
val["root_type"] = ""
|
||||
if chart:
|
||||
fpath = os.path.join(
|
||||
"erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
|
||||
"erpnext",
|
||||
"erpnext",
|
||||
"accounts",
|
||||
"doctype",
|
||||
"account",
|
||||
"chart_of_accounts",
|
||||
filename + ".json",
|
||||
)
|
||||
|
||||
with open(fpath, "r") as chartfile:
|
||||
with open(fpath) as chartfile:
|
||||
old_content = chartfile.read()
|
||||
if not old_content or (
|
||||
json.loads(old_content).get("is_active", "No") == "No"
|
||||
|
||||
@@ -260,28 +260,20 @@ class TestAccount(unittest.TestCase):
|
||||
acc.insert()
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
|
||||
)
|
||||
frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 4"})
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 5"})
|
||||
)
|
||||
|
||||
# Try renaming child company account
|
||||
acc_tc_5 = frappe.db.get_value(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
|
||||
|
||||
# Rename child company account with allow_account_creation_against_child_company enabled
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
|
||||
|
||||
update_account_number(acc_tc_5, "Test Modified Account")
|
||||
self.assertTrue(
|
||||
@@ -290,9 +282,7 @@ class TestAccount(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
|
||||
|
||||
to_delete = [
|
||||
"Test Group Account - _TC3",
|
||||
@@ -317,9 +307,7 @@ class TestAccount(unittest.TestCase):
|
||||
self.assertEqual(acc.account_currency, "INR")
|
||||
|
||||
# Make a JV against this account
|
||||
make_journal_entry(
|
||||
"Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
|
||||
)
|
||||
make_journal_entry("Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True)
|
||||
|
||||
acc.account_currency = "USD"
|
||||
self.assertRaises(frappe.ValidationError, acc.save)
|
||||
|
||||
@@ -17,16 +17,12 @@ class AccountClosingBalance(Document):
|
||||
def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
previous_closing_entries = get_previous_closing_entries(
|
||||
company, closing_date, accounting_dimensions
|
||||
)
|
||||
previous_closing_entries = get_previous_closing_entries(company, closing_date, accounting_dimensions)
|
||||
combined_entries = closing_entries + previous_closing_entries
|
||||
|
||||
merged_entries = aggregate_with_last_account_closing_balance(
|
||||
combined_entries, accounting_dimensions
|
||||
)
|
||||
merged_entries = aggregate_with_last_account_closing_balance(combined_entries, accounting_dimensions)
|
||||
|
||||
for key, value in merged_entries.items():
|
||||
for _key, value in merged_entries.items():
|
||||
cle = frappe.new_doc("Account Closing Balance")
|
||||
cle.update(value)
|
||||
cle.update(value["dimensions"])
|
||||
|
||||
@@ -17,7 +17,8 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
if self.document_type in core_doctypes_list + (
|
||||
if self.document_type in (
|
||||
*core_doctypes_list,
|
||||
"Accounting Dimension",
|
||||
"Project",
|
||||
"Cost Center",
|
||||
@@ -25,13 +26,10 @@ class AccountingDimension(Document):
|
||||
"Company",
|
||||
"Account",
|
||||
):
|
||||
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
exists = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.document_type}, ["name"]
|
||||
)
|
||||
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
|
||||
|
||||
if exists and self.is_new():
|
||||
frappe.throw(_("Document Type already used as a dimension"))
|
||||
@@ -89,7 +87,6 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
count = 0
|
||||
|
||||
for doctype in doclist:
|
||||
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
@@ -123,7 +120,7 @@ def add_dimension_to_budget_doctype(df, doc):
|
||||
df.update(
|
||||
{
|
||||
"insert_after": "cost_center",
|
||||
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
|
||||
"depends_on": f"eval:doc.budget_against == '{doc.document_type}'",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -157,19 +154,17 @@ def delete_accounting_dimension(doc):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabCustom Field`
|
||||
WHERE fieldname = %s
|
||||
AND dt IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
WHERE fieldname = {}
|
||||
AND dt IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname, *doclist]),
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabProperty Setter`
|
||||
WHERE field_name = %s
|
||||
AND doc_type IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
WHERE field_name = {}
|
||||
AND doc_type IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname, *doclist]),
|
||||
)
|
||||
|
||||
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
|
||||
@@ -218,7 +213,6 @@ def get_doctypes_with_dimensions():
|
||||
|
||||
|
||||
def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
if not filters:
|
||||
filters = {"disabled": 0}
|
||||
|
||||
@@ -249,7 +243,6 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
|
||||
|
||||
def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
if isinstance(dimensions, str):
|
||||
dimensions = [dimensions]
|
||||
|
||||
@@ -257,9 +250,7 @@ def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
for dimension in dimensions:
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||
)
|
||||
children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
return all_dimensions
|
||||
|
||||
@@ -57,9 +57,7 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
|
||||
|
||||
def create_accounting_dimension_filter():
|
||||
if not frappe.db.get_value(
|
||||
"Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
|
||||
):
|
||||
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension Filter",
|
||||
|
||||
@@ -67,7 +67,10 @@ class AccountingPeriod(Document):
|
||||
for doctype_for_closing in self.get_doctypes_for_closing():
|
||||
self.append(
|
||||
"closed_documents",
|
||||
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
|
||||
{
|
||||
"document_type": doctype_for_closing.document_type,
|
||||
"closed": doctype_for_closing.closed,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ class TestAccountingPeriod(unittest.TestCase):
|
||||
ap1 = create_accounting_period(period_name="Test Accounting Period 2")
|
||||
ap1.save()
|
||||
|
||||
doc = create_sales_invoice(
|
||||
do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
|
||||
)
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
@@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@@ -27,7 +27,7 @@ class BankClearance(Document):
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
@@ -41,9 +41,7 @@ class BankClearance(Document):
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
""",
|
||||
{"account": self.account, "from": self.from_date, "to": self.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -52,7 +50,7 @@ class BankClearance(Document):
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
@@ -67,9 +65,7 @@ class BankClearance(Document):
|
||||
{condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
""",
|
||||
{
|
||||
"account": self.account,
|
||||
"from": self.from_date,
|
||||
@@ -132,11 +128,9 @@ class BankClearance(Document):
|
||||
query = query.where(loan_repayment.clearance_date.isnull())
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
query = query.where(loan_repayment.repay_from_salary == 0)
|
||||
|
||||
query = query.orderby(loan_repayment.posting_date).orderby(
|
||||
loan_repayment.name, order=frappe.qb.desc
|
||||
)
|
||||
query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, order=frappe.qb.desc)
|
||||
|
||||
loan_repayments = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
def get_account_balance(bank_account, till_date):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict(
|
||||
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
||||
)
|
||||
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
|
||||
data = get_entries(filters)
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
@@ -76,10 +74,7 @@ def get_account_balance(bank_account, till_date):
|
||||
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
|
||||
|
||||
bank_bal = (
|
||||
flt(balance_as_per_system)
|
||||
- flt(total_debit)
|
||||
+ flt(total_credit)
|
||||
+ amounts_not_reflected_in_system
|
||||
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
)
|
||||
|
||||
return bank_bal
|
||||
@@ -377,12 +372,13 @@ def auto_reconcile_vouchers(
|
||||
)
|
||||
transaction = frappe.get_doc("Bank Transaction", transaction.name)
|
||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
matched_trans = 0
|
||||
for voucher in vouchers:
|
||||
gl_entry = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
dict(
|
||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
||||
account=account,
|
||||
voucher_type=voucher["payment_doctype"],
|
||||
voucher_no=voucher["payment_name"],
|
||||
),
|
||||
["credit", "debit"],
|
||||
as_dict=1,
|
||||
@@ -731,7 +727,7 @@ def get_lr_matching_query(bank_account, exact_match, filters):
|
||||
)
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
query = query.where(loan_repayment.repay_from_salary == 0)
|
||||
|
||||
if exact_match:
|
||||
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
||||
@@ -764,7 +760,7 @@ def get_pe_matching_query(
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " reference_date"
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
SELECT
|
||||
@@ -815,7 +811,7 @@ def get_je_matching_query(
|
||||
if cint(filter_by_reference_date):
|
||||
filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
|
||||
order_by = " je.cheque_date"
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
|
||||
return f"""
|
||||
SELECT
|
||||
|
||||
@@ -21,7 +21,7 @@ INVALID_VALUES = ("", None)
|
||||
|
||||
class BankStatementImport(DataImport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BankStatementImport, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
@@ -30,7 +30,6 @@ class BankStatementImport(DataImport):
|
||||
or (doc_before_save and doc_before_save.import_file != self.import_file)
|
||||
or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url)
|
||||
):
|
||||
|
||||
template_options_dict = {}
|
||||
column_to_field_map = {}
|
||||
bank = frappe.get_doc("Bank", self.bank)
|
||||
@@ -45,7 +44,6 @@ class BankStatementImport(DataImport):
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
@@ -102,7 +100,7 @@ def download_errored_template(data_import_name):
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
for i, row in enumerate(raw_data):
|
||||
for _i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
continue
|
||||
@@ -112,9 +110,7 @@ def parse_data_from_template(raw_data):
|
||||
return data
|
||||
|
||||
|
||||
def start_import(
|
||||
data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
|
||||
):
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
update_mapping_db(bank, template_options)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
@@ -19,7 +17,7 @@ class AutoMatchParty:
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
def match(self) -> tuple | None:
|
||||
result = None
|
||||
result = AutoMatchbyAccountIBAN(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
@@ -50,7 +48,7 @@ class AutoMatchbyAccountIBAN:
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
|
||||
def match_account_in_party(self) -> Union[Tuple, None]:
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
@@ -97,7 +95,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
def match(self) -> tuple | None:
|
||||
# fuzzy search by customer/supplier & employee
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
@@ -105,7 +103,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
|
||||
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
|
||||
def match_party_name_desc_in_party(self) -> tuple | None:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
@@ -130,7 +128,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
return result
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None:
|
||||
skip = False
|
||||
result = process.extract(
|
||||
query=self.get(field),
|
||||
@@ -147,7 +145,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
party_name,
|
||||
), skip
|
||||
|
||||
def process_fuzzy_result(self, result: Union[list, None]):
|
||||
def process_fuzzy_result(self, result: list | None):
|
||||
"""
|
||||
If there are multiple valid close matches return None as result may be faulty.
|
||||
Return the result only if one accurate match stands out.
|
||||
|
||||
@@ -186,9 +186,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -286,7 +284,6 @@ def get_total_allocated_amount(doctype, docname):
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
@@ -325,9 +322,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Repayment":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||
)
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
|
||||
|
||||
elif payment_entry.payment_document == "Bank Transaction":
|
||||
dep, wth = frappe.db.get_value(
|
||||
@@ -337,9 +332,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
else:
|
||||
frappe.throw(
|
||||
"Please reconcile {0}: {1} manually".format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ def upload_bank_statement():
|
||||
fcontent = frappe.local.uploaded_file
|
||||
fname = frappe.local.uploaded_filename
|
||||
|
||||
if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
|
||||
if frappe.safe_encode(fname).lower().endswith(b"csv"):
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
|
||||
rows = read_csv_content(fcontent, False)
|
||||
|
||||
elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
|
||||
elif frappe.safe_encode(fname).lower().endswith(b"xlsx"):
|
||||
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
|
||||
|
||||
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
|
||||
|
||||
@@ -430,9 +430,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||
):
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bisect Accounting Statements", {
|
||||
onload(frm) {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__("Bisect Left"), () => {
|
||||
frm.trigger("bisect_left");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Bisect Right"), () => {
|
||||
frm.trigger("bisect_right");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Up"), () => {
|
||||
frm.trigger("move_up");
|
||||
});
|
||||
frm.add_custom_button(__("Build Tree"), () => {
|
||||
frm.trigger("build_tree");
|
||||
});
|
||||
},
|
||||
render_heatmap(frm) {
|
||||
let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
|
||||
bisect_heatmap.addClass("bisect_heatmap_location");
|
||||
|
||||
// milliseconds in a day
|
||||
let msiad = 24 * 60 * 60 * 1000;
|
||||
let datapoints = {};
|
||||
let fr_dt = new Date(frm.doc.from_date).getTime();
|
||||
let to_dt = new Date(frm.doc.to_date).getTime();
|
||||
let bisect_start = new Date(frm.doc.current_from_date).getTime();
|
||||
let bisect_end = new Date(frm.doc.current_to_date).getTime();
|
||||
|
||||
for (let x = fr_dt; x <= to_dt; x += msiad) {
|
||||
let epoch_in_seconds = x / 1000;
|
||||
if (bisect_start <= x && x <= bisect_end) {
|
||||
datapoints[epoch_in_seconds] = 1.0;
|
||||
} else {
|
||||
datapoints[epoch_in_seconds] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
new frappe.Chart(".bisect_heatmap_location", {
|
||||
type: "heatmap",
|
||||
data: {
|
||||
dataPoints: datapoints,
|
||||
start: new Date(frm.doc.from_date),
|
||||
end: new Date(frm.doc.to_date),
|
||||
},
|
||||
countLabel: "Bisecting",
|
||||
discreteDomains: 1,
|
||||
});
|
||||
},
|
||||
bisect_left(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "bisect_left",
|
||||
freeze: true,
|
||||
freeze_message: __("Bisecting Left ..."),
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
bisect_right(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Bisecting Right ..."),
|
||||
method: "bisect_right",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
move_up(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Moving up in tree ..."),
|
||||
method: "move_up",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
build_tree(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Rebuilding BTree for period ..."),
|
||||
method: "build_tree",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-09-15 21:28:28.054773",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_cvfg",
|
||||
"company",
|
||||
"column_break_hcam",
|
||||
"from_date",
|
||||
"column_break_qxbi",
|
||||
"to_date",
|
||||
"column_break_iwny",
|
||||
"algorithm",
|
||||
"section_break_8ph9",
|
||||
"current_node",
|
||||
"section_break_ngid",
|
||||
"bisect_heatmap",
|
||||
"section_break_hmsy",
|
||||
"bisecting_from",
|
||||
"current_from_date",
|
||||
"column_break_uqyd",
|
||||
"bisecting_to",
|
||||
"current_to_date",
|
||||
"section_break_hbyo",
|
||||
"heading_cppb",
|
||||
"p_l_summary",
|
||||
"column_break_aivo",
|
||||
"balance_sheet_summary",
|
||||
"b_s_summary",
|
||||
"column_break_gvwx",
|
||||
"difference_heading",
|
||||
"difference"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_qxbi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "From Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "To Date"
|
||||
},
|
||||
{
|
||||
"default": "BFS",
|
||||
"fieldname": "algorithm",
|
||||
"fieldtype": "Select",
|
||||
"label": "Algorithm",
|
||||
"options": "BFS\nDFS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iwny",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_node",
|
||||
"fieldtype": "Link",
|
||||
"label": "Current Node",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hmsy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current_to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uqyd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hbyo",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "p_l_summary",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "b_s_summary",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "difference",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_aivo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gvwx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hcam",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ngid",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8ph9",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bisect_heatmap",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Heatmap"
|
||||
},
|
||||
{
|
||||
"fieldname": "heading_cppb",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Profit and Loss Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_sheet_summary",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Balance Sheet Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_heading",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Difference"
|
||||
},
|
||||
{
|
||||
"fieldname": "bisecting_from",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Bisecting From"
|
||||
},
|
||||
{
|
||||
"fieldname": "bisecting_to",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Bisecting To"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_cvfg",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-01 16:49:54.073890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bisect Accounting Statements",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import datetime
|
||||
from collections import deque
|
||||
from math import floor
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils.data import guess_date_format
|
||||
|
||||
|
||||
class BisectAccountingStatements(Document):
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.from_date) > getdate(self.to_date):
|
||||
frappe.throw(
|
||||
_("From Date: {0} cannot be greater than To date: {1}").format(
|
||||
frappe.bold(self.from_date), frappe.bold(self.to_date)
|
||||
)
|
||||
)
|
||||
|
||||
def bfs(self, from_date: datetime, to_date: datetime):
|
||||
# Make Root node
|
||||
node = frappe.new_doc("Bisect Nodes")
|
||||
node.root = None
|
||||
node.period_from_date = from_date
|
||||
node.period_to_date = to_date
|
||||
node.insert()
|
||||
|
||||
period_queue = deque([node])
|
||||
while period_queue:
|
||||
cur_node = period_queue.popleft()
|
||||
delta = cur_node.period_to_date - cur_node.period_from_date
|
||||
if delta.days == 0:
|
||||
continue
|
||||
else:
|
||||
cur_floor = floor(delta.days / 2)
|
||||
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
|
||||
left_node = frappe.new_doc("Bisect Nodes")
|
||||
left_node.period_from_date = cur_node.period_from_date
|
||||
left_node.period_to_date = next_to_date
|
||||
left_node.root = cur_node.name
|
||||
left_node.generated = False
|
||||
left_node.insert()
|
||||
cur_node.left_child = left_node.name
|
||||
period_queue.append(left_node)
|
||||
|
||||
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
|
||||
right_node = frappe.new_doc("Bisect Nodes")
|
||||
right_node.period_from_date = next_from_date
|
||||
right_node.period_to_date = cur_node.period_to_date
|
||||
right_node.root = cur_node.name
|
||||
right_node.generated = False
|
||||
right_node.insert()
|
||||
cur_node.right_child = right_node.name
|
||||
period_queue.append(right_node)
|
||||
|
||||
cur_node.save()
|
||||
|
||||
def dfs(self, from_date: datetime, to_date: datetime):
|
||||
# Make Root node
|
||||
node = frappe.new_doc("Bisect Nodes")
|
||||
node.root = None
|
||||
node.period_from_date = from_date
|
||||
node.period_to_date = to_date
|
||||
node.insert()
|
||||
|
||||
period_stack = [node]
|
||||
while period_stack:
|
||||
cur_node = period_stack.pop()
|
||||
delta = cur_node.period_to_date - cur_node.period_from_date
|
||||
if delta.days == 0:
|
||||
continue
|
||||
else:
|
||||
cur_floor = floor(delta.days / 2)
|
||||
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
|
||||
left_node = frappe.new_doc("Bisect Nodes")
|
||||
left_node.period_from_date = cur_node.period_from_date
|
||||
left_node.period_to_date = next_to_date
|
||||
left_node.root = cur_node.name
|
||||
left_node.generated = False
|
||||
left_node.insert()
|
||||
cur_node.left_child = left_node.name
|
||||
period_stack.append(left_node)
|
||||
|
||||
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
|
||||
right_node = frappe.new_doc("Bisect Nodes")
|
||||
right_node.period_from_date = next_from_date
|
||||
right_node.period_to_date = cur_node.period_to_date
|
||||
right_node.root = cur_node.name
|
||||
right_node.generated = False
|
||||
right_node.insert()
|
||||
cur_node.right_child = right_node.name
|
||||
period_stack.append(right_node)
|
||||
|
||||
cur_node.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def build_tree(self):
|
||||
frappe.db.delete("Bisect Nodes")
|
||||
|
||||
# Convert str to datetime format
|
||||
dt_format = guess_date_format(self.from_date)
|
||||
from_date = datetime.datetime.strptime(self.from_date, dt_format)
|
||||
to_date = datetime.datetime.strptime(self.to_date, dt_format)
|
||||
|
||||
if self.algorithm == "BFS":
|
||||
self.bfs(from_date, to_date)
|
||||
|
||||
if self.algorithm == "DFS":
|
||||
self.dfs(from_date, to_date)
|
||||
|
||||
# set root as current node
|
||||
root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
|
||||
self.get_report_summary()
|
||||
self.current_node = root.name
|
||||
self.current_from_date = self.from_date
|
||||
self.current_to_date = self.to_date
|
||||
self.save()
|
||||
|
||||
def get_report_summary(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": self.current_from_date,
|
||||
"period_end_date": self.current_to_date,
|
||||
"periodicity": "Yearly",
|
||||
}
|
||||
pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
|
||||
self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
|
||||
bs_summary = frappe.get_doc("Report", "Balance Sheet")
|
||||
self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
|
||||
self.difference = abs(self.p_l_summary - self.b_s_summary)
|
||||
|
||||
def update_node(self):
|
||||
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
current_node.balance_sheet_summary = self.b_s_summary
|
||||
current_node.profit_loss_summary = self.p_l_summary
|
||||
current_node.difference = self.difference
|
||||
current_node.generated = True
|
||||
current_node.save()
|
||||
|
||||
def current_node_has_summary_info(self):
|
||||
"Assertion method"
|
||||
return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
|
||||
|
||||
def fetch_summary_info_from_current_node(self):
|
||||
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
self.p_l_summary = current_node.balance_sheet_summary
|
||||
self.b_s_summary = current_node.profit_loss_summary
|
||||
self.difference = abs(self.p_l_summary - self.b_s_summary)
|
||||
|
||||
def fetch_or_calculate(self):
|
||||
if self.current_node_has_summary_info():
|
||||
self.fetch_summary_info_from_current_node()
|
||||
else:
|
||||
self.get_report_summary()
|
||||
self.update_node()
|
||||
|
||||
@frappe.whitelist()
|
||||
def bisect_left(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.left_child is not None:
|
||||
lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
|
||||
self.current_node = cur_node.left_child
|
||||
self.current_from_date = lft_node.period_from_date
|
||||
self.current_to_date = lft_node.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Left"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def bisect_right(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.right_child is not None:
|
||||
rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
|
||||
self.current_node = cur_node.right_child
|
||||
self.current_from_date = rgt_node.period_from_date
|
||||
self.current_to_date = rgt_node.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Right"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_up(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.root is not None:
|
||||
root = frappe.get_doc("Bisect Nodes", cur_node.root)
|
||||
self.current_node = cur_node.root
|
||||
self.current_from_date = root.period_from_date
|
||||
self.current_to_date = root.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("Reached Root"))
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBisectAccountingStatements(FrappeTestCase):
|
||||
pass
|
||||
0
erpnext/accounts/doctype/bisect_nodes/__init__.py
Normal file
0
erpnext/accounts/doctype/bisect_nodes/__init__.py
Normal file
8
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
Normal file
8
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Bisect Nodes", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
97
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
Normal file
97
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2023-09-27 14:56:38.112462",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"root",
|
||||
"left_child",
|
||||
"right_child",
|
||||
"period_from_date",
|
||||
"period_to_date",
|
||||
"difference",
|
||||
"balance_sheet_summary",
|
||||
"profit_loss_summary",
|
||||
"generated"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "root",
|
||||
"fieldtype": "Link",
|
||||
"label": "Root",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "left_child",
|
||||
"fieldtype": "Link",
|
||||
"label": "Left Child",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "right_child",
|
||||
"fieldtype": "Link",
|
||||
"label": "Right Child",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Period_from_date"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Period To Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference",
|
||||
"fieldtype": "Float",
|
||||
"label": "Difference"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_sheet_summary",
|
||||
"fieldtype": "Float",
|
||||
"label": "Balance Sheet Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "profit_loss_summary",
|
||||
"fieldtype": "Float",
|
||||
"label": "Profit and Loss Summary"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Generated"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-01 17:46:12.437996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bisect Nodes",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
29
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
Normal file
29
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BisectNodes(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
|
||||
|
||||
balance_sheet_summary: DF.Float
|
||||
difference: DF.Float
|
||||
generated: DF.Check
|
||||
left_child: DF.Link | None
|
||||
name: DF.Int | None
|
||||
period_from_date: DF.Datetime | None
|
||||
period_to_date: DF.Datetime | None
|
||||
profit_loss_summary: DF.Float
|
||||
right_child: DF.Link | None
|
||||
root: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBisectNodes(FrappeTestCase):
|
||||
pass
|
||||
@@ -40,10 +40,11 @@ class Budget(Document):
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
|
||||
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
|
||||
% ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
|
||||
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
|
||||
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
|
||||
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
|
||||
),
|
||||
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -66,12 +67,14 @@ class Budget(Document):
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
|
||||
frappe.throw(
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
d.account
|
||||
)
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
@@ -118,9 +121,7 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not frappe.get_cached_value(
|
||||
"Budget", {"fiscal_year": args.fiscal_year, "company": args.company}
|
||||
): # nosec
|
||||
if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec
|
||||
return
|
||||
|
||||
if not args.account:
|
||||
@@ -151,30 +152,24 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
and args.account
|
||||
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
|
||||
):
|
||||
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = """and exists(select name from `tab%s`
|
||||
where lft<=%s and rgt>=%s and name=b.%s)""" % (
|
||||
doctype,
|
||||
lft,
|
||||
rgt,
|
||||
budget_against,
|
||||
) # nosec
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
args.is_tree = True
|
||||
else:
|
||||
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
|
||||
args.is_tree = False
|
||||
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
||||
@@ -187,9 +182,7 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
{condition}
|
||||
""".format(
|
||||
condition=condition, budget_against_field=budget_against
|
||||
),
|
||||
""",
|
||||
(args.fiscal_year, args.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
@@ -201,12 +194,18 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
amount = expense_amount or get_amount(args, budget)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
args["for_material_request"] = budget.for_material_request
|
||||
args["for_purchase_order"] = budget.for_purchase_order
|
||||
|
||||
if yearly_action in ("Stop", "Warn"):
|
||||
compare_expense_with_budget(
|
||||
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
|
||||
args,
|
||||
flt(budget.budget_amount),
|
||||
_("Annual"),
|
||||
yearly_action,
|
||||
budget.budget_against,
|
||||
expense_amount,
|
||||
)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
@@ -217,18 +216,32 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
|
||||
compare_expense_with_budget(
|
||||
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
|
||||
args,
|
||||
budget_amount,
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
budget.budget_against,
|
||||
expense_amount,
|
||||
)
|
||||
|
||||
|
||||
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
|
||||
actual_expense = get_actual_expense(args)
|
||||
total_expense = actual_expense + amount
|
||||
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
|
||||
if not amount:
|
||||
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
|
||||
|
||||
if args.get("doctype") == "Material Request" and args.for_material_request:
|
||||
amount = args.requested_amount + args.ordered_amount
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
|
||||
amount = args.ordered_amount
|
||||
|
||||
total_expense = args.actual_expense + amount
|
||||
|
||||
if total_expense > budget_amount:
|
||||
if actual_expense > budget_amount:
|
||||
if args.actual_expense > budget_amount:
|
||||
error_tense = _("is already")
|
||||
diff = actual_expense - budget_amount
|
||||
diff = args.actual_expense - budget_amount
|
||||
else:
|
||||
error_tense = _("will be")
|
||||
diff = total_expense - budget_amount
|
||||
@@ -245,9 +258,10 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
if (
|
||||
frappe.flags.exception_approver_role
|
||||
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
|
||||
msg += get_expense_breakup(args, currency, budget_against)
|
||||
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
):
|
||||
action = "Warn"
|
||||
|
||||
@@ -257,6 +271,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
args.budget_against_field: budget_against,
|
||||
"account": args.account,
|
||||
"company": args.company,
|
||||
}
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label="Actual Expenses",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
||||
"is_cancelled": 0,
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label="Material Requests",
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Stopped"]],
|
||||
"docstatus": 1,
|
||||
"material_request_type": "Purchase",
|
||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_ordered": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||
+ "</li>"
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label="Unbilled Orders",
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"status": [["!=", "Closed"]],
|
||||
"docstatus": 1,
|
||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
||||
"item_code": args.item_code,
|
||||
"per_billed": [["<", 100]],
|
||||
}
|
||||
),
|
||||
)
|
||||
+ " - "
|
||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def get_actions(args, budget):
|
||||
yearly_action = budget.action_if_annual_budget_exceeded
|
||||
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
|
||||
@@ -272,31 +363,15 @@ def get_actions(args, budget):
|
||||
return yearly_action, monthly_action
|
||||
|
||||
|
||||
def get_amount(args, budget):
|
||||
amount = 0
|
||||
|
||||
if args.get("doctype") == "Material Request" and budget.for_material_request:
|
||||
amount = (
|
||||
get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
)
|
||||
|
||||
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
|
||||
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def get_requested_amount(args, budget):
|
||||
def get_requested_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Material Request")
|
||||
condition = get_other_condition(args, "Material Request")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
|
||||
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
|
||||
condition
|
||||
),
|
||||
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {} and
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition),
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
@@ -304,17 +379,15 @@ def get_requested_amount(args, budget):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_ordered_amount(args, budget):
|
||||
def get_ordered_amount(args):
|
||||
item_code = args.get("item_code")
|
||||
condition = get_other_condition(args, budget, "Purchase Order")
|
||||
condition = get_other_condition(args, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
|
||||
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
|
||||
and parent.status != 'Closed' and {0}""".format(
|
||||
condition
|
||||
),
|
||||
and parent.status != 'Closed' and {condition}""",
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
@@ -322,12 +395,12 @@ def get_ordered_amount(args, budget):
|
||||
return data[0][0] if data else 0
|
||||
|
||||
|
||||
def get_other_condition(args, budget, for_doc):
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
@@ -335,12 +408,8 @@ def get_other_condition(args, budget, for_doc):
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += """ and parent.%s
|
||||
between '%s' and '%s' """ % (
|
||||
date_field,
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
@@ -359,21 +428,17 @@ def get_actual_expense(args):
|
||||
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})""".format(
|
||||
doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
|
||||
)
|
||||
and name=gle.{budget_against_field})"""
|
||||
else:
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
where name=gle.{budget_against} and
|
||||
gle.{budget_against} = %({budget_against})s)""".format(
|
||||
doctype=args.budget_against_doctype, budget_against=budget_against_field
|
||||
)
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
@@ -384,9 +449,7 @@ def get_actual_expense(args):
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""".format(
|
||||
condition1=condition1, condition2=condition2
|
||||
),
|
||||
""",
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
@@ -41,9 +41,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -63,9 +61,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -97,9 +93,7 @@ class TestBudget(unittest.TestCase):
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
@@ -138,9 +132,7 @@ class TestBudget(unittest.TestCase):
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
|
||||
@@ -158,9 +150,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
@@ -223,7 +213,7 @@ class TestBudget(unittest.TestCase):
|
||||
if month > 9:
|
||||
month = 9
|
||||
|
||||
for i in range(month + 1):
|
||||
for _i in range(month + 1):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -237,9 +227,7 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@@ -255,7 +243,7 @@ class TestBudget(unittest.TestCase):
|
||||
month = 9
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
for i in range(month + 1):
|
||||
for _i in range(month + 1):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -270,9 +258,7 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@@ -284,9 +270,7 @@ class TestBudget(unittest.TestCase):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -316,9 +300,7 @@ class TestBudget(unittest.TestCase):
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -423,13 +405,11 @@ def make_budget(**args):
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project_name = "{0}%".format("_Test Project/" + fiscal_year)
|
||||
project_name = "{}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
else:
|
||||
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all(
|
||||
"Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
|
||||
)
|
||||
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
@@ -451,24 +431,18 @@ def make_budget(**args):
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
budget.append(
|
||||
"accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
|
||||
)
|
||||
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
budget.action_if_annual_budget_exceeded_on_mr = (
|
||||
args.action_if_annual_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or "Warn"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_purchase_order:
|
||||
budget.applicable_on_purchase_order = 1
|
||||
budget.action_if_annual_budget_exceeded_on_po = (
|
||||
args.action_if_annual_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or "Warn"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
@@ -26,9 +26,7 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
||||
class ChartofAccountsImporter(Document):
|
||||
def validate(self):
|
||||
if self.import_file:
|
||||
get_coa(
|
||||
"Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
|
||||
)
|
||||
get_coa("Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1)
|
||||
|
||||
|
||||
def validate_columns(data):
|
||||
@@ -104,7 +102,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
file_path = file_doc.get_full_path()
|
||||
|
||||
data = []
|
||||
with open(file_path, "r") as in_file:
|
||||
with open(file_path) as in_file:
|
||||
csv_reader = list(csv.reader(in_file))
|
||||
headers = csv_reader[0]
|
||||
del csv_reader[0] # delete top row and headers row
|
||||
@@ -203,10 +201,10 @@ def build_forest(data):
|
||||
for row in data:
|
||||
account_name, parent_account, account_number, parent_account_number = row[0:4]
|
||||
if account_number:
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
account_name = f"{account_number} - {account_name}"
|
||||
if parent_account_number:
|
||||
parent_account_number = cstr(parent_account_number).strip()
|
||||
parent_account = "{} - {}".format(parent_account_number, parent_account)
|
||||
parent_account = f"{parent_account_number} - {parent_account}"
|
||||
|
||||
if parent_account == account_name == child:
|
||||
return [parent_account]
|
||||
@@ -218,7 +216,7 @@ def build_forest(data):
|
||||
frappe.bold(parent_account)
|
||||
)
|
||||
)
|
||||
return [child] + parent_account_list
|
||||
return [child, *parent_account_list]
|
||||
|
||||
charts_map, paths = {}, []
|
||||
|
||||
@@ -238,12 +236,12 @@ def build_forest(data):
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
error_messages.append(f"Row {line_no}: Please enter Account Name")
|
||||
|
||||
name = account_name
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
account_name = f"{account_number} - {account_name}"
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]["account_name"] = name
|
||||
@@ -340,9 +338,9 @@ def get_template(template_type, company):
|
||||
|
||||
def get_sample_template(writer, company):
|
||||
currency = frappe.db.get_value("Company", company, "default_currency")
|
||||
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f:
|
||||
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv")) as f:
|
||||
for row in f:
|
||||
row = row.strip().split(",") + [currency]
|
||||
row = [*row.strip().split(","), currency]
|
||||
writer.writerow(row)
|
||||
|
||||
return writer
|
||||
@@ -451,7 +449,7 @@ def unset_existing_data(company):
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
'''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -31,71 +31,71 @@ def create_or_update_cheque_print_format(template_name):
|
||||
|
||||
cheque_print.html = """
|
||||
<style>
|
||||
.print-format {
|
||||
.print-format {{
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen {
|
||||
.print-format {
|
||||
}}
|
||||
@media screen {{
|
||||
.print-format {{
|
||||
padding: 0in;
|
||||
}
|
||||
}
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
<div style="position: relative; top:%(starting_position_from_top_edge)scm">
|
||||
<div style="width:%(cheque_width)scm;height:%(cheque_height)scm;">
|
||||
<span style="top:%(acc_pay_dist_from_top_edge)scm; left:%(acc_pay_dist_from_left_edge)scm;
|
||||
<div style="position: relative; top:{starting_position_from_top_edge}cm">
|
||||
<div style="width:{cheque_width}cm;height:{cheque_height}cm;">
|
||||
<span style="top:{acc_pay_dist_from_top_edge}cm; left:{acc_pay_dist_from_left_edge}cm;
|
||||
border-bottom: solid 1px;border-top:solid 1px; width:2cm;text-align: center; position: absolute;">
|
||||
%(message_to_show)s
|
||||
{message_to_show}
|
||||
</span>
|
||||
<span style="top:%(date_dist_from_top_edge)scm; left:%(date_dist_from_left_edge)scm;
|
||||
<span style="top:{date_dist_from_top_edge}cm; left:{date_dist_from_left_edge}cm;
|
||||
position: absolute;">
|
||||
{{ frappe.utils.formatdate(doc.reference_date) or '' }}
|
||||
{{{{ frappe.utils.formatdate(doc.reference_date) or '' }}}}
|
||||
</span>
|
||||
<span style="top:%(acc_no_dist_from_top_edge)scm;left:%(acc_no_dist_from_left_edge)scm;
|
||||
<span style="top:{acc_no_dist_from_top_edge}cm;left:{acc_no_dist_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{ doc.account_no or '' }}
|
||||
{{{{ doc.account_no or '' }}}}
|
||||
</span>
|
||||
<span style="top:%(payer_name_from_top_edge)scm;left: %(payer_name_from_left_edge)scm;
|
||||
<span style="top:{payer_name_from_top_edge}cm;left: {payer_name_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{doc.party_name}}
|
||||
{{{{doc.party_name}}}}
|
||||
</span>
|
||||
<span style="top:%(amt_in_words_from_top_edge)scm; left:%(amt_in_words_from_left_edge)scm;
|
||||
position: absolute; display: block; width: %(amt_in_word_width)scm;
|
||||
line-height:%(amt_in_words_line_spacing)scm; word-wrap: break-word;">
|
||||
{{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}}
|
||||
<span style="top:{amt_in_words_from_top_edge}cm; left:{amt_in_words_from_left_edge}cm;
|
||||
position: absolute; display: block; width: {amt_in_word_width}cm;
|
||||
line-height:{amt_in_words_line_spacing}cm; word-wrap: break-word;">
|
||||
{{{{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}}}}
|
||||
</span>
|
||||
<span style="top:%(amt_in_figures_from_top_edge)scm;left: %(amt_in_figures_from_left_edge)scm;
|
||||
<span style="top:{amt_in_figures_from_top_edge}cm;left: {amt_in_figures_from_left_edge}cm;
|
||||
position: absolute; min-width: 4cm;">
|
||||
{{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}}
|
||||
{{{{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}}}}
|
||||
</span>
|
||||
<span style="top:%(signatory_from_top_edge)scm;left: %(signatory_from_left_edge)scm;
|
||||
<span style="top:{signatory_from_top_edge}cm;left: {signatory_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{doc.company}}
|
||||
{{{{doc.company}}}}
|
||||
</span>
|
||||
</div>
|
||||
</div>""" % {
|
||||
"starting_position_from_top_edge": doc.starting_position_from_top_edge
|
||||
</div>""".format(
|
||||
starting_position_from_top_edge=doc.starting_position_from_top_edge
|
||||
if doc.cheque_size == "A4"
|
||||
else 0.0,
|
||||
"cheque_width": doc.cheque_width,
|
||||
"cheque_height": doc.cheque_height,
|
||||
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
|
||||
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
|
||||
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
|
||||
"date_dist_from_top_edge": doc.date_dist_from_top_edge,
|
||||
"date_dist_from_left_edge": doc.date_dist_from_left_edge,
|
||||
"acc_no_dist_from_top_edge": doc.acc_no_dist_from_top_edge,
|
||||
"acc_no_dist_from_left_edge": doc.acc_no_dist_from_left_edge,
|
||||
"payer_name_from_top_edge": doc.payer_name_from_top_edge,
|
||||
"payer_name_from_left_edge": doc.payer_name_from_left_edge,
|
||||
"amt_in_words_from_top_edge": doc.amt_in_words_from_top_edge,
|
||||
"amt_in_words_from_left_edge": doc.amt_in_words_from_left_edge,
|
||||
"amt_in_word_width": doc.amt_in_word_width,
|
||||
"amt_in_words_line_spacing": doc.amt_in_words_line_spacing,
|
||||
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
|
||||
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
|
||||
"signatory_from_top_edge": doc.signatory_from_top_edge,
|
||||
"signatory_from_left_edge": doc.signatory_from_left_edge,
|
||||
}
|
||||
cheque_width=doc.cheque_width,
|
||||
cheque_height=doc.cheque_height,
|
||||
acc_pay_dist_from_top_edge=doc.acc_pay_dist_from_top_edge,
|
||||
acc_pay_dist_from_left_edge=doc.acc_pay_dist_from_left_edge,
|
||||
message_to_show=doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
|
||||
date_dist_from_top_edge=doc.date_dist_from_top_edge,
|
||||
date_dist_from_left_edge=doc.date_dist_from_left_edge,
|
||||
acc_no_dist_from_top_edge=doc.acc_no_dist_from_top_edge,
|
||||
acc_no_dist_from_left_edge=doc.acc_no_dist_from_left_edge,
|
||||
payer_name_from_top_edge=doc.payer_name_from_top_edge,
|
||||
payer_name_from_left_edge=doc.payer_name_from_left_edge,
|
||||
amt_in_words_from_top_edge=doc.amt_in_words_from_top_edge,
|
||||
amt_in_words_from_left_edge=doc.amt_in_words_from_left_edge,
|
||||
amt_in_word_width=doc.amt_in_word_width,
|
||||
amt_in_words_line_spacing=doc.amt_in_words_line_spacing,
|
||||
amt_in_figures_from_top_edge=doc.amt_in_figures_from_top_edge,
|
||||
amt_in_figures_from_left_edge=doc.amt_in_figures_from_left_edge,
|
||||
signatory_from_top_edge=doc.signatory_from_top_edge,
|
||||
signatory_from_left_edge=doc.signatory_from_left_edge,
|
||||
)
|
||||
|
||||
cheque_print.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-31 13:22:58.916273",
|
||||
"modified": "2024-04-24 10:55:54.083042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@@ -163,6 +163,15 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "Employee",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_cost_center, is_group",
|
||||
|
||||
@@ -15,9 +15,7 @@ class CostCenter(NestedSet):
|
||||
def autoname(self):
|
||||
from erpnext.accounts.utils import get_autoname_with_number
|
||||
|
||||
self.name = get_autoname_with_number(
|
||||
self.cost_center_number, self.cost_center_name, self.company
|
||||
)
|
||||
self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, self.company)
|
||||
|
||||
def validate(self):
|
||||
self.validate_mandatory()
|
||||
@@ -90,14 +88,14 @@ class CostCenter(NestedSet):
|
||||
new_cost_center = get_name_with_abbr(newdn, self.company)
|
||||
|
||||
# Validate properties before merging
|
||||
super(CostCenter, self).before_rename(olddn, new_cost_center, merge, "is_group")
|
||||
super().before_rename(olddn, new_cost_center, merge, "is_group")
|
||||
if not merge:
|
||||
new_cost_center = get_name_with_number(new_cost_center, self.cost_center_number)
|
||||
|
||||
return new_cost_center
|
||||
|
||||
def after_rename(self, olddn, newdn, merge=False):
|
||||
super(CostCenter, self).after_rename(olddn, newdn, merge)
|
||||
super().after_rename(olddn, newdn, merge)
|
||||
|
||||
if not merge:
|
||||
new_cost_center = frappe.db.get_value(
|
||||
|
||||
@@ -10,7 +10,6 @@ test_records = frappe.get_test_records("Cost Center")
|
||||
|
||||
class TestCostCenter(unittest.TestCase):
|
||||
def test_cost_center_creation_against_child_node(self):
|
||||
|
||||
if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
|
||||
frappe.get_doc(test_records[1]).insert()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class InvalidDateError(frappe.ValidationError):
|
||||
|
||||
class CostCenterAllocation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CostCenterAllocation, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._skip_from_date_validation = False
|
||||
|
||||
def validate(self):
|
||||
@@ -44,9 +44,7 @@ class CostCenterAllocation(Document):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(
|
||||
_("Total percentage against cost centers should be 100"), WrongPercentageAllocation
|
||||
)
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
def validate_from_date_based_on_existing_gle(self):
|
||||
# Check if GLE exists against the main cost center
|
||||
|
||||
@@ -18,7 +18,6 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == "exchangerate.host":
|
||||
|
||||
if not self.access_key:
|
||||
frappe.throw(
|
||||
_("Access Key is required for Service Provider: {0}").format(
|
||||
@@ -53,9 +52,7 @@ class CurrencyExchangeSettings(Document):
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
|
||||
api_url = self.api_endpoint.format(
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
api_url = self.api_endpoint.format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, params=params)
|
||||
@@ -75,14 +72,14 @@ class CurrencyExchangeSettings(Document):
|
||||
]
|
||||
except Exception:
|
||||
frappe.throw(_("Invalid result key. Response:") + " " + response.text)
|
||||
if not isinstance(value, (int, float)):
|
||||
if not isinstance(value, int | float):
|
||||
frappe.throw(_("Returned exchange rate is neither integer not float."))
|
||||
|
||||
self.url = response.url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str = None, use_http: bool = False):
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
|
||||
@@ -246,7 +246,6 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
|
||||
if d.balance != 0:
|
||||
current_exchange_rate = new_exchange_rate = 0
|
||||
|
||||
@@ -259,7 +258,8 @@ class ExchangeRateRevaluation(Document):
|
||||
new_balance_in_account_currency = 0
|
||||
|
||||
current_exchange_rate = (
|
||||
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0
|
||||
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
gain_loss = new_balance_in_account_currency - (
|
||||
@@ -313,9 +313,7 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
revaluation_jv = self.make_jv_for_revaluation()
|
||||
if revaluation_jv:
|
||||
frappe.msgprint(
|
||||
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
|
||||
)
|
||||
frappe.msgprint(f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}")
|
||||
|
||||
return {
|
||||
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
|
||||
@@ -372,7 +370,8 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_account.update(
|
||||
{
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
abs(d.get("balance_in_account_currency")),
|
||||
d.precision("balance_in_account_currency"),
|
||||
),
|
||||
reverse_dr_or_cr: 0,
|
||||
"debit": 0,
|
||||
@@ -498,7 +497,9 @@ class ExchangeRateRevaluation(Document):
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"exchange_rate": flt(
|
||||
d.get("current_exchange_rate"), d.precision("current_exchange_rate")
|
||||
),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
@@ -576,7 +577,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
@@ -589,7 +590,7 @@ def get_account_details(
|
||||
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
|
||||
|
||||
account_details = {}
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
erpnext.get_company_currency(company)
|
||||
|
||||
account_details = {
|
||||
"account_currency": account_currency,
|
||||
@@ -603,9 +604,7 @@ def get_account_details(
|
||||
rounding_loss_allowance=rounding_loss_allowance,
|
||||
)
|
||||
|
||||
if account_balance and (
|
||||
account_balance[0].balance or account_balance[0].balance_in_account_currency
|
||||
):
|
||||
if account_balance and (account_balance[0].balance or account_balance[0].balance_in_account_currency):
|
||||
if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
company, posting_date, account_balance
|
||||
):
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -73,9 +66,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
err.extend("accounts", accounts)
|
||||
row = err.accounts[0]
|
||||
row.new_exchange_rate = 85
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency))
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
@@ -127,9 +118,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
pe.save().submit()
|
||||
|
||||
# Cancel the auto created gain/loss JE to simulate balance only in base currency
|
||||
je = frappe.db.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
|
||||
)[0]
|
||||
je = frappe.db.get_all("Journal Entry Account", filters={"reference_name": si.name}, pluck="parent")[
|
||||
0
|
||||
]
|
||||
frappe.get_doc("Journal Entry", je).cancel()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
@@ -235,9 +226,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(flt(acc.debit, precision), 0.0)
|
||||
self.assertEqual(flt(acc.credit, precision), 0.0)
|
||||
|
||||
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
|
||||
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 = [x for x in je.accounts if x.account != self.debtors_usd][0]
|
||||
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
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
@@ -294,5 +285,5 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
"new_balance_in_account_currency": 100.0,
|
||||
}
|
||||
|
||||
for key, val in expected_data.items():
|
||||
for key, _val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -118,9 +118,21 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Employee"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "name",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ class FiscalYear(Document):
|
||||
|
||||
if overlap:
|
||||
frappe.throw(
|
||||
_("Year start date or end date is overlapping with {0}. To avoid please set company").format(
|
||||
existing.name
|
||||
),
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(existing.name),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
@@ -116,9 +116,9 @@ def check_duplicate_fiscal_year(doc):
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
|
||||
fiscal_year
|
||||
)
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -63,13 +63,18 @@ class GLEntry(Document):
|
||||
]:
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
self.against_voucher_type
|
||||
in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
self.account,
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.against_voucher_type,
|
||||
self.against_voucher,
|
||||
)
|
||||
|
||||
def check_mandatory(self):
|
||||
@@ -135,12 +140,13 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -148,12 +154,13 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
def check_pl_account(self):
|
||||
@@ -201,9 +208,7 @@ class GLEntry(Document):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value(
|
||||
"Cost Center", self.cost_center, ["is_group", "company"]
|
||||
)
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(
|
||||
@@ -272,7 +277,7 @@ def update_outstanding_amt(
|
||||
account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
|
||||
):
|
||||
if party_type and party:
|
||||
party_condition = " and party_type={0} and party={1}".format(
|
||||
party_condition = " and party_type={} and party={}".format(
|
||||
frappe.db.escape(party_type), frappe.db.escape(party)
|
||||
)
|
||||
else:
|
||||
@@ -280,23 +285,19 @@ def update_outstanding_amt(
|
||||
|
||||
if against_voucher_type == "Sales Invoice":
|
||||
party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to")
|
||||
account_condition = "and account in ({0}, {1})".format(
|
||||
frappe.db.escape(account), frappe.db.escape(party_account)
|
||||
)
|
||||
account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
|
||||
else:
|
||||
account_condition = " and account = {0}".format(frappe.db.escape(account))
|
||||
account_condition = f" and account = {frappe.db.escape(account)}"
|
||||
|
||||
# get final outstanding amt
|
||||
bal = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type=%s and against_voucher=%s
|
||||
and voucher_type != 'Invoice Discounting'
|
||||
{0} {1}""".format(
|
||||
party_condition, account_condition
|
||||
),
|
||||
{party_condition} {account_condition}""",
|
||||
(against_voucher_type, against_voucher),
|
||||
)[0][0]
|
||||
or 0.0
|
||||
@@ -307,12 +308,10 @@ def update_outstanding_amt(
|
||||
elif against_voucher_type == "Journal Entry":
|
||||
against_voucher_amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
|
||||
and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
|
||||
party_condition
|
||||
),
|
||||
and account = %s and (against_voucher is null or against_voucher='') {party_condition}""",
|
||||
(against_voucher, account),
|
||||
)[0][0]
|
||||
)
|
||||
@@ -331,7 +330,9 @@ def update_outstanding_amt(
|
||||
# Validation : Outstanding can not be negative for JV
|
||||
if bal < 0 and not on_cancel:
|
||||
frappe.throw(
|
||||
_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
|
||||
_("Outstanding for {0} cannot be less than zero ({1})").format(
|
||||
against_voucher, fmt_money(bal)
|
||||
)
|
||||
)
|
||||
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
@@ -404,7 +405,7 @@ def rename_temporarily_named_docs(doctype):
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@@ -14,9 +14,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
|
||||
class TestGLEntry(unittest.TestCase):
|
||||
def test_round_off_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -73,7 +71,9 @@ class TestGLEntry(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
|
||||
|
||||
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
|
||||
self.assertTrue(
|
||||
all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries, strict=False))
|
||||
)
|
||||
|
||||
new_naming_series_current_value = frappe.db.sql(
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
|
||||
@@ -55,9 +55,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
|
||||
).format(
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
|
||||
)
|
||||
).format(record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))
|
||||
)
|
||||
|
||||
def calculate_total_amount(self):
|
||||
@@ -77,7 +75,9 @@ class InvoiceDiscounting(AccountsController):
|
||||
self.status = status
|
||||
self.db_set("status", status)
|
||||
for d in self.invoices:
|
||||
frappe.get_doc("Sales Invoice", d.sales_invoice).set_status(update=True, update_modified=False)
|
||||
frappe.get_doc("Sales Invoice", d.sales_invoice).set_status(
|
||||
update=True, update_modified=False
|
||||
)
|
||||
else:
|
||||
self.status = "Draft"
|
||||
if self.docstatus == 1:
|
||||
|
||||
@@ -75,8 +75,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
gle = get_gl_entries("Invoice Discounting", inv_disc.name)
|
||||
|
||||
expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
|
||||
for i, gle in enumerate(gle):
|
||||
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
|
||||
for _i, gle_value in enumerate(gle):
|
||||
self.assertEqual([gle_value.debit, gle_value.credit], expected_gle.get(gle_value.account))
|
||||
|
||||
def test_loan_on_submit(self):
|
||||
inv = create_sales_invoice(rate=300)
|
||||
@@ -92,9 +92,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
period=60,
|
||||
)
|
||||
self.assertEqual(inv_disc.status, "Sanctioned")
|
||||
self.assertEqual(
|
||||
inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
|
||||
)
|
||||
self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period))
|
||||
|
||||
def test_on_disbursed(self):
|
||||
inv = create_sales_invoice(rate=500)
|
||||
@@ -262,13 +260,9 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
@@ -304,13 +298,9 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
|
||||
@@ -14,7 +14,7 @@ class ItemTaxTemplate(Document):
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
self.name = "{0} - {1}".format(self.title, abbr)
|
||||
self.name = f"{self.title} - {abbr}"
|
||||
|
||||
def validate_tax_accounts(self):
|
||||
"""Check whether Tax Rate is not entered twice for same Tax Type"""
|
||||
|
||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
!(frm.doc.accounts || []).length ||
|
||||
((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)
|
||||
) {
|
||||
if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) {
|
||||
if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) {
|
||||
return frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
|
||||
@@ -283,7 +283,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
filters: [[jvd.reference_type, "docstatus", "=", 1]],
|
||||
};
|
||||
|
||||
if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) {
|
||||
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
|
||||
// Filter by cost center
|
||||
if (jvd.cost_center) {
|
||||
@@ -295,7 +295,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
|
||||
}
|
||||
|
||||
if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
|
||||
if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) {
|
||||
// party_type and party mandatory
|
||||
frappe.model.validate_missing(jvd, "party_type");
|
||||
frappe.model.validate_missing(jvd, "party");
|
||||
@@ -428,7 +428,10 @@ frappe.ui.form.on("Journal Entry Account", {
|
||||
}
|
||||
},
|
||||
cost_center: function (frm, dt, dn) {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
|
||||
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
|
||||
account: function (frm, dt, dn) {
|
||||
|
||||
@@ -557,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -33,7 +33,7 @@ class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
|
||||
class JournalEntry(AccountsController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JournalEntry, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_feed(self):
|
||||
return self.voucher_type
|
||||
@@ -102,7 +102,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super(JournalEntry, self).on_cancel()
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
@@ -137,10 +137,7 @@ class JournalEntry(AccountsController):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
|
||||
account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
|
||||
@@ -286,10 +283,7 @@ class JournalEntry(AccountsController):
|
||||
asset.set_status()
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
frappe.db.set_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
@@ -317,17 +311,25 @@ class JournalEntry(AccountsController):
|
||||
if d.account == inv_disc_doc.short_term_loan and d.reference_name == inv_disc:
|
||||
if self.docstatus == 1:
|
||||
if d.credit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Sanctioned", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Sanctioned", d.idx
|
||||
)
|
||||
status = "Disbursed"
|
||||
elif d.debit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Disbursed", d.idx
|
||||
)
|
||||
status = "Settled"
|
||||
else:
|
||||
if d.credit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Disbursed", d.idx
|
||||
)
|
||||
status = "Sanctioned"
|
||||
elif d.debit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Settled", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Settled", d.idx
|
||||
)
|
||||
status = "Disbursed"
|
||||
break
|
||||
if status:
|
||||
@@ -384,10 +386,7 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
frappe.db.set_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
@@ -409,9 +408,9 @@ class JournalEntry(AccountsController):
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party):
|
||||
frappe.throw(
|
||||
_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(
|
||||
d.idx, d.account
|
||||
)
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
).format(d.idx, d.account)
|
||||
)
|
||||
elif (
|
||||
d.party_type
|
||||
@@ -476,16 +475,18 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def system_generated_gain_loss(self):
|
||||
return (
|
||||
self.voucher_type == "Exchange Gain Or Loss"
|
||||
and self.multi_currency
|
||||
and self.is_system_generated
|
||||
self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency and self.is_system_generated
|
||||
)
|
||||
|
||||
def validate_against_jv(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.reference_type == "Journal Entry":
|
||||
account_root_type = frappe.get_cached_value("Account", d.account, "root_type")
|
||||
if account_root_type == "Asset" and flt(d.debit) > 0 and not self.system_generated_gain_loss():
|
||||
if (
|
||||
account_root_type == "Asset"
|
||||
and flt(d.debit) > 0
|
||||
and not self.system_generated_gain_loss()
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: For {1}, you can select reference document only if account gets credited"
|
||||
@@ -567,11 +568,13 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if d.reference_type == "Purchase Order" and flt(d.credit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type)
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(
|
||||
d.idx, d.reference_type
|
||||
)
|
||||
)
|
||||
|
||||
# set totals
|
||||
if not d.reference_name in self.reference_totals:
|
||||
if d.reference_name not in self.reference_totals:
|
||||
self.reference_totals[d.reference_name] = 0.0
|
||||
|
||||
if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
|
||||
@@ -589,7 +592,10 @@ class JournalEntry(AccountsController):
|
||||
|
||||
# check if party and account match
|
||||
if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no:
|
||||
if (
|
||||
self.voucher_type in ("Deferred Revenue", "Deferred Expense")
|
||||
and d.reference_detail_no
|
||||
):
|
||||
debit_or_credit = "Debit" if d.debit else "Credit"
|
||||
party_account = get_deferred_booking_accounts(
|
||||
d.reference_type, d.reference_detail_no, debit_or_credit
|
||||
@@ -598,7 +604,8 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or against_voucher[1]
|
||||
)
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
@@ -722,7 +729,9 @@ class JournalEntry(AccountsController):
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
if self.difference:
|
||||
frappe.throw(
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(
|
||||
self.difference
|
||||
)
|
||||
)
|
||||
|
||||
def set_total_debit_credit(self):
|
||||
@@ -786,7 +795,6 @@ class JournalEntry(AccountsController):
|
||||
and self.posting_date
|
||||
)
|
||||
):
|
||||
|
||||
ignore_exchange_rate = False
|
||||
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
|
||||
ignore_exchange_rate = True
|
||||
@@ -1032,27 +1040,21 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = (
|
||||
" and outstanding_amount <= {0}".format(self.write_off_amount)
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
"""select name, debit_to as account, customer as party, outstanding_amount
|
||||
from `tabSales Invoice` where docstatus = 1 and company = %s
|
||||
and outstanding_amount > 0 %s"""
|
||||
% ("%s", cond),
|
||||
from `tabSales Invoice` where docstatus = 1 and company = {}
|
||||
and outstanding_amount > 0 {}""".format("%s", cond),
|
||||
self.company,
|
||||
as_dict=True,
|
||||
)
|
||||
elif self.write_off_based_on == "Accounts Payable":
|
||||
return frappe.db.sql(
|
||||
"""select name, credit_to as account, supplier as party, outstanding_amount
|
||||
from `tabPurchase Invoice` where docstatus = 1 and company = %s
|
||||
and outstanding_amount > 0 %s"""
|
||||
% ("%s", cond),
|
||||
from `tabPurchase Invoice` where docstatus = 1 and company = {}
|
||||
and outstanding_amount > 0 {}""".format("%s", cond),
|
||||
self.company,
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -1161,7 +1163,7 @@ def get_payment_entry_against_order(
|
||||
"amount_field_bank": amount_field_bank,
|
||||
"amount": amount,
|
||||
"debit_in_account_currency": debit_in_account_currency,
|
||||
"remarks": "Advance Payment received against {0} {1}".format(dt, dn),
|
||||
"remarks": f"Advance Payment received against {dt} {dn}",
|
||||
"is_advance": "Yes",
|
||||
"bank_account": bank_account,
|
||||
"journal_entry": journal_entry,
|
||||
@@ -1200,7 +1202,7 @@ def get_payment_entry_against_invoice(
|
||||
"amount_field_bank": amount_field_bank,
|
||||
"amount": amount if amount else abs(ref_doc.outstanding_amount),
|
||||
"debit_in_account_currency": debit_in_account_currency,
|
||||
"remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks),
|
||||
"remarks": f"Payment received against {dt} {dn}. {ref_doc.remarks}",
|
||||
"is_advance": "No",
|
||||
"bank_account": bank_account,
|
||||
"journal_entry": journal_entry,
|
||||
@@ -1226,9 +1228,7 @@ def get_payment_entry(ref_doc, args):
|
||||
)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.update(
|
||||
{"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}
|
||||
)
|
||||
je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")})
|
||||
|
||||
party_row = je.append(
|
||||
"accounts",
|
||||
@@ -1251,9 +1251,7 @@ def get_payment_entry(ref_doc, args):
|
||||
bank_row = je.append("accounts")
|
||||
|
||||
# Make it bank_details
|
||||
bank_account = get_default_bank_cash_account(
|
||||
ref_doc.company, "Bank", account=args.get("bank_account")
|
||||
)
|
||||
bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account"))
|
||||
if bank_account:
|
||||
bank_row.update(bank_account)
|
||||
# Modified to include the posting date for which the exchange rate is required.
|
||||
@@ -1293,7 +1291,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
return []
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT jv.name, jv.posting_date, jv.user_remark
|
||||
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
|
||||
WHERE jv_detail.parent = jv.name
|
||||
@@ -1304,16 +1302,14 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
OR jv_detail.reference_type = ''
|
||||
)
|
||||
AND jv.docstatus = 1
|
||||
AND jv.`{0}` LIKE %(txt)s
|
||||
AND jv.`{searchfield}` LIKE %(txt)s
|
||||
ORDER BY jv.name DESC
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""".format(
|
||||
searchfield
|
||||
),
|
||||
""",
|
||||
dict(
|
||||
account=filters.get("account"),
|
||||
party=cstr(filters.get("party")),
|
||||
txt="%{0}%".format(txt),
|
||||
txt=f"%{txt}%",
|
||||
offset=start,
|
||||
limit=page_len,
|
||||
),
|
||||
@@ -1335,19 +1331,15 @@ def get_outstanding(args):
|
||||
condition = " and party=%(party)s" if args.get("party") else ""
|
||||
|
||||
against_jv_amount = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0}
|
||||
and (reference_type is null or reference_type = '')""".format(
|
||||
condition
|
||||
),
|
||||
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {condition}
|
||||
and (reference_type is null or reference_type = '')""",
|
||||
args,
|
||||
)
|
||||
|
||||
against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0
|
||||
amount_field = (
|
||||
"credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
|
||||
)
|
||||
amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
|
||||
return {amount_field: abs(against_jv_amount)}
|
||||
elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"):
|
||||
party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier"
|
||||
@@ -1360,9 +1352,7 @@ def get_outstanding(args):
|
||||
|
||||
due_date = invoice.get("due_date")
|
||||
|
||||
exchange_rate = (
|
||||
invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
|
||||
)
|
||||
exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
|
||||
|
||||
if args["doctype"] == "Sales Invoice":
|
||||
amount_field = (
|
||||
@@ -1400,17 +1390,13 @@ def get_party_account_and_currency(company, party_type, party):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details_and_party_type(
|
||||
account, date, company, debit=None, credit=None, exchange_rate=None
|
||||
):
|
||||
def get_account_details_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None):
|
||||
"""Returns dict of account details and party type to be set in Journal Entry on selection of account."""
|
||||
if not frappe.has_permission("Account"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
account_details = frappe.db.get_value(
|
||||
"Account", account, ["account_type", "account_currency"], as_dict=1
|
||||
)
|
||||
account_details = frappe.db.get_value("Account", account, ["account_type", "account_currency"], as_dict=1)
|
||||
|
||||
if not account_details:
|
||||
return
|
||||
|
||||
@@ -69,10 +69,8 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.sql(
|
||||
"""select name from `tabJournal Entry Account`
|
||||
where reference_type = %s and reference_name = %s and {0}=400""".format(
|
||||
dr_or_cr
|
||||
),
|
||||
f"""select name from `tabJournal Entry Account`
|
||||
where reference_type = %s and reference_name = %s and {dr_or_cr}=400""",
|
||||
(submitted_voucher.doctype, submitted_voucher.name),
|
||||
)
|
||||
)
|
||||
@@ -84,9 +82,8 @@ class TestJournalEntry(unittest.TestCase):
|
||||
def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr):
|
||||
# Test advance paid field
|
||||
advance_paid = frappe.db.sql(
|
||||
"""select advance_paid from `tab%s`
|
||||
where name=%s"""
|
||||
% (test_voucher.doctype, "%s"),
|
||||
"""select advance_paid from `tab{}`
|
||||
where name={}""".format(test_voucher.doctype, "%s"),
|
||||
(test_voucher.name),
|
||||
)
|
||||
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
|
||||
@@ -159,9 +156,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.cancel()
|
||||
|
||||
def test_multi_currency(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False
|
||||
)
|
||||
jv = make_journal_entry("_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False)
|
||||
|
||||
jv.get("accounts")[1].credit_in_account_currency = 5000
|
||||
jv.submit()
|
||||
@@ -201,7 +196,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
"credit",
|
||||
"credit_in_account_currency",
|
||||
):
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account][field], gle[field])
|
||||
|
||||
# cancel
|
||||
@@ -263,7 +258,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
"credit",
|
||||
"credit_in_account_currency",
|
||||
):
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account][field], gle[field])
|
||||
|
||||
def test_disallow_change_in_account_currency_for_a_party(self):
|
||||
|
||||
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2024-03-26 17:01:47.443986",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"checked_on",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit mismatch"
|
||||
},
|
||||
{
|
||||
"fieldname": "checked_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Checked On"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "General and Payment Ledger mismatch"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-09 11:16:07.044484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# 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 LedgerHealth(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
|
||||
|
||||
checked_on: DF.Datetime | None
|
||||
debit_credit_mismatch: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
name: DF.Int | None
|
||||
voucher_no: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import run_ledger_health_checks
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
monitor_settings.enable_health_monitor = True
|
||||
monitor_settings.enable_for_last_x_days = 60
|
||||
monitor_settings.debit_credit_mismatch = True
|
||||
monitor_settings.general_and_payment_ledger_mismatch = True
|
||||
exists = [x for x in monitor_settings.companies if x.company == self.company]
|
||||
if not exists:
|
||||
monitor_settings.append("companies", {"company": self.company})
|
||||
monitor_settings.save()
|
||||
|
||||
def clear_old_entries(self):
|
||||
super().clear_old_entries()
|
||||
lh = qb.DocType("Ledger Health")
|
||||
qb.from_(lh).delete().run()
|
||||
|
||||
def create_journal(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.posting_date = nowdate()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 10000,
|
||||
},
|
||||
)
|
||||
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
|
||||
je.save().submit()
|
||||
self.je = je
|
||||
|
||||
def test_debit_credit_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause debit-credit mismatch
|
||||
gle = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
|
||||
)[0]
|
||||
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": True,
|
||||
"general_and_payment_ledger_mismatch": False,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
|
||||
def test_gl_and_pl_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause GL and PL discrepancy
|
||||
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": False,
|
||||
"general_and_payment_ledger_mismatch": True,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health Monitor", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 09:38:07.427997",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_health_monitor",
|
||||
"monitor_section",
|
||||
"monitor_for_last_x_days",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
"section_break_xdsp",
|
||||
"companies"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_health_monitor",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Health Monitor"
|
||||
},
|
||||
{
|
||||
"fieldname": "monitor_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit Mismatch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Discrepancy between General and Payment Ledger"
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"fieldname": "monitor_for_last_x_days",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Monitor for Last 'X' days",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xdsp",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Companies"
|
||||
},
|
||||
{
|
||||
"fieldname": "companies",
|
||||
"fieldtype": "Table",
|
||||
"options": "Ledger Health Monitor Company"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:14:16.511681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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 LedgerHealthMonitor(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
|
||||
LedgerHealthMonitorCompany,
|
||||
)
|
||||
|
||||
companies: DF.Table[LedgerHealthMonitorCompany]
|
||||
debit_credit_mismatch: DF.Check
|
||||
enable_health_monitor: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
monitor_for_last_x_days: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLedgerHealthMonitor(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 10:04:45.727054",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:06:22.806155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 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 LedgerHealthMonitorCompany(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
|
||||
|
||||
company: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -83,7 +83,10 @@ class TestLedgerMerge(unittest.TestCase):
|
||||
"account": "Indirect Income - _TC",
|
||||
"merge_accounts": [
|
||||
{"account": "Indirect Test Income - _TC", "account_name": "Indirect Test Income"},
|
||||
{"account": "Administrative Test Income - _TC", "account_name": "Administrative Test Income"},
|
||||
{
|
||||
"account": "Administrative Test Income - _TC",
|
||||
"account_name": "Administrative Test Income",
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
@@ -25,13 +25,11 @@ def get_loyalty_details(
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
"""select sum(loyalty_points) as loyalty_points,
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""".format(
|
||||
condition=condition
|
||||
),
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -52,9 +50,7 @@ def get_loyalty_program_details_with_points(
|
||||
include_expired_entry=False,
|
||||
current_transaction_amount=0,
|
||||
):
|
||||
lp_details = get_loyalty_program_details(
|
||||
customer, loyalty_program, company=company, silent=silent
|
||||
)
|
||||
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
|
||||
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
|
||||
lp_details.update(
|
||||
get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
|
||||
|
||||
@@ -19,9 +19,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
create_records()
|
||||
|
||||
def test_loyalty_points_earned_single_tier(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si_original = create_sales_invoice_record()
|
||||
si_original.insert()
|
||||
@@ -69,9 +67,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
d.cancel()
|
||||
|
||||
def test_loyalty_points_earned_multiple_tier(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty")
|
||||
# assign multiple tier program to the customer
|
||||
customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
|
||||
customer.loyalty_program = frappe.get_doc(
|
||||
@@ -128,9 +124,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
|
||||
def test_cancel_sales_invoice(self):
|
||||
"""cancelling the sales invoice should cancel the earned points"""
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si = create_sales_invoice_record()
|
||||
si.insert()
|
||||
@@ -140,7 +134,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
"Loyalty Point Entry",
|
||||
{"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
|
||||
)
|
||||
self.assertEqual(True, not (lpe is None))
|
||||
self.assertEqual(True, lpe is not None)
|
||||
|
||||
# cancelling sales invoice
|
||||
si.cancel()
|
||||
@@ -148,9 +142,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
self.assertEqual(True, (lpe is None))
|
||||
|
||||
def test_sales_invoice_return(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si_original = create_sales_invoice_record(2)
|
||||
si_original.conversion_rate = flt(1)
|
||||
@@ -346,9 +338,7 @@ def create_records():
|
||||
).insert()
|
||||
|
||||
# create item price
|
||||
if not frappe.db.exists(
|
||||
"Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}
|
||||
):
|
||||
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
|
||||
@@ -37,9 +37,7 @@ class MonthlyDistribution(Document):
|
||||
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
|
||||
|
||||
if flt(total, 2) != 100.0:
|
||||
frappe.throw(
|
||||
_("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2)))
|
||||
)
|
||||
frappe.throw(_("Percentage Allocation should be equal to 100%") + f" ({flt(total, 2)!s}%)")
|
||||
|
||||
|
||||
def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
|
||||
|
||||
@@ -83,9 +83,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
|
||||
old_default_receivable_account = frappe.db.get_value(
|
||||
"Company", company, "default_receivable_account"
|
||||
)
|
||||
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", "")
|
||||
|
||||
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
|
||||
@@ -121,9 +119,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
self.assertTrue(error_log)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value(
|
||||
"Company", company, "default_receivable_account", old_default_receivable_account
|
||||
)
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
@@ -169,7 +165,7 @@ def get_opening_invoice_creation_dict(**args):
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 300,
|
||||
"party": args.get("party_1") or "_Test {0}".format(party),
|
||||
"party": args.get("party_1") or f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
@@ -179,7 +175,7 @@ def get_opening_invoice_creation_dict(**args):
|
||||
{
|
||||
"qty": 2.0,
|
||||
"outstanding_amount": 250,
|
||||
"party": args.get("party_2") or "_Test {0} 1".format(party),
|
||||
"party": args.get("party_2") or f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
|
||||
@@ -24,7 +24,10 @@ class PartyLink(Document):
|
||||
if existing_party_link:
|
||||
frappe.throw(
|
||||
_("{} {} is already linked with {} {}").format(
|
||||
self.primary_role, bold(self.primary_party), self.secondary_role, bold(self.secondary_party)
|
||||
self.primary_role,
|
||||
bold(self.primary_party),
|
||||
self.secondary_role,
|
||||
bold(self.secondary_party),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_from", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
filters: {
|
||||
@@ -75,7 +75,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_to", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
filters: {
|
||||
@@ -121,7 +121,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
if (['Purchase Invoice', 'Sales Invoice'].includes(child.reference_doctype) && child.reference_name) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
@@ -145,10 +145,27 @@ frappe.ui.form.on('Payment Entry', {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
erpnext.hide_company();
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
@@ -179,7 +196,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
hide_unhide_fields: function(frm) {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
|
||||
|
||||
frm.toggle_display("source_exchange_rate",
|
||||
(frm.doc.paid_amount && frm.doc.paid_from_account_currency != company_currency));
|
||||
@@ -485,7 +502,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
@@ -853,7 +870,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
|
||||
if(paid_amount > total_negative_outstanding) {
|
||||
if(total_negative_outstanding == 0) {
|
||||
@@ -988,7 +1005,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Customer" &&
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||
!["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
|
||||
@@ -996,7 +1013,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Supplier" &&
|
||||
!in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype)
|
||||
!["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry", [row.idx]));
|
||||
@@ -1080,7 +1097,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
bank_account: function(frm) {
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from":"paid_to";
|
||||
if (frm.doc.bank_account && in_list(['Pay', 'Receive'], frm.doc.payment_type)) {
|
||||
if (frm.doc.bank_account && ['Pay', 'Receive'].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details",
|
||||
args: {
|
||||
@@ -1088,7 +1105,9 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.set_value(field, r.message.account);
|
||||
if (!frm.doc.mode_of_payment) {
|
||||
frm.set_value(field, r.message.account);
|
||||
}
|
||||
frm.set_value('bank', r.message.bank);
|
||||
frm.set_value('bank_account_no', r.message.bank_account_no);
|
||||
}
|
||||
@@ -1393,8 +1412,9 @@ frappe.ui.form.on('Payment Entry Reference', {
|
||||
args: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
party_account_currency: frm.doc.payment_type=="Receive" ?
|
||||
frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency
|
||||
party_account_currency: frm.doc.payment_type == "Receive" ? frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
|
||||
@@ -576,6 +576,7 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Payment Order Status",
|
||||
"no_copy": 1,
|
||||
"options": "Initiated\nPayment Ordered",
|
||||
"read_only": 1
|
||||
},
|
||||
|
||||
@@ -9,8 +9,7 @@ import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
from pypika.functions import Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -47,7 +46,7 @@ class InvalidPaymentEntry(ValidationError):
|
||||
|
||||
class PaymentEntry(AccountsController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentEntry, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.is_new():
|
||||
self.setup_party_account_field()
|
||||
|
||||
@@ -75,7 +74,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
@@ -90,6 +88,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
@@ -113,7 +112,7 @@ class PaymentEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
@@ -221,9 +220,7 @@ class PaymentEntry(AccountsController):
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
@@ -235,7 +232,9 @@ class PaymentEntry(AccountsController):
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
_("{0} {1} has already been fully paid.").format(
|
||||
_(d.reference_doctype), d.reference_name
|
||||
)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
@@ -259,14 +258,14 @@ class PaymentEntry(AccountsController):
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
@@ -338,22 +337,30 @@ class PaymentEntry(AccountsController):
|
||||
self,
|
||||
force: bool = False,
|
||||
update_ref_details_only_for: list | None = None,
|
||||
ref_exchange_rate: float | None = None,
|
||||
reference_exchange_details: dict | None = None,
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
if update_ref_details_only_for and (
|
||||
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
|
||||
(d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
||||
):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
|
||||
ref_details.update({"exchange_rate": ref_exchange_rate})
|
||||
if (
|
||||
reference_exchange_details
|
||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||
and d.reference_name == reference_exchange_details.reference_name
|
||||
):
|
||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
@@ -386,7 +393,9 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
if ref_doc:
|
||||
if self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get(
|
||||
"conversion_rate"
|
||||
)
|
||||
|
||||
if not self.source_exchange_rate:
|
||||
self.source_exchange_rate = get_exchange_rate(
|
||||
@@ -423,7 +432,7 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or((_(d) for d in valid_reference_doctypes))
|
||||
comma_or(_(d) for d in valid_reference_doctypes)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -446,7 +455,8 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
||||
if self.party_type == "Customer":
|
||||
ref_party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or ref_doc.debit_to
|
||||
)
|
||||
elif self.party_type == "Supplier":
|
||||
ref_party_account = ref_doc.credit_to
|
||||
@@ -456,7 +466,10 @@ class PaymentEntry(AccountsController):
|
||||
if ref_party_account != self.party_account:
|
||||
frappe.throw(
|
||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||
_(d.reference_doctype), d.reference_name, ref_party_account, self.party_account
|
||||
_(d.reference_doctype),
|
||||
d.reference_name,
|
||||
ref_party_account,
|
||||
self.party_account,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -467,7 +480,9 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
frappe.throw(_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name))
|
||||
frappe.throw(
|
||||
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
def get_valid_reference_doctypes(self):
|
||||
if self.party_type == "Customer":
|
||||
@@ -638,9 +653,7 @@ class PaymentEntry(AccountsController):
|
||||
if not (is_single_currency and reference_is_multi_currency):
|
||||
return allocated_amount
|
||||
|
||||
allocated_amount = flt(
|
||||
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||
)
|
||||
allocated_amount = flt(allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount"))
|
||||
|
||||
return allocated_amount
|
||||
|
||||
@@ -661,9 +674,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -688,7 +699,6 @@ class PaymentEntry(AccountsController):
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
|
||||
# Preserve user updated included in paid amount
|
||||
if d.included_in_paid_amount:
|
||||
tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
|
||||
@@ -708,7 +718,26 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
@@ -716,12 +745,15 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -808,7 +840,6 @@ class PaymentEntry(AccountsController):
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
|
||||
# Use source/target exchange rate, so no difference amount is calculated.
|
||||
# then update exchange gain/loss amount in reference table
|
||||
# if there is an exchange gain/loss amount in reference table, submit a JE for that
|
||||
@@ -925,7 +956,9 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
total_negative_outstanding = flt(
|
||||
sum(
|
||||
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
|
||||
abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references")
|
||||
if flt(d.outstanding_amount) < 0
|
||||
),
|
||||
self.references[0].precision("outstanding_amount") if self.references else None,
|
||||
)
|
||||
@@ -978,7 +1011,6 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
]
|
||||
else:
|
||||
|
||||
remarks = [
|
||||
_("Amount {0} {1} {2} {3}").format(
|
||||
self.party_account_currency,
|
||||
@@ -998,14 +1030,19 @@ class PaymentEntry(AccountsController):
|
||||
if d.allocated_amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} against {2} {3}").format(
|
||||
self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
|
||||
self.party_account_currency,
|
||||
d.allocated_amount,
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
)
|
||||
)
|
||||
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
|
||||
_("Amount {0} {1} deducted against {2}").format(
|
||||
self.company_currency, d.amount, d.account
|
||||
)
|
||||
)
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
@@ -1440,7 +1477,8 @@ def get_outstanding_reference_documents(args):
|
||||
return []
|
||||
elif supplier_status["hold_type"] == "Payments":
|
||||
if (
|
||||
not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"]
|
||||
not supplier_status["release_date"]
|
||||
or getdate(nowdate()) <= supplier_status["release_date"]
|
||||
):
|
||||
return []
|
||||
|
||||
@@ -1450,7 +1488,7 @@ def get_outstanding_reference_documents(args):
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = " and voucher_type={0} and voucher_no={1}".format(
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
@@ -1465,7 +1503,7 @@ def get_outstanding_reference_documents(args):
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -1475,21 +1513,21 @@ def get_outstanding_reference_documents(args):
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
|
||||
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
condition += " and company = {}".format(frappe.db.escape(args.get("company")))
|
||||
common_filter.append(ple.company == args.get("company"))
|
||||
|
||||
outstanding_invoices = []
|
||||
@@ -1561,9 +1599,7 @@ def get_outstanding_reference_documents(args):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||
).format(
|
||||
_(ref_document_type), _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
|
||||
)
|
||||
).format(_(ref_document_type), _(args.get("party_type")).lower(), frappe.bold(args.get("party")))
|
||||
)
|
||||
|
||||
return data
|
||||
@@ -1599,12 +1635,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
@@ -1699,7 +1733,7 @@ def get_orders_to_be_billed(
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
@@ -1848,18 +1882,14 @@ def get_account_details(account, date, cost_center=None):
|
||||
frappe.has_permission("Payment Entry", throw=True)
|
||||
|
||||
# to check if the passed account is accessible under reference doctype Payment Entry
|
||||
account_list = frappe.get_list(
|
||||
"Account", {"name": account}, reference_doctype="Payment Entry", limit=1
|
||||
)
|
||||
account_list = frappe.get_list("Account", {"name": account}, reference_doctype="Payment Entry", limit=1)
|
||||
|
||||
# There might be some user permissions which will allow account under certain doctypes
|
||||
# except for Payment Entry, only in such case we should throw permission error
|
||||
if not account_list:
|
||||
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
|
||||
|
||||
account_balance = get_balance_on(
|
||||
account, date, cost_center=cost_center, ignore_account_permission=True
|
||||
)
|
||||
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -1876,53 +1906,59 @@ def get_company_defaults(company):
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
res = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
Case()
|
||||
.when(
|
||||
gl.party_type == "Customer",
|
||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
||||
)
|
||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
||||
.as_("outstanding_amount")
|
||||
)
|
||||
def get_outstanding_on_journal_entry(voucher_no, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
|
||||
outstanding = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(Coalesce(gl.party_type, "") != "")
|
||||
& (gl.is_cancelled == 0)
|
||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
||||
(ple.against_voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
).run()
|
||||
|
||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
||||
outstanding_amount = outstanding[0][0] if outstanding else 0
|
||||
|
||||
return outstanding_amount
|
||||
total = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(ple.voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run()
|
||||
|
||||
total_amount = total[0][0] if total else 0
|
||||
|
||||
return outstanding_amount, total_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reference_details(reference_doctype, reference_name, party_account_currency):
|
||||
def get_reference_details(
|
||||
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(
|
||||
ref_doc.company
|
||||
)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
if reference_doctype == "Dunning":
|
||||
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
if ref_doc.multi_currency:
|
||||
exchange_rate = get_exchange_rate(
|
||||
party_account_currency, company_currency, ref_doc.posting_date
|
||||
)
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype != "Journal Entry":
|
||||
if not total_amount:
|
||||
@@ -1977,9 +2013,7 @@ def get_payment_entry(
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
100.0 + over_billing_allowance
|
||||
):
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
|
||||
if not party_type:
|
||||
@@ -2032,9 +2066,7 @@ def get_payment_entry(
|
||||
pe.paid_from_account_currency = (
|
||||
party_account_currency if payment_type == "Receive" else bank.account_currency
|
||||
)
|
||||
pe.paid_to_account_currency = (
|
||||
party_account_currency if payment_type == "Pay" else bank.account_currency
|
||||
)
|
||||
pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
@@ -2063,7 +2095,6 @@ def get_payment_entry(
|
||||
{"name": doc.payment_terms_template},
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
):
|
||||
|
||||
for reference in get_reference_as_per_payment_terms(
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
@@ -2184,9 +2215,9 @@ def set_party_account_currency(dt, party_account, doc):
|
||||
|
||||
|
||||
def set_payment_type(dt, doc):
|
||||
if (
|
||||
dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)
|
||||
) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
|
||||
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)) or (
|
||||
dt == "Purchase Invoice" and doc.outstanding_amount < 0
|
||||
):
|
||||
payment_type = "Receive"
|
||||
else:
|
||||
payment_type = "Pay"
|
||||
@@ -2246,9 +2277,7 @@ def set_paid_amount_and_received_amount(
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
def apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
):
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency, reference_date):
|
||||
total_discount = 0
|
||||
valid_discounts = []
|
||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
@@ -2258,7 +2287,6 @@ def apply_early_payment_discount(
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||
@@ -2287,9 +2315,7 @@ def apply_early_payment_discount(
|
||||
return paid_amount, received_amount, total_discount, valid_discounts
|
||||
|
||||
|
||||
def set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
):
|
||||
def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss, party_account_currency):
|
||||
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||
if party_account_currency != doc.company_currency:
|
||||
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||
@@ -2309,7 +2335,8 @@ def set_pending_discount_loss(
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
)
|
||||
@@ -2332,9 +2359,7 @@ def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||
"""Get total percentage and amount discount applied as a percentage."""
|
||||
total_discount_percent = (
|
||||
sum(
|
||||
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||
)
|
||||
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
@@ -2377,9 +2402,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||
total_discount_percentage / 100
|
||||
)
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (total_discount_percentage / 100)
|
||||
|
||||
account = tax.get("account_head")
|
||||
if not tax_discount_loss.get(account):
|
||||
@@ -2396,7 +2419,8 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"deductions",
|
||||
{
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
},
|
||||
)
|
||||
@@ -2419,7 +2443,8 @@ def get_reference_as_per_payment_terms(
|
||||
if not is_multi_currency_acc:
|
||||
# If accounting is done in company currency for multi-currency transaction
|
||||
payment_term_outstanding = flt(
|
||||
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||
payment_term_outstanding * doc.get("conversion_rate"),
|
||||
payment_term.precision("payment_amount"),
|
||||
)
|
||||
|
||||
if payment_term_outstanding:
|
||||
@@ -2447,7 +2472,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
|
||||
paid_amount = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select ifnull(sum({dr_or_cr}), 0) as paid_amount
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type = %s
|
||||
@@ -2457,9 +2482,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
and account = %s
|
||||
and due_date = %s
|
||||
and {dr_or_cr} > 0
|
||||
""".format(
|
||||
dr_or_cr=dr_or_cr
|
||||
),
|
||||
""",
|
||||
(dt, dn, party_type, party, account, due_date),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
@@ -163,7 +162,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
supplier.on_hold = 0
|
||||
supplier.save()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
raise Exception
|
||||
@@ -470,9 +469,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||
)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
@@ -639,9 +636,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(
|
||||
pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
|
||||
)
|
||||
self.assertEqual(pe.source_exchange_rate, 65.1, f"{pe.source_exchange_rate} is not equal to {65.1}")
|
||||
|
||||
def test_internal_transfer_usd_to_inr(self):
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
@@ -910,9 +905,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
cost_center = "_Test Cost Center for BS Account - _TC"
|
||||
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
||||
|
||||
pi = make_purchase_invoice_against_cost_center(
|
||||
cost_center=cost_center, credit_to="Creditors - _TC"
|
||||
)
|
||||
pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC")
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
self.assertEqual(pe.cost_center, pi.cost_center)
|
||||
@@ -953,9 +946,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
|
||||
|
||||
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center)
|
||||
party_balance = get_balance_on(
|
||||
party_type="Customer", party=si.customer, cost_center=si.cost_center
|
||||
)
|
||||
party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center)
|
||||
party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
@@ -1096,7 +1087,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
ref_details = get_reference_details(
|
||||
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||
)
|
||||
expected_response = {
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
@@ -1214,7 +1207,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
@@ -1273,9 +1266,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si = create_sales_invoice(qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4))
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
@@ -1401,12 +1392,11 @@ def create_payment_entry(**args):
|
||||
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
create_payment_term("Basic Amount Receivable")
|
||||
create_payment_term("Tax Receivable")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"):
|
||||
payment_term_template = frappe.get_doc(
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "Test Receivable Template",
|
||||
|
||||
@@ -108,9 +108,9 @@ class PaymentLedgerEntry(Document):
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -121,9 +121,9 @@ class PaymentLedgerEntry(Document):
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -84,11 +84,14 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -112,10 +115,12 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate()):
|
||||
def create_payment_entry(self, amount=100, posting_date=None):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
@@ -128,9 +133,10 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def create_sales_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
def create_sales_order(self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False):
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
transaction_date=posting_date,
|
||||
@@ -159,9 +165,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
@@ -319,9 +323,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||
)
|
||||
.where((ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
@@ -362,9 +364,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
)
|
||||
cr_note2.is_return = 1
|
||||
cr_note2 = cr_note2.save().submit()
|
||||
je1 = self.create_journal_entry(
|
||||
self.debit_to, self.debit_to, amount, posting_date=transaction_date
|
||||
)
|
||||
je1 = self.create_journal_entry(self.debit_to, self.debit_to, amount, posting_date=transaction_date)
|
||||
je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
|
||||
je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
|
||||
je1.get("accounts")[0].reference_type = cr_note2.doctype
|
||||
@@ -419,9 +419,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
|
||||
)
|
||||
.where((ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
@@ -518,7 +516,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
amount = 100
|
||||
so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit()
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name).save().submit()
|
||||
get_payment_entry(so.doctype, so.name).save().submit()
|
||||
|
||||
so.reload()
|
||||
so.cancel()
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
|
||||
// payment Entry
|
||||
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") {
|
||||
frm.add_custom_button(__("Create Payment Entries"), function () {
|
||||
frm.add_custom_button(__("Create Journal Entries"), function () {
|
||||
frm.trigger("make_payment_records");
|
||||
});
|
||||
}
|
||||
@@ -71,6 +71,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
target: frm,
|
||||
date_field: "posting_date",
|
||||
setters: {
|
||||
party_type: "Supplier",
|
||||
party: frm.doc.supplier || "",
|
||||
},
|
||||
get_query_filters: {
|
||||
@@ -91,6 +92,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
source_doctype: "Payment Request",
|
||||
target: frm,
|
||||
setters: {
|
||||
party_type: "Supplier",
|
||||
party: frm.doc.supplier || "",
|
||||
},
|
||||
get_query_filters: {
|
||||
|
||||
@@ -66,9 +66,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.payment_order = doc.name
|
||||
je.posting_date = nowdate()
|
||||
mode_of_payment_type = frappe._dict(
|
||||
frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)
|
||||
)
|
||||
mode_of_payment_type = frappe._dict(frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1))
|
||||
|
||||
je.voucher_type = "Bank Entry"
|
||||
if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash":
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -41,9 +40,7 @@ class TestPaymentOrder(FrappeTestCase):
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
|
||||
doc = create_payment_order_against_payment_entry(
|
||||
payment_entry, "Payment Entry", self.bank_account
|
||||
)
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry", self.bank_account)
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
|
||||
@@ -25,7 +25,7 @@ from erpnext.controllers.accounts_controller import get_advance_payment_entries_
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
@@ -200,6 +200,7 @@ class PaymentReconciliation(Document):
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
conditions.append(doc.outstanding_amount != 0)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
@@ -219,7 +220,6 @@ class PaymentReconciliation(Document):
|
||||
self.return_invoices = self.return_invoices_query.run(as_dict=True)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -340,9 +340,7 @@ class PaymentReconciliation(Document):
|
||||
payment_entry[0].get("reference_name")
|
||||
)
|
||||
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
new_difference_amount = self.get_difference_amount(payment_entry[0], invoice[0], allocated_amount)
|
||||
return new_difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -460,9 +458,9 @@ class PaymentReconciliation(Document):
|
||||
|
||||
if running_doc:
|
||||
frappe.throw(
|
||||
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
|
||||
get_link_to_form("Auto Reconcile", running_doc)
|
||||
)
|
||||
_(
|
||||
"A Reconciliation Job {0} is running for the same filters. Cannot reconcile now"
|
||||
).format(get_link_to_form("Auto Reconcile", running_doc))
|
||||
)
|
||||
return
|
||||
|
||||
@@ -555,9 +553,7 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals = [d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
@@ -677,7 +673,7 @@ class PaymentReconciliation(Document):
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
if self.from_payment_date:
|
||||
@@ -797,7 +793,7 @@ def adjust_allocations_for_taxes(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_queries_for_dimension_filters(company: str = None):
|
||||
def get_queries_for_dimension_filters(company: str | None = None):
|
||||
dimensions_with_filters = []
|
||||
for d in get_dimensions()[0]:
|
||||
filters = {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
@@ -127,11 +126,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
setattr(self, x.attribute, acc.name)
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -155,10 +157,13 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None):
|
||||
def create_payment_entry(self, amount=100, posting_date=None, customer=None):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
@@ -172,11 +177,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
return payment
|
||||
|
||||
def create_purchase_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
pinv = make_purchase_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -201,11 +209,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
return pinv
|
||||
|
||||
def create_purchase_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
pord = create_purchase_order(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -250,9 +261,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
return pr
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
@@ -402,7 +411,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
rate = 100
|
||||
invoices = []
|
||||
payments = []
|
||||
for i in range(5):
|
||||
for _i in range(5):
|
||||
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
|
||||
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
|
||||
payments.append(pe)
|
||||
@@ -821,9 +830,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
|
||||
cr_note.cancel()
|
||||
|
||||
pay = self.create_payment_entry(
|
||||
amount=amount, posting_date=transaction_date, customer=self.customer3
|
||||
)
|
||||
pay = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=self.customer3)
|
||||
pay.paid_from = self.debtors_eur
|
||||
pay.paid_from_account_currency = "EUR"
|
||||
pay.source_exchange_rate = exchange_rate
|
||||
@@ -1025,9 +1032,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
rate = 100
|
||||
|
||||
# 'Main - PR' Cost Center
|
||||
si1 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si1 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
|
||||
si1.cost_center = self.main_cc.name
|
||||
si1.submit()
|
||||
|
||||
@@ -1043,9 +1048,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
je1 = je1.save().submit()
|
||||
|
||||
# 'Sub - PR' Cost Center
|
||||
si2 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si2 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
|
||||
si2.cost_center = self.sub_cc.name
|
||||
si2.submit()
|
||||
|
||||
@@ -1332,6 +1335,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_cr_note_payment_limit_filter(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
for _ in range(6):
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 6)
|
||||
self.assertEqual(len(pr.payments), 6)
|
||||
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}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
# Limit should not affect in fetching the unallocated cr_note
|
||||
pr.invoice_limit = 5
|
||||
pr.payment_limit = 5
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
if (
|
||||
frm.doc.payment_request_type == "Inward" &&
|
||||
frm.doc.payment_channel !== "Phone" &&
|
||||
!in_list(["Initiated", "Paid"], frm.doc.status) &&
|
||||
!["Initiated", "Paid"].includes(frm.doc.status) &&
|
||||
!frm.doc.__islocal &&
|
||||
frm.doc.docstatus == 1
|
||||
) {
|
||||
|
||||
@@ -37,7 +37,7 @@ class PaymentRequest(Document):
|
||||
self.status = "Draft"
|
||||
self.validate_reference_document()
|
||||
self.validate_payment_request_amount()
|
||||
self.validate_currency()
|
||||
# self.validate_currency()
|
||||
self.validate_subscription_details()
|
||||
|
||||
def validate_reference_document(self):
|
||||
@@ -50,7 +50,7 @@ class PaymentRequest(Document):
|
||||
)
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
|
||||
if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
@@ -103,7 +103,7 @@ class PaymentRequest(Document):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
|
||||
if (
|
||||
hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"
|
||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||
) or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
@@ -155,7 +155,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def make_invoice(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
|
||||
if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
|
||||
@@ -241,14 +241,10 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(
|
||||
party_account
|
||||
)
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
|
||||
bank_amount = self.grand_total
|
||||
if (
|
||||
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
||||
):
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
@@ -266,7 +262,7 @@ class PaymentRequest(Document):
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
|
||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||
self.reference_doctype, self.reference_name, self.name
|
||||
),
|
||||
}
|
||||
@@ -280,21 +276,28 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
self.payment_request_type == "Outward"
|
||||
and payment_entry.paid_from_account_currency == ref_doc.company_currency
|
||||
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
|
||||
):
|
||||
payment_entry.paid_amount = payment_entry.base_paid_amount = (
|
||||
payment_entry.target_exchange_rate * payment_entry.received_amount
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
if payment_entry.difference_amount:
|
||||
company_details = get_company_defaults(ref_doc.company)
|
||||
|
||||
payment_entry.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": company_details.exchange_gain_loss_account,
|
||||
"cost_center": company_details.cost_center,
|
||||
"amount": payment_entry.difference_amount,
|
||||
},
|
||||
)
|
||||
|
||||
if submit:
|
||||
payment_entry.insert(ignore_permissions=True)
|
||||
payment_entry.submit()
|
||||
@@ -380,14 +383,13 @@ class PaymentRequest(Document):
|
||||
and hasattr(frappe.local, "session")
|
||||
and frappe.local.session.user != "Guest"
|
||||
) and self.payment_channel != "Phone":
|
||||
|
||||
success_url = shopping_cart_settings.payment_success_url
|
||||
if success_url:
|
||||
redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
|
||||
success_url, "/me"
|
||||
)
|
||||
else:
|
||||
redirect_to = get_url("/orders/{0}".format(self.reference_name))
|
||||
redirect_to = get_url(f"/orders/{self.reference_name}")
|
||||
|
||||
return redirect_to
|
||||
|
||||
@@ -413,15 +415,11 @@ def make_payment_request(**args):
|
||||
frappe.db.set_value(
|
||||
"Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False
|
||||
)
|
||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||
grand_total = grand_total - loyalty_amount
|
||||
|
||||
bank_account = (
|
||||
get_party_bank_account(args.get("party_type"), args.get("party"))
|
||||
if args.get("party_type")
|
||||
else ""
|
||||
get_party_bank_account(args.get("party_type"), args.get("party")) if args.get("party_type") else ""
|
||||
)
|
||||
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
@@ -441,6 +439,12 @@ def make_payment_request(**args):
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
|
||||
if not args.get("payment_request_type"):
|
||||
args["payment_request_type"] = (
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -499,9 +503,9 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -603,7 +607,11 @@ def update_payment_req_status(doc, method):
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype, ref.reference_name, doc.party_account_currency
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
@@ -32,7 +34,7 @@ payment_method = [
|
||||
]
|
||||
|
||||
|
||||
class TestPaymentRequest(unittest.TestCase):
|
||||
class TestPaymentRequest(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
@@ -86,6 +88,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=si_usd.name,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
@@ -93,11 +97,56 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
pr.create_payment_entry()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
|
||||
def test_multiple_payment_entry_against_purchase_invoice(self):
|
||||
purchase_invoice = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.grand_total = pr.grand_total / 2
|
||||
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Partly Paid")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Paid")
|
||||
|
||||
def test_payment_entry(self):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
@@ -158,7 +207,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_gle[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_gle[gle.account][2], gle.credit)
|
||||
@@ -213,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
# Try to make Payment Request more than SO amount, should give validation
|
||||
pr2.grand_total = 900
|
||||
self.assertRaises(frappe.ValidationError, pr2.save)
|
||||
|
||||
def test_conversion_on_foreign_currency_accounts(self):
|
||||
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
|
||||
po_doc.conversion_rate = 80
|
||||
po_doc.items[0].qty = 1
|
||||
po_doc.items[0].rate = 10
|
||||
po_doc.save().submit()
|
||||
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
@@ -19,9 +19,7 @@ class PaymentTermsTemplate(Document):
|
||||
total_portion += flt(term.get("invoice_portion", 0))
|
||||
|
||||
if flt(total_portion, 2) != 100.00:
|
||||
frappe.msgprint(
|
||||
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
|
||||
)
|
||||
frappe.msgprint(_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red")
|
||||
|
||||
def validate_terms(self):
|
||||
terms = []
|
||||
|
||||
@@ -47,7 +47,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
@@ -89,9 +90,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
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.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
@@ -121,7 +120,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
|
||||
"GL Entry",
|
||||
{"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0},
|
||||
):
|
||||
return
|
||||
|
||||
@@ -204,7 +204,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"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,
|
||||
"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,
|
||||
@@ -228,7 +230,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"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,
|
||||
"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,
|
||||
|
||||
@@ -33,7 +33,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -128,9 +128,7 @@ def get_pos_invoices(start, end, pos_profile, user):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
data = list(
|
||||
filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)
|
||||
)
|
||||
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
|
||||
# need to get taxes and payments so can't avoid get_doc
|
||||
data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data]
|
||||
|
||||
@@ -201,7 +199,11 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
else:
|
||||
payments.append(
|
||||
frappe._dict(
|
||||
{"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount}
|
||||
{
|
||||
"mode_of_payment": p.mode_of_payment,
|
||||
"opening_amount": 0,
|
||||
"expected_amount": p.amount,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(POSInvoice, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
@@ -129,7 +129,9 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
|
||||
if paid_amt and pay.amount != paid_amt:
|
||||
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||
return frappe.throw(
|
||||
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
||||
)
|
||||
|
||||
def validate_pos_reserved_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
@@ -164,7 +166,7 @@ class POSInvoice(SalesInvoice):
|
||||
serial_nos = row.serial_no.split("\n")
|
||||
|
||||
if serial_nos:
|
||||
for key, value in collections.Counter(serial_nos).items():
|
||||
for _key, value in collections.Counter(serial_nos).items():
|
||||
if value > 1:
|
||||
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
|
||||
|
||||
@@ -191,9 +193,7 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
|
||||
).format(
|
||||
item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed
|
||||
),
|
||||
).format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
@@ -249,7 +249,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||
|
||||
item_code, warehouse, qty = (
|
||||
item_code, warehouse, _qty = (
|
||||
frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
|
||||
@@ -338,9 +338,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
pos.set("payments", [])
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50})
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1}
|
||||
)
|
||||
@@ -594,9 +592,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
||||
|
||||
create_records()
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
before_lp_details = get_loyalty_program_details_with_points(
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
)
|
||||
@@ -670,9 +666,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
rounded_total = frappe.db.get_value(
|
||||
"Sales Invoice", pos_inv.consolidated_invoice, "rounded_total"
|
||||
)
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 3470)
|
||||
|
||||
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
|
||||
@@ -719,9 +713,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
rounded_total = frappe.db.get_value(
|
||||
"Sales Invoice", pos_inv.consolidated_invoice, "rounded_total"
|
||||
)
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 840)
|
||||
|
||||
def test_merging_with_validate_selling_price(self):
|
||||
@@ -773,9 +765,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv2.load_from_db()
|
||||
rounded_total = frappe.db.get_value(
|
||||
"Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total"
|
||||
)
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 400)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
@@ -839,19 +829,19 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||
self.assertEqual(pos_inv.items[0].discount_percentage, 10)
|
||||
# rate shouldn't change
|
||||
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||
self.assertEqual(pos_inv.items[0].rate, 405)
|
||||
|
||||
pos_inv.ignore_pricing_rule = 1
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||
self.assertEqual(pos_inv.ignore_pricing_rule, 1)
|
||||
# rate should reset since pricing rules are ignored
|
||||
self.assertEquals(pos_inv.items[0].rate, 450)
|
||||
self.assertEqual(pos_inv.items[0].rate, 450)
|
||||
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||
self.assertEqual(pos_inv.items[0].rate, 300)
|
||||
|
||||
finally:
|
||||
item_price.delete()
|
||||
@@ -874,7 +864,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no)
|
||||
|
||||
delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no")
|
||||
self.assertEquals(delivery_document_no, dn.name)
|
||||
self.assertEqual(delivery_document_no, dn.name)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -56,7 +56,9 @@ class POSInvoiceMergeLog(Document):
|
||||
bold_pos_invoice = frappe.bold(d.pos_invoice)
|
||||
bold_status = frappe.bold(status)
|
||||
if docstatus != 1:
|
||||
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
|
||||
frappe.throw(
|
||||
_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)
|
||||
)
|
||||
if status == "Consolidated":
|
||||
frappe.throw(
|
||||
_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)
|
||||
@@ -75,15 +77,17 @@ class POSInvoiceMergeLog(Document):
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg += " "
|
||||
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
def on_submit(self):
|
||||
pos_invoice_docs = [
|
||||
frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices
|
||||
]
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
returns = [d for d in pos_invoice_docs if d.get("is_return") == 1]
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
@@ -100,9 +104,7 @@ class POSInvoiceMergeLog(Document):
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
|
||||
def on_cancel(self):
|
||||
pos_invoice_docs = [
|
||||
frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices
|
||||
]
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.cancel_linked_invoices()
|
||||
@@ -192,7 +194,9 @@ class POSInvoiceMergeLog(Document):
|
||||
for t in taxes:
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(
|
||||
tax.base_tax_amount_after_discount_amount
|
||||
)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
found = True
|
||||
if not found:
|
||||
@@ -292,9 +296,7 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
|
||||
else:
|
||||
consolidated_tax_detail.update({item_code: [tax_data[0], tax_data[1]]})
|
||||
|
||||
consolidate_tax_row.item_wise_tax_detail = json.dumps(
|
||||
consolidated_tax_detail, separators=(",", ":")
|
||||
)
|
||||
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(",", ":"))
|
||||
|
||||
|
||||
def get_all_unconsolidated_invoices():
|
||||
@@ -339,9 +341,7 @@ def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
|
||||
|
||||
if len(invoices) >= 10 and closing_entry:
|
||||
closing_entry.set_status(update=True, status="Queued")
|
||||
enqueue_job(
|
||||
create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry
|
||||
)
|
||||
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
|
||||
else:
|
||||
create_merge_logs(invoice_by_customer, closing_entry)
|
||||
|
||||
@@ -389,9 +389,7 @@ def split_invoices(invoices):
|
||||
if not item.serial_no:
|
||||
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.pos_invoice == pos_invoice.return_against)
|
||||
if return_against_is_added:
|
||||
break
|
||||
|
||||
@@ -442,8 +440,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if type(error_message) == list:
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
if isinstance(error_message, list):
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
@@ -493,7 +491,7 @@ def enqueue_job(job, **kwargs):
|
||||
timeout=10000,
|
||||
event="processing_merge_logs",
|
||||
job_name=job_name,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
)
|
||||
|
||||
if job == create_merge_logs:
|
||||
|
||||
@@ -28,15 +28,11 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}
|
||||
)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}
|
||||
)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -65,15 +61,11 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}
|
||||
)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}
|
||||
)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -309,7 +301,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
init_user_and_profile()
|
||||
|
||||
item_rates = [69, 59, 29]
|
||||
for i in [1, 2]:
|
||||
for _i in [1, 2]:
|
||||
inv = create_pos_invoice(is_return=1, do_not_save=1)
|
||||
inv.items = []
|
||||
for rate in item_rates:
|
||||
|
||||
@@ -114,10 +114,8 @@ class POSProfile(Document):
|
||||
condition = " where pfu.default = 1 "
|
||||
|
||||
pos_view_users = frappe.db.sql_list(
|
||||
"""select pfu.user
|
||||
from `tabPOS Profile User` as pfu {0}""".format(
|
||||
condition
|
||||
)
|
||||
f"""select pfu.user
|
||||
from `tabPOS Profile User` as pfu {condition}"""
|
||||
)
|
||||
|
||||
for user in pos_view_users:
|
||||
@@ -144,10 +142,8 @@ def get_item_groups(pos_profile):
|
||||
def get_child_nodes(group_type, root):
|
||||
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
|
||||
return frappe.db.sql(
|
||||
""" Select name, lft, rgt from `tab{tab}` where
|
||||
lft >= {lft} and rgt <= {rgt} order by lft""".format(
|
||||
tab=group_type, lft=lft, rgt=rgt
|
||||
),
|
||||
f""" Select name, lft, rgt from `tab{group_type}` where
|
||||
lft >= {lft} and rgt <= {rgt} order by lft""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,11 +52,9 @@ def get_customers_list(pos_profile=None):
|
||||
|
||||
return (
|
||||
frappe.db.sql(
|
||||
""" select name, customer_name, customer_group,
|
||||
f""" select name, customer_name, customer_group,
|
||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
||||
and {cond}""".format(
|
||||
cond=cond
|
||||
),
|
||||
and {cond}""",
|
||||
tuple(customer_groups),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -75,7 +73,7 @@ def get_items_list(pos_profile, company):
|
||||
cond = "and i.item_group in (%s)" % (", ".join(["%s"] * len(args_list)))
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
|
||||
i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
|
||||
@@ -88,10 +86,8 @@ def get_items_list(pos_profile, company):
|
||||
where
|
||||
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0
|
||||
{cond}
|
||||
""".format(
|
||||
cond=cond
|
||||
),
|
||||
tuple([company] + args_list),
|
||||
""",
|
||||
tuple([company, *args_list]),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,15 +74,21 @@
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_13",
|
||||
"threshold_percentage",
|
||||
"priority",
|
||||
"dynamic_condition_tab",
|
||||
"condition",
|
||||
"column_break_66",
|
||||
"section_break_13",
|
||||
"apply_multiple_pricing_rules",
|
||||
"apply_discount_on_rate",
|
||||
"column_break_66",
|
||||
"threshold_percentage",
|
||||
"validate_pricing_rule_section",
|
||||
"validate_applied_rule",
|
||||
"column_break_texp",
|
||||
"rule_description",
|
||||
"priority_section",
|
||||
"has_priority",
|
||||
"column_break_sayg",
|
||||
"priority",
|
||||
"help_section",
|
||||
"pricing_rule_help",
|
||||
"reference_section",
|
||||
@@ -477,7 +483,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
@@ -487,6 +493,7 @@
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_priority",
|
||||
"description": "Higher the number, higher the priority",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@@ -513,6 +520,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -525,7 +533,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Help Article",
|
||||
"options": "Simple"
|
||||
},
|
||||
{
|
||||
@@ -603,12 +612,42 @@
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_condition_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dynamic Condition"
|
||||
},
|
||||
{
|
||||
"fieldname": "validate_pricing_rule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validate Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_texp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sayg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -31,6 +31,7 @@ class PricingRule(Document):
|
||||
self.validate_price_list_with_currency()
|
||||
self.validate_dates()
|
||||
self.validate_condition()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
if not self.margin_type:
|
||||
self.margin_rate_or_amount = 0.0
|
||||
@@ -47,6 +48,12 @@ class PricingRule(Document):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.has_priority and not self.priority:
|
||||
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||
|
||||
if self.priority and not self.has_priority:
|
||||
self.has_priority = 1
|
||||
|
||||
for apply_on, field in apply_on_dict.items():
|
||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||
@@ -77,9 +84,9 @@ class PricingRule(Document):
|
||||
|
||||
if self.priority and cint(self.priority) == 1:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the value of the field {1} should be more than 1.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
)
|
||||
_(
|
||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
@@ -195,6 +202,10 @@ class PricingRule(Document):
|
||||
):
|
||||
frappe.throw(_("Invalid condition expression"))
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions and self.is_recursive:
|
||||
frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@@ -273,9 +284,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
def get_serial_no_for_item(args):
|
||||
from erpnext.stock.get_item_details import get_serial_no
|
||||
|
||||
item_details = frappe._dict(
|
||||
{"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no}
|
||||
)
|
||||
item_details = frappe._dict({"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no})
|
||||
if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0:
|
||||
item_details.serial_no = get_serial_no(args)
|
||||
return item_details
|
||||
@@ -373,9 +382,11 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
)
|
||||
|
||||
if pricing_rule.apply_rule_on_other_items:
|
||||
item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items)
|
||||
item_details["apply_rule_on_other_items"] = json.dumps(
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code == None:
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
@@ -419,7 +430,6 @@ def update_args_for_pricing_rule(args):
|
||||
|
||||
if args.transaction_type == "selling":
|
||||
if args.customer and not (args.customer_group and args.territory):
|
||||
|
||||
if args.quotation_to and args.quotation_to != "Customer":
|
||||
customer = frappe._dict()
|
||||
else:
|
||||
@@ -450,9 +460,9 @@ def get_pricing_rule_details(args, pricing_rule):
|
||||
def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details.pricing_rule_for = pricing_rule.rate_or_discount
|
||||
|
||||
if (
|
||||
pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency
|
||||
) or (pricing_rule.margin_type == "Percentage"):
|
||||
if (pricing_rule.margin_type in ["Amount", "Percentage"] and pricing_rule.currency == args.currency) or (
|
||||
pricing_rule.margin_type == "Percentage"
|
||||
):
|
||||
item_details.margin_type = pricing_rule.margin_type
|
||||
item_details.has_margin = True
|
||||
|
||||
@@ -487,6 +497,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
||||
# Apply discount on discounted rate
|
||||
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
||||
elif args.price_list_rate:
|
||||
value = pricing_rule.get(field, 0)
|
||||
calculate_discount_percentage = False
|
||||
if field == "discount_percentage":
|
||||
field = "discount_amount"
|
||||
value = args.price_list_rate * (value / 100)
|
||||
calculate_discount_percentage = True
|
||||
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
|
||||
item_details[field] += value if pricing_rule else args.get(field, 0)
|
||||
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
|
||||
item_details.discount_percentage = flt(
|
||||
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
|
||||
)
|
||||
else:
|
||||
if field not in item_details:
|
||||
item_details.setdefault(field, 0)
|
||||
@@ -595,7 +621,7 @@ def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.get_all(
|
||||
"UOM Conversion Detail",
|
||||
filters={"parent": ("in", items), "uom": ("like", "{0}%".format(txt))},
|
||||
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
|
||||
fields=["distinct uom"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user