mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-11 17:32:28 +00:00
Compare commits
1015 Commits
future_sub
...
payments/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ecd99049 | ||
|
|
c0323890a8 | ||
|
|
19c6bf2f92 | ||
|
|
aa54516444 | ||
|
|
74fb070959 | ||
|
|
7ca9ec14a4 | ||
|
|
3413bad51d | ||
|
|
70ed75a397 | ||
|
|
873571272d | ||
|
|
48b5567af4 | ||
|
|
340e35f209 | ||
|
|
2ae1704d11 | ||
|
|
e93ff9b267 | ||
|
|
efd8f1e978 | ||
|
|
4bd26b845e | ||
|
|
ad02e4112f | ||
|
|
c914ea4adc | ||
|
|
81dc5872c4 | ||
|
|
618e737b0b | ||
|
|
b38b8ded7b | ||
|
|
613830bfba | ||
|
|
d72d858efa | ||
|
|
00b70d7654 | ||
|
|
5b3e0c5543 | ||
|
|
257e13c299 | ||
|
|
eedf22b07a | ||
|
|
c13a147df1 | ||
|
|
a8055a6da9 | ||
|
|
5f25cde788 | ||
|
|
a464bd861b | ||
|
|
0bab6f34c1 | ||
|
|
146ce59b87 | ||
|
|
6fdb8659cc | ||
|
|
ef0682a216 | ||
|
|
9d899f3c4a | ||
|
|
5442487b6c | ||
|
|
38f925b376 | ||
|
|
4da289ec61 | ||
|
|
b7525ca4e5 | ||
|
|
890016695f | ||
|
|
80ace72541 | ||
|
|
4a7cc4da87 | ||
|
|
fb6b960806 | ||
|
|
2fdc56cebf | ||
|
|
07339e2ddf | ||
|
|
418f7e97b9 | ||
|
|
baff301e8a | ||
|
|
861edd3041 | ||
|
|
77ac87494a | ||
|
|
0a5b21d002 | ||
|
|
c50aba8333 | ||
|
|
c595bf684c | ||
|
|
f51e9df921 | ||
|
|
6ebd52bdfe | ||
|
|
aa57a98872 | ||
|
|
f9d1bdd9f9 | ||
|
|
0bdffdfa98 | ||
|
|
f369b583b9 | ||
|
|
f3b91d4d62 | ||
|
|
a84a5b43e2 | ||
|
|
78be5fc276 | ||
|
|
01b345e046 | ||
|
|
e21fcbae6a | ||
|
|
a5314165c1 | ||
|
|
d45e6781c1 | ||
|
|
d4fdada83c | ||
|
|
9b4bb30af2 | ||
|
|
ab2d7f7279 | ||
|
|
9d4ad987e1 | ||
|
|
1d9ed27a89 | ||
|
|
e8f8fb8a8f | ||
|
|
b01ac4d6fd | ||
|
|
272f42eaf2 | ||
|
|
3baffffd1b | ||
|
|
83fe78bcda | ||
|
|
4500f22671 | ||
|
|
00eac65712 | ||
|
|
5d99f17583 | ||
|
|
ac01686ded | ||
|
|
e3008843d1 | ||
|
|
735558d073 | ||
|
|
6a06e26d04 | ||
|
|
4e29654458 | ||
|
|
f1d2138258 | ||
|
|
a07100d5b3 | ||
|
|
d51f9ce815 | ||
|
|
c73ea6bad9 | ||
|
|
7c5b88a4b9 | ||
|
|
598e9c1390 | ||
|
|
bb29fc4c3d | ||
|
|
df3267ad3c | ||
|
|
e9cf8937cd | ||
|
|
663a08e4cd | ||
|
|
758d1606a9 | ||
|
|
c34737f4c6 | ||
|
|
193d13be0d | ||
|
|
5d0c25f5b5 | ||
|
|
1064d7636d | ||
|
|
33e37cfbff | ||
|
|
d3d0af3815 | ||
|
|
6310497403 | ||
|
|
f960aac639 | ||
|
|
ab2b2dc5d4 | ||
|
|
f88d5dcf5c | ||
|
|
520db864b5 | ||
|
|
62873309e5 | ||
|
|
86d3a9ab03 | ||
|
|
d855a8ec1c | ||
|
|
adf1e487e1 | ||
|
|
45804c68f0 | ||
|
|
fb32d2cafb | ||
|
|
ca2fde891e | ||
|
|
a132ca1857 | ||
|
|
a1aaffbebe | ||
|
|
8dcb1ab63b | ||
|
|
6b88777829 | ||
|
|
b3a901b631 | ||
|
|
c9015f7c04 | ||
|
|
27364b7e6b | ||
|
|
10434742e9 | ||
|
|
c64deac4d3 | ||
|
|
92bde71ab1 | ||
|
|
0aae621d0e | ||
|
|
94629e0b1b | ||
|
|
2144e0337d | ||
|
|
06e5a3fe23 | ||
|
|
74b36db24e | ||
|
|
092411b54f | ||
|
|
ef7d9004d5 | ||
|
|
8bdb8e71d4 | ||
|
|
66d7b18204 | ||
|
|
784dec24c8 | ||
|
|
4cd023444a | ||
|
|
ceaa1be729 | ||
|
|
28890fa833 | ||
|
|
6721ae76de | ||
|
|
0d57a7d4a4 | ||
|
|
18bc1df6b7 | ||
|
|
13eb9ea421 | ||
|
|
fd51dd8999 | ||
|
|
cc4afc53e3 | ||
|
|
ce7f6ee71c | ||
|
|
63ca1025bc | ||
|
|
f6371f4a0d | ||
|
|
b64f70d95a | ||
|
|
e7350f18f6 | ||
|
|
2ca0f416a9 | ||
|
|
1efdc04e46 | ||
|
|
b4d22c2936 | ||
|
|
96df19149d | ||
|
|
930286e105 | ||
|
|
f440243b75 | ||
|
|
28df8aa9cf | ||
|
|
fb846ffa12 | ||
|
|
5b9309cf34 | ||
|
|
63ad9f4f86 | ||
|
|
6cbf98294a | ||
|
|
9c82c2b5d3 | ||
|
|
ee9c3e12b4 | ||
|
|
a1183f0165 | ||
|
|
74afa57a9f | ||
|
|
85641fd21c | ||
|
|
c039cfd5c3 | ||
|
|
f48b3c41de | ||
|
|
b7f5012f15 | ||
|
|
0b7657ecf1 | ||
|
|
5c5e2c3ac2 | ||
|
|
6d19aae423 | ||
|
|
3ddb18de41 | ||
|
|
7ceb294aab | ||
|
|
fbfc5b4b1d | ||
|
|
0448ea3daa | ||
|
|
9cfbd8c8e3 | ||
|
|
2238ca55a5 | ||
|
|
6e84fc5143 | ||
|
|
5404b21c7d | ||
|
|
1511280464 | ||
|
|
949c0cab72 | ||
|
|
4d41e1538d | ||
|
|
5e8700f0b4 | ||
|
|
73c4d8a8e6 | ||
|
|
5ae756a93b | ||
|
|
c9d474bc87 | ||
|
|
226095c6b8 | ||
|
|
200a43e41f | ||
|
|
834d8c64c2 | ||
|
|
d32fdaaff5 | ||
|
|
63b772a7a3 | ||
|
|
2142f8b4a3 | ||
|
|
fa20320f39 | ||
|
|
0e52ef4fd9 | ||
|
|
5f3963b3e6 | ||
|
|
4db7054542 | ||
|
|
97c2853497 | ||
|
|
48513b1087 | ||
|
|
505cd95150 | ||
|
|
c08cd938bc | ||
|
|
11f6e570a5 | ||
|
|
77e5787036 | ||
|
|
60aaeefd70 | ||
|
|
af0ae930ca | ||
|
|
9d4165b9f9 | ||
|
|
1bdeddcb04 | ||
|
|
48c3b0d094 | ||
|
|
1721175a20 | ||
|
|
13bb48434f | ||
|
|
32c4fab14f | ||
|
|
c1a5a0510e | ||
|
|
5011b76de9 | ||
|
|
4c0c0d3eb3 | ||
|
|
164498bafb | ||
|
|
066e935892 | ||
|
|
3dfee0ae43 | ||
|
|
85c47b9a5a | ||
|
|
6f8eb4d9ed | ||
|
|
3231bedfb3 | ||
|
|
e79b57975e | ||
|
|
c1812f74e8 | ||
|
|
0db82ec93a | ||
|
|
ad4991dc47 | ||
|
|
0d42793397 | ||
|
|
6fa29376a0 | ||
|
|
2fd8de2f76 | ||
|
|
9e8f25d020 | ||
|
|
144cad81a4 | ||
|
|
da2286802a | ||
|
|
3fb0858321 | ||
|
|
ad5aadde0f | ||
|
|
88a5824e31 | ||
|
|
c085b6159b | ||
|
|
8aadc18ee8 | ||
|
|
f6933111ac | ||
|
|
7eb811c71a | ||
|
|
fd680380bb | ||
|
|
54ed6a9868 | ||
|
|
f65292612b | ||
|
|
616ce2dbec | ||
|
|
af11480b7f | ||
|
|
44261223d5 | ||
|
|
d8b6767697 | ||
|
|
3a11577411 | ||
|
|
1ef890db73 | ||
|
|
4dc0d3a003 | ||
|
|
487d0a55f5 | ||
|
|
2cd9b28e5b | ||
|
|
6c2700cd8f | ||
|
|
90880c8c01 | ||
|
|
0cf478c4c2 | ||
|
|
4cc5cd5a71 | ||
|
|
245549951d | ||
|
|
4b7f1f2d67 | ||
|
|
18b4b09371 | ||
|
|
5280132423 | ||
|
|
1d52ef7afe | ||
|
|
4abdb28a1a | ||
|
|
61bc0925d5 | ||
|
|
0a4db47c69 | ||
|
|
f194615057 | ||
|
|
5a68e918cd | ||
|
|
17ba0cff44 | ||
|
|
61a57aaed5 | ||
|
|
deccb007c1 | ||
|
|
2a999e8f58 | ||
|
|
bf39e3ab32 | ||
|
|
534243a4c8 | ||
|
|
d8de6cb708 | ||
|
|
14c40d17d3 | ||
|
|
aa652c8e15 | ||
|
|
54d6d346c8 | ||
|
|
4070359174 | ||
|
|
908ca511e7 | ||
|
|
2ee70c3968 | ||
|
|
2c6938120e | ||
|
|
f913c0fde1 | ||
|
|
d6d0a1b38d | ||
|
|
2028e52c33 | ||
|
|
b65072cd98 | ||
|
|
2312757a1d | ||
|
|
1dcd1b2a23 | ||
|
|
9cdca0d662 | ||
|
|
70ff4e7644 | ||
|
|
1d6d383ec8 | ||
|
|
34089ba230 | ||
|
|
38568a3e15 | ||
|
|
8bd84e9d51 | ||
|
|
dc29fb8759 | ||
|
|
29fe8377a1 | ||
|
|
657acab32f | ||
|
|
91ce9fce9b | ||
|
|
7b0dfb2a05 | ||
|
|
755c162093 | ||
|
|
e177ece008 | ||
|
|
47c2127a31 | ||
|
|
12834ccf9a | ||
|
|
efadf94cf3 | ||
|
|
b1c23ba85c | ||
|
|
82b02d6c9b | ||
|
|
8040544216 | ||
|
|
34ad9d33ff | ||
|
|
113362e2d9 | ||
|
|
165ec2ed53 | ||
|
|
faff84c6e5 | ||
|
|
4a4e1d36f2 | ||
|
|
c930f8ba9d | ||
|
|
42885a81f0 | ||
|
|
c8af544ef3 | ||
|
|
0f0b19688a | ||
|
|
4253caf910 | ||
|
|
213b2ba942 | ||
|
|
e78e948b7c | ||
|
|
56620785a0 | ||
|
|
751a25c4b7 | ||
|
|
05b92c5321 | ||
|
|
9ade269b7a | ||
|
|
991069bfbc | ||
|
|
3617b41b95 | ||
|
|
03f3ab522f | ||
|
|
3ffac73598 | ||
|
|
f7c0ccce0a | ||
|
|
a1bcc6b418 | ||
|
|
6d2a36c4f9 | ||
|
|
5040e3ca7d | ||
|
|
da22a8df42 | ||
|
|
511a0b9f37 | ||
|
|
5d58eb67a6 | ||
|
|
bb8c9b5a58 | ||
|
|
59d5beee20 | ||
|
|
bb877f4a6b | ||
|
|
cfe2ae604b | ||
|
|
5d0a38dfc7 | ||
|
|
41e395a7e3 | ||
|
|
a4311e345d | ||
|
|
61576ca030 | ||
|
|
281198456d | ||
|
|
ac629ede79 | ||
|
|
fb2aa7d205 | ||
|
|
9fe47ac101 | ||
|
|
d811fdf675 | ||
|
|
dd5a5e4919 | ||
|
|
f620ef20ae | ||
|
|
40c166a0a0 | ||
|
|
33a0a529a5 | ||
|
|
8624aeca54 | ||
|
|
a694390a12 | ||
|
|
4986f28a89 | ||
|
|
0e5e503b42 | ||
|
|
fd71d8af52 | ||
|
|
1c7f7c8d1a | ||
|
|
93ee922c5f | ||
|
|
23fed831a0 | ||
|
|
0ecfa709d8 | ||
|
|
c5d68333c9 | ||
|
|
ff3a5058c1 | ||
|
|
5ec9df8d1c | ||
|
|
0b6e7f83cd | ||
|
|
f0768010d9 | ||
|
|
cbb749a3a5 | ||
|
|
b94af28587 | ||
|
|
8eff168d76 | ||
|
|
25dac1f18e | ||
|
|
cb522f8f22 | ||
|
|
0b27b19c35 | ||
|
|
21d8b09f47 | ||
|
|
e9c1a7e3e3 | ||
|
|
24b26627e7 | ||
|
|
9aaf6e47d5 | ||
|
|
4931b74b3f | ||
|
|
532bc163f6 | ||
|
|
4c3ec767ce | ||
|
|
0d77e0b0cd | ||
|
|
723ac0ffc4 | ||
|
|
9a0894fd65 | ||
|
|
096ec2db6a | ||
|
|
3b481ae656 | ||
|
|
ad31b63537 | ||
|
|
3e19041fa3 | ||
|
|
ce81fd9ba6 | ||
|
|
07281d30bb | ||
|
|
764dd12b10 | ||
|
|
77590e6077 | ||
|
|
40b59de4cd | ||
|
|
302339998f | ||
|
|
0e817f42ef | ||
|
|
8c6d666163 | ||
|
|
4ab6134306 | ||
|
|
e062ec38b0 | ||
|
|
fc4e5f165c | ||
|
|
ce2b9e0f1a | ||
|
|
06e2d7265c | ||
|
|
9cd3374101 | ||
|
|
2533808f1e | ||
|
|
89c458e229 | ||
|
|
ffc5bbfa4c | ||
|
|
92a6fc8c2e | ||
|
|
8ccf8c0ea2 | ||
|
|
972329cc16 | ||
|
|
35981b8730 | ||
|
|
6e1fc33d01 | ||
|
|
c8e85f4bb7 | ||
|
|
bf2c9a7f41 | ||
|
|
be2648245b | ||
|
|
45a6ecbd38 | ||
|
|
669cf6094e | ||
|
|
65f80abf2f | ||
|
|
9b463753b7 | ||
|
|
f4d4658cbb | ||
|
|
69b8e10e20 | ||
|
|
9e93bc5819 | ||
|
|
ebb95b7d9a | ||
|
|
c3c5d3f615 | ||
|
|
1bbe020cfd | ||
|
|
d62eb0f5c1 | ||
|
|
930ecb3170 | ||
|
|
f44de670ca | ||
|
|
5d4578a172 | ||
|
|
45391c951b | ||
|
|
741fc54eca | ||
|
|
e5dfc5e545 | ||
|
|
e1b9b432c3 | ||
|
|
67bf9bec47 | ||
|
|
6d83c11d8e | ||
|
|
65d672da65 | ||
|
|
79a46ad6f8 | ||
|
|
a912e07b74 | ||
|
|
e71cb4eab7 | ||
|
|
07fc952a43 | ||
|
|
09f429ffba | ||
|
|
fe46e1d089 | ||
|
|
e81373bb6a | ||
|
|
a467888a67 | ||
|
|
06c5334f2a | ||
|
|
f3fda9ce98 | ||
|
|
57f928da55 | ||
|
|
bb9e42cce2 | ||
|
|
c587ede548 | ||
|
|
ee44022249 | ||
|
|
29d50b770e | ||
|
|
2f63fae31d | ||
|
|
cf55c2ab3d | ||
|
|
e1dedc5402 | ||
|
|
62fb495a65 | ||
|
|
da4ed90a3e | ||
|
|
2d686c06ea | ||
|
|
a30af68e9e | ||
|
|
c8dad976e2 | ||
|
|
ea03ce3662 | ||
|
|
001e5b612b | ||
|
|
81e0b96c30 | ||
|
|
85d2d34116 | ||
|
|
21df38bf18 | ||
|
|
327b19cba6 | ||
|
|
656e363aef | ||
|
|
6a50b40976 | ||
|
|
17b437709c | ||
|
|
66b35ec9fb | ||
|
|
6d42cd0f4c | ||
|
|
58bfd3b973 | ||
|
|
fde348c8a0 | ||
|
|
40cebf257b | ||
|
|
8784966138 | ||
|
|
794a62aecb | ||
|
|
c283cda169 | ||
|
|
8f135e9859 | ||
|
|
05fedf046c | ||
|
|
577ce5ccd4 | ||
|
|
f9d2dd0a62 | ||
|
|
372b5a4ca7 | ||
|
|
5e3359c5c5 | ||
|
|
61daa318fe | ||
|
|
7656220075 | ||
|
|
c8ad6d1b2c | ||
|
|
9cf92eaeab | ||
|
|
b04da63aad | ||
|
|
d0bbc8ca70 | ||
|
|
9838f7e6ba | ||
|
|
10280d6140 | ||
|
|
722ef92324 | ||
|
|
b17696a8ae | ||
|
|
3bde29c98b | ||
|
|
cf4fbfb601 | ||
|
|
a4a6d90857 | ||
|
|
1049550951 | ||
|
|
62fc3bd586 | ||
|
|
22549186ab | ||
|
|
d4c16c1740 | ||
|
|
9562628ed6 | ||
|
|
8633080dff | ||
|
|
3ef7e6ce5c | ||
|
|
d3520a62cc | ||
|
|
5c0d52f783 | ||
|
|
d86e0ad694 | ||
|
|
0893a06b42 | ||
|
|
1186ee128c | ||
|
|
623b4c21cd | ||
|
|
ad137250fc | ||
|
|
0c76a8cc9e | ||
|
|
4936514c8d | ||
|
|
96b275c61e | ||
|
|
edc1f84250 | ||
|
|
5f53ad3117 | ||
|
|
2383a1a18e | ||
|
|
e7ee508de4 | ||
|
|
cd3a900495 | ||
|
|
10ae5aaf52 | ||
|
|
fa614dd8d3 | ||
|
|
4b415987f9 | ||
|
|
b4d3dfc234 | ||
|
|
cfda332faf | ||
|
|
c52fdffdaf | ||
|
|
4cea34c1c6 | ||
|
|
f06ba0cc36 | ||
|
|
3aaa22e672 | ||
|
|
9474f72776 | ||
|
|
36403cde98 | ||
|
|
ee862126e4 | ||
|
|
55e2b876a2 | ||
|
|
ddaf81fc33 | ||
|
|
9cfe09bdf6 | ||
|
|
1594e7bc25 | ||
|
|
6a2a6667d2 | ||
|
|
eb4af58bf0 | ||
|
|
9bd4e7b709 | ||
|
|
9d7be293ae | ||
|
|
e923e539d8 | ||
|
|
c4ae0d283f | ||
|
|
4512432816 | ||
|
|
938dd4b2aa | ||
|
|
909aa8f359 | ||
|
|
1dae2156e3 | ||
|
|
dc5d3b69be | ||
|
|
9eb0521e95 | ||
|
|
c39a44c016 | ||
|
|
c8a34cde7f | ||
|
|
97f02015c2 | ||
|
|
0e256b8b29 | ||
|
|
bd98f2f685 | ||
|
|
6760c9c4e2 | ||
|
|
6ba6b5aa33 | ||
|
|
406dfd528f | ||
|
|
fa56555150 | ||
|
|
0fcd5d5130 | ||
|
|
99f2735ad3 | ||
|
|
cbd25aeaa4 | ||
|
|
c5e474f4f5 | ||
|
|
60694e09c4 | ||
|
|
db807d433b | ||
|
|
5eed781263 | ||
|
|
8f424528dd | ||
|
|
7c9129d0be | ||
|
|
f440e7cd94 | ||
|
|
55a4bd469b | ||
|
|
9d8794f3ca | ||
|
|
ec9ab6d762 | ||
|
|
188f0e0f32 | ||
|
|
6e2ac09821 | ||
|
|
8b0de4aaf7 | ||
|
|
32bdcdb08f | ||
|
|
03e674e21d | ||
|
|
9ec6aef95d | ||
|
|
ad7efd5939 | ||
|
|
1b384b9942 | ||
|
|
5e84272cf9 | ||
|
|
f6c1dffb35 | ||
|
|
affaaee1f4 | ||
|
|
0c5cda1380 | ||
|
|
252d052f86 | ||
|
|
1f74439b16 | ||
|
|
ba79e68190 | ||
|
|
9738c04ef0 | ||
|
|
f850ccc4d8 | ||
|
|
7b5e041928 | ||
|
|
42d401a1dd | ||
|
|
26aa1a128f | ||
|
|
80c6981cfa | ||
|
|
8ae2b8ff8c | ||
|
|
625f16dee0 | ||
|
|
5fdd1d3278 | ||
|
|
eca3e02f8d | ||
|
|
1a9899b32b | ||
|
|
2e76b9f9db | ||
|
|
ba08b2e8e8 | ||
|
|
61fe95a0bd | ||
|
|
1c643a0ead | ||
|
|
5738d93f95 | ||
|
|
98ad01c736 | ||
|
|
2112d8f772 | ||
|
|
1ad264de48 | ||
|
|
97c9941143 | ||
|
|
07f68884e0 | ||
|
|
b202409428 | ||
|
|
13fb560401 | ||
|
|
9ab333d105 | ||
|
|
5844897c34 | ||
|
|
8304d19e8b | ||
|
|
d50487ce53 | ||
|
|
7d91c6cbd5 | ||
|
|
379a290625 | ||
|
|
cf6c13ecae | ||
|
|
73d56c4b33 | ||
|
|
5ec62756fb | ||
|
|
a4c0115b5e | ||
|
|
ac6d85aed6 | ||
|
|
efebc3662e | ||
|
|
3fabf4aaa4 | ||
|
|
fd7666a029 | ||
|
|
4f92ab0ac7 | ||
|
|
21adc7b63e | ||
|
|
20c4098399 | ||
|
|
c0a8800832 | ||
|
|
5f8f81c0ba | ||
|
|
3fab00135b | ||
|
|
5e875b238c | ||
|
|
86869ac8b8 | ||
|
|
100d8e2007 | ||
|
|
d5ed4582c3 | ||
|
|
f28c692dca | ||
|
|
306ab454d5 | ||
|
|
ef744c1f2c | ||
|
|
b823aa66bf | ||
|
|
7f7b363d48 | ||
|
|
86ebe58231 | ||
|
|
654764e398 | ||
|
|
2851764ed6 | ||
|
|
7bae18aba8 | ||
|
|
f740c94363 | ||
|
|
003a9608dc | ||
|
|
49331e6109 | ||
|
|
5de91cf55e | ||
|
|
a9910817f5 | ||
|
|
64e63887be | ||
|
|
c696d13a5e | ||
|
|
8a397a4506 | ||
|
|
1cce7064e0 | ||
|
|
3025501500 | ||
|
|
a2ba0f51a0 | ||
|
|
46d33d27cf | ||
|
|
8141eacb02 | ||
|
|
194f1dc674 | ||
|
|
28f195023e | ||
|
|
2e5b85c329 | ||
|
|
6d539e0fc0 | ||
|
|
1b072228d3 | ||
|
|
18993a97ce | ||
|
|
2c0f03d43a | ||
|
|
555510c128 | ||
|
|
757aef3972 | ||
|
|
f876fec6f5 | ||
|
|
3f13554bfb | ||
|
|
4fc43b9009 | ||
|
|
317047c808 | ||
|
|
7e318c0132 | ||
|
|
07d59443b7 | ||
|
|
83ff94b9b8 | ||
|
|
b401ba2c26 | ||
|
|
7479459cb6 | ||
|
|
895aede590 | ||
|
|
2fb3e5da4a | ||
|
|
77959596db | ||
|
|
73f22ba9a0 | ||
|
|
62e4e88f1d | ||
|
|
cff1050159 | ||
|
|
ebb0cef601 | ||
|
|
b618d685c6 | ||
|
|
13cb53fa89 | ||
|
|
339b6692fe | ||
|
|
99695df7bc | ||
|
|
7996723e07 | ||
|
|
8b3c0f6d69 | ||
|
|
f4e369977a | ||
|
|
6e55651795 | ||
|
|
b44630a28c | ||
|
|
d85f316995 | ||
|
|
56082f5a29 | ||
|
|
75983ead33 | ||
|
|
17731f09ef | ||
|
|
9f3b1ce5d5 | ||
|
|
7ee2a19f53 | ||
|
|
53d99b7c8c | ||
|
|
a1c4e0e93c | ||
|
|
cb52b76342 | ||
|
|
f4acdd9636 | ||
|
|
eb426c69fe | ||
|
|
7249a691b3 | ||
|
|
db313706a7 | ||
|
|
6bded59f1c | ||
|
|
857c689405 | ||
|
|
8d7e68d355 | ||
|
|
4c03dcb4ba | ||
|
|
17f968e1e1 | ||
|
|
107b614518 | ||
|
|
4f9a228175 | ||
|
|
0f0b4d88bc | ||
|
|
90c84822d0 | ||
|
|
827d67d02f | ||
|
|
475e0ddeee | ||
|
|
7dce6e03c7 | ||
|
|
c9ede1ffbe | ||
|
|
e7740033ca | ||
|
|
1a10f0bcbd | ||
|
|
7ce9f7bc12 | ||
|
|
d9ddad0f1f | ||
|
|
cf0a29b05f | ||
|
|
fec9d45ab3 | ||
|
|
4b026d66dc | ||
|
|
36413d14d8 | ||
|
|
c9bcd70173 | ||
|
|
f6a4d391c0 | ||
|
|
8ec364df6f | ||
|
|
3956354e08 | ||
|
|
699cfd85c6 | ||
|
|
c992656fa8 | ||
|
|
2c164b47a1 | ||
|
|
746f95660d | ||
|
|
458d8f5ed8 | ||
|
|
49e180a9b9 | ||
|
|
a7ec0c1cec | ||
|
|
5c8ea86a3f | ||
|
|
c76f466528 | ||
|
|
689e1cfc23 | ||
|
|
d163dbec7d | ||
|
|
4ff3e6caaa | ||
|
|
71f5470dfd | ||
|
|
12e48f0b63 | ||
|
|
cb6d8afa26 | ||
|
|
cf508ce1ed | ||
|
|
a8fc32dc32 | ||
|
|
60eb03a6c6 | ||
|
|
80f6228d45 | ||
|
|
c3ae5bc6a5 | ||
|
|
e26bc17c75 | ||
|
|
d165638bbb | ||
|
|
a2039bae10 | ||
|
|
740c495014 | ||
|
|
9a3569ff15 | ||
|
|
d30c797d24 | ||
|
|
2be8e0dce0 | ||
|
|
441596f795 | ||
|
|
4558f64c0f | ||
|
|
4f0214d00e | ||
|
|
3c3313594d | ||
|
|
5a75c847fe | ||
|
|
2b1242170c | ||
|
|
d2af36e1eb | ||
|
|
9e9296e444 | ||
|
|
24926ab665 | ||
|
|
169d77da54 | ||
|
|
666232fd1c | ||
|
|
ee846f5950 | ||
|
|
0ba4bff943 | ||
|
|
c36f0e4a33 | ||
|
|
05d17d0d73 | ||
|
|
ee69ec97dd | ||
|
|
4e37ed9033 | ||
|
|
07c421228b | ||
|
|
c6c696247a | ||
|
|
968120d0eb | ||
|
|
be9f960705 | ||
|
|
091c5496b2 | ||
|
|
3f2343614b | ||
|
|
388ba7f945 | ||
|
|
289d216552 | ||
|
|
1c9fe691ea | ||
|
|
c41b239881 | ||
|
|
9f85483199 | ||
|
|
882227a460 | ||
|
|
006b1f553c | ||
|
|
6b0ce33030 | ||
|
|
ce834f5dba | ||
|
|
e3cf53a8b7 | ||
|
|
4165fd44b5 | ||
|
|
d3bdd41358 | ||
|
|
eaa4efbc45 | ||
|
|
06401cc84f | ||
|
|
e3fd82766f | ||
|
|
537e0e32b2 | ||
|
|
47bc5691a1 | ||
|
|
c5c4d5c61f | ||
|
|
25931c4db2 | ||
|
|
683c1f04c8 | ||
|
|
b6350c86d1 | ||
|
|
fac0fcb9d5 | ||
|
|
3f06f1905f | ||
|
|
954ef85b95 | ||
|
|
06c7ce1e95 | ||
|
|
fffcb5dc9f | ||
|
|
5d0e82f40b | ||
|
|
e2ec3e453a | ||
|
|
0303d7dbdc | ||
|
|
418439b58f | ||
|
|
e39d2e1e69 | ||
|
|
c4e2abb973 | ||
|
|
014b542cf3 | ||
|
|
cb3c20dcd3 | ||
|
|
ad817cddad | ||
|
|
04a49cef24 | ||
|
|
eafa88b8e9 | ||
|
|
76073ae228 | ||
|
|
85b1a8001b | ||
|
|
3b4913ec81 | ||
|
|
d7df2cbdc5 | ||
|
|
ac4bcd5ca7 | ||
|
|
4261c3474b | ||
|
|
4e4c1e4bef | ||
|
|
ae3f5a38e1 | ||
|
|
dfdb95bbbe | ||
|
|
a1d489195d | ||
|
|
f24d61daea | ||
|
|
0191727149 | ||
|
|
e22b0fd6e1 | ||
|
|
91c9d964f9 | ||
|
|
6e3782a061 | ||
|
|
eb4f94ddf2 | ||
|
|
be1fce2da6 | ||
|
|
30aa4e031d | ||
|
|
214b38f7c8 | ||
|
|
28e10dd703 | ||
|
|
f7eb9d44a8 | ||
|
|
b071e952dd | ||
|
|
7b9fd394b0 | ||
|
|
7ce8fc9ba2 | ||
|
|
63c2cf13fd | ||
|
|
37d97fad17 | ||
|
|
5cf5b18aea | ||
|
|
1c0a24424a | ||
|
|
61a4188440 | ||
|
|
3f6787b267 | ||
|
|
deb9766f2a | ||
|
|
d61f56207c | ||
|
|
76131f8e10 | ||
|
|
cafa2f52e5 | ||
|
|
e883d6a7d3 | ||
|
|
44e6563af6 | ||
|
|
7a3f8a5afb | ||
|
|
8a30701893 | ||
|
|
500c546691 | ||
|
|
af8275c321 | ||
|
|
bd381cc0c6 | ||
|
|
8ac11ae88d | ||
|
|
2761705c62 | ||
|
|
000c1b49dc | ||
|
|
070e2d4d26 | ||
|
|
3c1ca1cfbb | ||
|
|
81a9521f04 | ||
|
|
0363afcfd0 | ||
|
|
7a47b2157b | ||
|
|
abfb8a7201 | ||
|
|
fc797836af | ||
|
|
7d86881579 | ||
|
|
d22df324ec | ||
|
|
4a4c674553 | ||
|
|
7b264e5e11 | ||
|
|
c0c8c1bb02 | ||
|
|
d40b55468c | ||
|
|
fccd37d32d | ||
|
|
b8a98a273b | ||
|
|
99d417bbc2 | ||
|
|
f337392f3e | ||
|
|
e843683ad1 | ||
|
|
a36b7fb95a | ||
|
|
16751ec935 | ||
|
|
0736154074 | ||
|
|
fa2b6c4490 | ||
|
|
3bd455ac85 | ||
|
|
d6fe220fab | ||
|
|
e3f539325e | ||
|
|
e3a80ebdf3 | ||
|
|
3309359822 | ||
|
|
6bd13d7452 | ||
|
|
13a3a6a82e | ||
|
|
64333373ca | ||
|
|
c6fd42fe60 | ||
|
|
77bfea975f | ||
|
|
663fcb374d | ||
|
|
065163146c | ||
|
|
d56f52b0ba | ||
|
|
32a463652e | ||
|
|
7f2062e2c5 | ||
|
|
1b45ecfcae | ||
|
|
382d0ff453 | ||
|
|
98e7dfe97f | ||
|
|
6b24143f72 | ||
|
|
d3200fb67f | ||
|
|
7501fe8ebd | ||
|
|
3fa8706f57 | ||
|
|
f92aabd0f3 | ||
|
|
628d7e6458 | ||
|
|
360c3b36ed | ||
|
|
31841b4ab2 | ||
|
|
ba60b5911a | ||
|
|
501c6aa126 | ||
|
|
cfde8088b4 | ||
|
|
249e8264dd | ||
|
|
c90185f533 | ||
|
|
08899d05fb | ||
|
|
754c7f6d1c | ||
|
|
92673d706c | ||
|
|
9c3a105f85 | ||
|
|
418bdc1dcc | ||
|
|
c29d955371 | ||
|
|
272ce24016 | ||
|
|
651e4696fd | ||
|
|
5ed1b6b8fb | ||
|
|
cd5d244d2c | ||
|
|
db5ed972e5 | ||
|
|
0887161f2a | ||
|
|
544fc60093 | ||
|
|
c50663c640 | ||
|
|
c41a037174 | ||
|
|
c710ad822c | ||
|
|
ebfbe94aaf | ||
|
|
e51ccecd8a | ||
|
|
bd8382c592 | ||
|
|
6142d07f1a | ||
|
|
ed5344d58b | ||
|
|
44228b754f | ||
|
|
d19ae4ce4c | ||
|
|
58f7039630 | ||
|
|
eac7be2d0f | ||
|
|
42ef95759d | ||
|
|
eb31017058 | ||
|
|
69d68592ad | ||
|
|
eb9f579b8f | ||
|
|
e4db0562ac | ||
|
|
a61148c464 | ||
|
|
7fa22069d8 | ||
|
|
54a12c5186 | ||
|
|
c8e92cb1b2 | ||
|
|
93c6324115 | ||
|
|
f26be2a303 | ||
|
|
61f5ebd2dd | ||
|
|
22c345a24a | ||
|
|
9bf37426c1 | ||
|
|
1cbc200770 | ||
|
|
8289f3c724 | ||
|
|
da3010a41f | ||
|
|
ed9af7d12e | ||
|
|
bf755fab55 | ||
|
|
2402447568 | ||
|
|
f77d5db38e | ||
|
|
fed2d11905 | ||
|
|
8861a76bd0 | ||
|
|
56f0b2628e | ||
|
|
7a6cc84d41 | ||
|
|
a34c99385c | ||
|
|
94339c569e | ||
|
|
1df06efd97 | ||
|
|
52e52e83a9 | ||
|
|
9f092dbe9f | ||
|
|
b324a02fbb | ||
|
|
5fa4cfee04 | ||
|
|
5e48c1ac51 | ||
|
|
e0d12ba4d0 | ||
|
|
f7f3b22786 | ||
|
|
e43697d359 | ||
|
|
72057adc94 | ||
|
|
2f9a144023 | ||
|
|
9e2ecd67f9 | ||
|
|
b195f519e2 | ||
|
|
9f346e7ba0 | ||
|
|
41695e601d | ||
|
|
066859cca0 | ||
|
|
44f9059338 | ||
|
|
d95d00b7c8 | ||
|
|
880d061164 | ||
|
|
bb5f1dae75 | ||
|
|
1bb1b69dea | ||
|
|
3562c2d1d1 | ||
|
|
46736a737f | ||
|
|
5b1e860489 | ||
|
|
8e095335be | ||
|
|
7fa94843aa | ||
|
|
be7fd6bfb4 | ||
|
|
90d1873ad0 | ||
|
|
341fb6d8f3 | ||
|
|
ca577f7aaa | ||
|
|
2f359e201d | ||
|
|
3c1af2acf0 | ||
|
|
7cb66f7fd3 | ||
|
|
a48966f08c | ||
|
|
ecf07bd128 | ||
|
|
1c613ada6f | ||
|
|
4250ac821f | ||
|
|
aefbe21b46 | ||
|
|
808dbfcade | ||
|
|
58c7922c36 | ||
|
|
54a58e9205 | ||
|
|
eb119dcb4d | ||
|
|
9d9331408f | ||
|
|
2896e3666c | ||
|
|
24a68a79df | ||
|
|
fe5b88522e | ||
|
|
39d6df7c7d | ||
|
|
5f7e06ba6f | ||
|
|
d791fa7460 | ||
|
|
01888c98bc | ||
|
|
581af4eced | ||
|
|
7ef4dbcaf6 | ||
|
|
4e5bc82a17 | ||
|
|
5c9ce575f6 | ||
|
|
a2f878e1d1 | ||
|
|
ee140369a3 | ||
|
|
7032a108a8 | ||
|
|
db1f7fcf63 | ||
|
|
eccd5b4c5d | ||
|
|
05e8b6c9f6 | ||
|
|
d721de13aa | ||
|
|
9941469fca | ||
|
|
c6a3403270 | ||
|
|
0b36cbe307 | ||
|
|
8b3d46610e | ||
|
|
53642e7417 | ||
|
|
baa3fee1bf | ||
|
|
3c9640df27 | ||
|
|
b39f7b1c2f | ||
|
|
ff7e0eaba5 |
@@ -18,4 +18,4 @@ max_line_length = 110
|
||||
[{*.json}]
|
||||
insert_final_newline = false
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_size = 1
|
||||
|
||||
4
.github/helper/update_pot_file.sh
vendored
4
.github/helper/update_pot_file.sh
vendored
@@ -30,11 +30,11 @@ branch_name="pot_${BASE_BRANCH}_${isodate}"
|
||||
git checkout -b "${branch_name}"
|
||||
|
||||
echo "Commiting changes..."
|
||||
git add .
|
||||
git add erpnext/locale/main.pot
|
||||
git commit -m "chore: update POT file"
|
||||
|
||||
gh auth setup-git
|
||||
git push -u upstream "${branch_name}"
|
||||
|
||||
echo "Creating a PR..."
|
||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/erpnext
|
||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" --reviewer ${PR_REVIEWER} -R frappe/erpnext
|
||||
|
||||
5
.github/workflows/generate-pot-file.yml
vendored
5
.github/workflows/generate-pot-file.yml
vendored
@@ -9,8 +9,8 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
regeneratee-pot-file:
|
||||
name: Release
|
||||
regenerate-pot-file:
|
||||
name: Regenerate POT file
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -36,3 +36,4 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
BASE_BRANCH: ${{ matrix.branch }}
|
||||
PR_REVIEWER: barredterra # change to your GitHub username if you copied this file
|
||||
|
||||
6
.github/workflows/release_notes.yml
vendored
6
.github/workflows/release_notes.yml
vendored
@@ -29,7 +29,11 @@ jobs:
|
||||
steps:
|
||||
- name: Update notes
|
||||
run: |
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
|
||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG \
|
||||
| jq -r '.body' \
|
||||
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
|
||||
| sed -E 's/by @mergify //'
|
||||
)
|
||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
|
||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,5 +14,8 @@ __pycache__
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
.helix/
|
||||
node_modules/
|
||||
.backportrc.json
|
||||
.backportrc.json
|
||||
# Aider AI Chat
|
||||
.aider*
|
||||
|
||||
@@ -59,12 +59,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
**/setup/setup_wizard/data/uom_data.json,erpnext.gettext.extractors.uom_data.extract
|
||||
**/setup/doctype/incoterm/incoterms.csv,erpnext.gettext.extractors.incoterms.extract
|
||||
**/setup/setup_wizard/data/*.txt,erpnext.gettext.extractors.lines_from_txt_file.extract
|
||||
|
||||
|
@@ -4,5 +4,7 @@ files:
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
|
||||
@@ -2,6 +2,7 @@ import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0-dev"
|
||||
|
||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
def check_app_permission():
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
if is_website_user():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -360,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:
|
||||
|
||||
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
|
||||
// hide fields if group
|
||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||
|
||||
// disable fields
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
|
||||
|
||||
if (cint(frm.doc.is_group) == 0) {
|
||||
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1
|
||||
"label": "Account Number"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -65,6 +64,8 @@
|
||||
"label": "Is Group"
|
||||
},
|
||||
{
|
||||
"fetch_from": "parent_account.company",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
@@ -72,7 +73,6 @@
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -124,7 +124,8 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -193,7 +194,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:55.866034",
|
||||
"modified": "2024-06-27 16:23:04.444354",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
|
||||
@@ -200,7 +200,7 @@ class Account(NestedSet):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
||||
"General Ledger",
|
||||
"Balance Sheet",
|
||||
"Profit and Loss Statement",
|
||||
"Cash Flow Statement",
|
||||
"Cash Flow",
|
||||
"Accounts Payable",
|
||||
"Accounts Receivable",
|
||||
]) {
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
{
|
||||
"country_code": "tr",
|
||||
"name": "Turkey - Tek D\u00fczen Hesap Plan\u0131",
|
||||
"tree": {
|
||||
"Duran Varl\u0131klar": {
|
||||
"Di\u011fer Alacaklar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Alacaklar": {},
|
||||
"Di\u011fer Alacak Senetleri Reeskontu(-)": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Alacaklar": {},
|
||||
"Ortaklardan Alacaklar": {},
|
||||
"Personelden Alacaklar": {},
|
||||
"\u0130\u015ftiraklerden Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Di\u011fer Duran Varl\u0131klar": {
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Di\u011fer KDV": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Duran Varl\u0131klar": {},
|
||||
"Elden \u00c7\u0131kar\u0131lacak Stoklar Ve Maddi Duran Varl\u0131klar": {},
|
||||
"Gelecek Y\u0131llar \u0130htiyac\u0131 Stoklar": {},
|
||||
"Gelecek Y\u0131llarda \u0130ndirilecek KDV": {},
|
||||
"Pe\u015fin \u00d6denen Vergi Ve Fonlar": {},
|
||||
"Stok De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Gelecek Y\u0131llara Ait Giderler ve Gelir Tahakkuklar\u0131": {
|
||||
"Gelecek Y\u0131llara Ait Giderler": {},
|
||||
"Gelir Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Maddi Duran Varl\u0131klar": {
|
||||
"Arazi Ve Arsalar": {},
|
||||
"Binalar": {},
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Demirba\u015flar": {},
|
||||
"Di\u011fer Maddi Duran Varl\u0131klar": {},
|
||||
"Ta\u015f\u0131tlar": {},
|
||||
"Tesis, Makine Ve Cihazlar": {},
|
||||
"Verilen Avanslar": {},
|
||||
"Yap\u0131lmakta Olan Yat\u0131r\u0131mlar": {},
|
||||
"Yer Alt\u0131 Ve Yer \u00dcst\u00fc D\u00fczenleri": {}
|
||||
},
|
||||
"Maddi Olmayan Duran Varl\u0131klar": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Birikmi\u015f Amortismanlar(-)": {},
|
||||
"Di\u011fer Maddi Olmayan Duran Varl\u0131klar": {},
|
||||
"Haklar": {},
|
||||
"Kurulu\u015f Ve \u00d6rg\u00fctlenme Giderleri": {},
|
||||
"Verilen Avanslar": {},
|
||||
"\u00d6zel Maliyetler": {},
|
||||
"\u015eerefiye": {}
|
||||
},
|
||||
"Mali Duran Varl\u0131klar": {
|
||||
"Ba\u011fl\u0131 Menkul K\u0131ymetler": {},
|
||||
"Ba\u011fl\u0131 Menkul K\u0131ymetler De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klar": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klar Sermaye Paylar\u0131 De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Sermaye Taahh\u00fctleri(-)": {},
|
||||
"Di\u011fer Mali Duran Varl\u0131klar": {},
|
||||
"Di\u011fer Mali Duran Varl\u0131klar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u0130\u015ftirakler": {},
|
||||
"\u0130\u015ftirakler Sermaye Paylar\u0131 De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u0130\u015ftiraklere Sermaye Taahh\u00fctleri(-)": {}
|
||||
},
|
||||
"Ticari Alacaklar": {
|
||||
"Alacak Senetleri": {},
|
||||
"Alacak Senetleri Reeskontu(-)": {},
|
||||
"Al\u0131c\u0131lar": {},
|
||||
"Kazaqn\u0131lmam\u0131\u015f Finansal Kiralama Faiz Gelirleri(-)": {},
|
||||
"Verilen Depozito Ve Teminatlar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6zel T\u00fckenmeye Tabi Varl\u0131klar": {
|
||||
"Arama Giderleri": {},
|
||||
"Birikmi\u015f T\u00fckenme Paylar\u0131(-)": {},
|
||||
"Di\u011fer \u00d6zel T\u00fckenmeye Tabi Varl\u0131klar": {},
|
||||
"Haz\u0131rl\u0131k Ve Geli\u015ftirme Giderleri": {},
|
||||
"Verilen Avanslar": {}
|
||||
}
|
||||
},
|
||||
"D\u00f6nen Varl\u0131klar": {
|
||||
"Di\u011fer Alacaklar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Alacaklar": {},
|
||||
"Di\u011fer Alacak Senetleri Reeskontu(-)": {},
|
||||
"Di\u011fer \u00c7e\u015fitli Alacaklar": {},
|
||||
"Ortaklardan Alacaklar": {},
|
||||
"Personelden Alacaklar": {},
|
||||
"\u0130\u015ftiraklerden Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar": {},
|
||||
"\u015e\u00fcpheli Di\u011fer Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {}
|
||||
},
|
||||
"Di\u011fer D\u00f6nen Varl\u0131klar": {
|
||||
"Devreden KDV": {},
|
||||
"Di\u011fer D\u00f6nen Varl\u0131klar Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Di\u011fer KDV": {},
|
||||
"Di\u011fer \u00c7e\u015fitli D\u00f6nen Varl\u0131klar": {},
|
||||
"Personel Avanslar\u0131": {},
|
||||
"Pe\u015fin \u00d6denen Vergiler Ve Fonlar": {},
|
||||
"Say\u0131m Ve Tesell\u00fcm Noksanlar\u0131": {},
|
||||
"\u0130ndirilecek KDV": {},
|
||||
"\u0130\u015f Avanslar\u0131": {}
|
||||
},
|
||||
"Gelecek Aylara Ait Giderler ve Gelir Tahakkuklar\u0131": {
|
||||
"Gelecek Aylara Ait Giderler": {},
|
||||
"Gelir Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Haz\u0131r De\u011ferler": {
|
||||
"Al\u0131nan \u00c7ekler": {},
|
||||
"Bankalar": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Di\u011fer Haz\u0131r De\u011ferler": {},
|
||||
"Kasa": {
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Verilen \u00c7ekler ve \u00d6deme Emirleri(-)": {}
|
||||
},
|
||||
"Menkul K\u0131ymetler": {
|
||||
"Di\u011fer Menkul K\u0131ymetler": {},
|
||||
"Hisse Senetleri": {},
|
||||
"Kamu Kesimi Tahvil, Senet ve Bonolar\u0131": {},
|
||||
"Menkul K\u0131ymetler De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"\u00d6zel Kesim Tahvil Senet Ve Bonolar\u0131": {}
|
||||
},
|
||||
"Stoklar": {
|
||||
"Mamuller": {},
|
||||
"Stok De\u011fer D\u00fc\u015f\u00fckl\u00fc\u011f\u00fc Kar\u015f\u0131l\u0131\u011f\u0131(-)": {},
|
||||
"Ticari Mallar": {},
|
||||
"Verilen Sipari\u015f Avanslar\u0131": {},
|
||||
"Yar\u0131 Mamuller": {},
|
||||
"\u0130lk Madde Malzeme": {}
|
||||
},
|
||||
"Ticari Alacaklar": {
|
||||
"Alacak Senetleri": {},
|
||||
"Alacak Senetleri Reeskontu(-)": {},
|
||||
"Al\u0131c\u0131lar": {},
|
||||
"Di\u011fer Ticari Alacaklar": {},
|
||||
"Kazan\u0131lmam\u0131\u015f Finansal Kiralama Faiz Gelirleri(-)": {},
|
||||
"Verilen Depozito ve Teminatlar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar": {},
|
||||
"\u015e\u00fcpheli Ticari Alacaklar Kar\u015f\u0131l\u0131\u011f\u0131": {}
|
||||
},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat ve Onar\u0131m Maliyetleri": {
|
||||
"Ta\u015feronlara Verilen Avanslar": {},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Maliyetleri": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"Gelir Tablosu Hesaplar\u0131": {
|
||||
"Br\u00fct Sat\u0131\u015flar": {
|
||||
"Di\u011fer Gelirler": {},
|
||||
"Yurt D\u0131\u015f\u0131 Sat\u0131\u015flar": {},
|
||||
"Yurt \u0130\u00e7i Sat\u0131\u015flar": {}
|
||||
},
|
||||
"Di\u011fer Faaliyetlerden Olu\u015fan Gelir ve K\u00e2rlar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klardan Temett\u00fc Gelirleri": {},
|
||||
"Di\u011fer Ola\u011fan Gelir Ve K\u00e2rlar": {},
|
||||
"Enflasyon D\u00fczeltme K\u00e2rlar\u0131": {},
|
||||
"Faiz Gelirleri": {},
|
||||
"Kambiyo K\u00e2rlar\u0131": {},
|
||||
"Komisyon Gelirleri": {},
|
||||
"Konusu Kalmayan Kar\u015f\u0131l\u0131klar": {},
|
||||
"Menkul K\u0131ymet Sat\u0131\u015f K\u00e2rlar\u0131": {},
|
||||
"Reeskont Faiz Gelirleri": {},
|
||||
"\u0130\u015ftiraklerden Temett\u00fc Gelirleri": {}
|
||||
},
|
||||
"Di\u011fer Faaliyetlerden Olu\u015fan Gider ve Zararlar (-)": {
|
||||
"Di\u011fer Ola\u011fan Gider Ve Zararlar(-)": {},
|
||||
"Enflasyon D\u00fczeltmesi Zararlar\u0131(-)": {},
|
||||
"Kambiyo Zararlar\u0131(-)": {},
|
||||
"Kar\u015f\u0131l\u0131k Giderleri(-)": {},
|
||||
"Komisyon Giderleri(-)": {},
|
||||
"Menkul K\u0131ymet Sat\u0131\u015f Zararlar\u0131(-)": {},
|
||||
"Reeskont Faiz Giderleri(-)": {}
|
||||
},
|
||||
"D\u00f6nem Net K\u00e2r\u0131 Ve Zarar\u0131": {
|
||||
"D\u00f6nem K\u00e2r\u0131 Vergi Ve Di\u011fer Yasal Y\u00fck\u00fcml\u00fcl\u00fck Kar\u015f\u0131l\u0131klar\u0131(-)": {},
|
||||
"D\u00f6nem K\u00e2r\u0131 Veya Zarar\u0131": {},
|
||||
"D\u00f6nem Net K\u00e2r\u0131 Veya Zarar\u0131": {},
|
||||
"Enflasyon D\u00fczeltme Hesab\u0131": {},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Enflasyon D\u00fczeltme Hesab\u0131": {}
|
||||
},
|
||||
"Faaliyet Giderleri(-)": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri(-)": {},
|
||||
"Genel Y\u00f6netim Giderleri(-)": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri(-)": {}
|
||||
},
|
||||
"Finansman Giderleri": {
|
||||
"K\u0131sa Vadeli Bor\u00e7lanma Giderleri(-)": {},
|
||||
"Uzun Vadeli Bor\u00e7lanma Giderleri(-)": {}
|
||||
},
|
||||
"Ola\u011fan D\u0131\u015f\u0131 Gelir Ve K\u00e2rlar": {
|
||||
"Di\u011fer Ola\u011fan D\u0131\u015f\u0131 Gelir Ve K\u00e2rlar": {},
|
||||
"\u00d6nceki D\u00f6nem Gelir Ve K\u00e2rlar\u0131": {}
|
||||
},
|
||||
"Ola\u011fan D\u0131\u015f\u0131 Gider Ve Zaralar(-)": {
|
||||
"Di\u011fer Ola\u011fan D\u0131\u015f\u0131 Gider Ve Zararlar(-)": {},
|
||||
"\u00c7al\u0131\u015fmayan K\u0131s\u0131m Gider Ve Zararlar\u0131(-)": {},
|
||||
"\u00d6nceki D\u00f6nem Gider Ve Zararlar\u0131(-)": {}
|
||||
},
|
||||
"Sat\u0131\u015f \u0130ndirimleri (-)": {
|
||||
"Di\u011fer \u0130ndirimler": {},
|
||||
"Sat\u0131\u015f \u0130ndirimleri(-)": {},
|
||||
"Sat\u0131\u015ftan \u0130adeler(-)": {}
|
||||
},
|
||||
"Sat\u0131\u015flar\u0131n Maliyeti(-)": {
|
||||
"Di\u011fer Sat\u0131\u015flar\u0131n Maliyeti(-)": {},
|
||||
"Sat\u0131lan Hizmet Maliyeti(-)": {},
|
||||
"Sat\u0131lan Mamuller Maliyeti(-)": {},
|
||||
"Sat\u0131lan Ticari Mallar Maliyeti(-)": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"K\u0131sa Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Al\u0131nan Avanslar": {
|
||||
"Al\u0131nan Di\u011fer Avanslar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Al\u0131nan Sipari\u015f Avanslar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"Di\u011fer Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"D\u00f6nem K\u00e2r\u0131 Vergi Ve Di\u011fer Yasal Y\u00fck\u00fcml\u00fcl\u00fck Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"D\u00f6nem K\u00e2r\u0131n\u0131n Pe\u015fin \u00d6denen Vergi Ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler(-)": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"K\u0131dem Tazminat\u0131 Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"Maliyet Giderleri Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7lar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ortaklara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Personele Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u0130\u015ftiraklere Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Di\u011fer K\u0131sa Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Di\u011fer KDV": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Yabanc\u0131 Kaynaklar": {},
|
||||
"Hesaplanan KDV": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Merkez Ve \u015eubeler Cari Hesab\u0131": {},
|
||||
"Say\u0131m Ve Tesell\u00fcm Fazlalar\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Aylara Ait Gelirler Ve Gider Tahakkuklar\u0131": {
|
||||
"Gelecek Aylara Ait Gelirler": {},
|
||||
"Gider Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Mali Bor\u00e7lar": {
|
||||
"Banka Kredileri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Mali Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ertelenmi\u015f Finansal Kiralama Bor\u00e7lanma Maliyetleri(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Finansal Kiralama \u0130\u015flemlerinden Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Menkul K\u0131ymetler \u0130hra\u00e7 Fark\u0131(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Tahvil Anapara Bor\u00e7, Taksit Ve Faizleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Uzun Vadeli Kredilerin Anapara Taksitleri Ve Faizleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u00c7\u0131kar\u0131lan Bonolar Ve Senetler": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Di\u011fer Menkul K\u0131ymetler": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Ticari Bor\u00e7lar": {
|
||||
"Al\u0131nan Depozito Ve Teminatlar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Ticari Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Sat\u0131c\u0131lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Hakedi\u015fleri": {
|
||||
"350 Y\u0131llara Yayg\u0131n \u0130n\u015faat Ve Onar\u0131m Hakedi\u015fleri Bedelleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6denecek Vergi ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"Vadesi Ge\u00e7mi\u015f, Ertelenmi\u015f Veya Taksitlendirilmi\u015f Vergi Ve Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_type": "Tax",
|
||||
"\u00d6denecek Di\u011fer Y\u00fck\u00fcml\u00fcl\u00fckler": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"\u00d6denecek Sosyal G\u00fcvenl\u00fck Kesintileri": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"\u00d6denecek Vergi Ve Fonlar": {
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Maliyet Hesaplar\u0131": {
|
||||
"Ara\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Giderleri": {
|
||||
"Direk \u0130lk Madde Ve Malzeme Giderleri Hesab\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Fiyat Fark\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Miktar Fark\u0131": {},
|
||||
"Direkt \u0130lk Madde Ve Malzeme Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri": {
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik Giderleri Yans\u0131tma Hesab\u0131": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik S\u00fcre Farklar\u0131": {},
|
||||
"Direkt \u0130\u015f\u00e7ilik \u00dccret Farklar\u0131": {}
|
||||
},
|
||||
"Finansman Giderleri": {
|
||||
"Finansman Giderleri": {},
|
||||
"Finansman Giderleri Fark Hesab\u0131": {},
|
||||
"Finansman Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Genel Y\u00f6netim Giderleri": {
|
||||
"Genel Y\u00f6netim Gider Farklar\u0131 Hesab\u0131": {},
|
||||
"Genel Y\u00f6netim Giderleri": {},
|
||||
"Genel Y\u00f6netim Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Genel \u00dcretim Giderleri": {
|
||||
"Genel \u00dcretim Giderleri": {},
|
||||
"Genel \u00dcretim Giderleri B\u00fct\u00e7e Farklar\u0131": {},
|
||||
"Genel \u00dcretim Giderleri Kapasite Farklar\u0131": {},
|
||||
"Genel \u00dcretim Giderleri Verimlilik Giderleri": {},
|
||||
"Genel \u00dcretim Giderleri Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Hizmet \u00dcretim Maliyeti": {
|
||||
"Hizmet \u00dcretim Maliyeti": {},
|
||||
"Hizmet \u00dcretim Maliyeti Fark Hesaplar\u0131": {},
|
||||
"Hizmet \u00dcretim Maliyeti Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Maliyet Muhasebesi Ba\u011flant\u0131 Hesaplar\u0131": {
|
||||
"Maliyet Muhasebesi Ba\u011flant\u0131 Hesab\u0131": {},
|
||||
"Maliyet Muhasebesi Yans\u0131tma Hesab\u0131": {}
|
||||
},
|
||||
"Pazarlama, Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri": {
|
||||
"Atra\u015ft\u0131rma Ve Geli\u015ftirme Giderleri": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Dag\u0131t\u0131m Giderleri Yans\u0131tma Hesab\u0131": {},
|
||||
"Pazarlama Sat\u0131\u015f Ve Da\u011f\u0131t\u0131m Giderleri Fark Hesab\u0131": {}
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"Naz\u0131m Hesaplar": {
|
||||
"root_type": ""
|
||||
},
|
||||
"Serbest Hesaplar": {
|
||||
"root_type": ""
|
||||
},
|
||||
"Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Al\u0131nan Avanslar": {
|
||||
"Al\u0131nan Di\u011fer Avanslar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Al\u0131nan Sipari\u015f Avanslar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"Di\u011fer Bor\u00e7 Ve Gider Kar\u015f\u0131l\u0131klar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"K\u0131dem Tazminat\u0131 Kar\u015f\u0131l\u0131\u011f\u0131": {},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7lar": {
|
||||
"Ba\u011fl\u0131 Ortakl\u0131klara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer \u00c7e\u015fitli Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Kamuya Olan Ertelenmi\u015f Veya Taksitlendirilmi\u015f Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ortaklara Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u0130\u015ftiraklere Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Di\u011fer Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"Di\u011fer \u00c7e\u015fitli Uzun Vadeli Yabanc\u0131 Kaynaklar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Y\u0131llara Ertelenmi\u015f Veya Terkin Edilecek KDV": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Tesise Kat\u0131lma Paylar\u0131": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Gelecek Y\u0131llara Ait Gelirler Ve Gider Tahakkuklar\u0131": {
|
||||
"Gelecek Y\u0131llara Ait Gelirler": {},
|
||||
"Gider Tahakkuklar\u0131": {}
|
||||
},
|
||||
"Mali Bor\u00e7lar": {
|
||||
"Banka Kredileri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Mali Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Ertelenmi\u015f Finansal Kiralama Bor\u00e7lanma Maliyetleri(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Finansal Kiralama \u0130\u015flemlerinden Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Menkul K\u0131ymetler \u0130hra\u00e7 Fark\u0131(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable",
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Di\u011fer Menkul K\u0131ymetler": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"\u00c7\u0131kar\u0131lm\u0131\u015f Tahviller": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Ticari Bor\u00e7lar": {
|
||||
"Al\u0131nan Depozito Ve Teminatlar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Bor\u00e7 Senetleri Reeskontu(-)": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Di\u011fer Ticari Bor\u00e7lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Sat\u0131c\u0131lar": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"root_type": ""
|
||||
},
|
||||
"\u00d6z Kaynaklar": {
|
||||
"D\u00f6nem Net K\u00e2r\u0131 (Zarar\u0131)": {
|
||||
"D\u00f6nem Net K\u00e2r\u0131": {},
|
||||
"D\u00f6nem Net Zarar\u0131(-)": {}
|
||||
},
|
||||
"Ge\u00e7mi\u015f Y\u0131llar K\u00e2rlar\u0131": {
|
||||
"Ge\u00e7mi\u015f Y\u0131llar K\u00e2rlar\u0131": {}
|
||||
},
|
||||
"Ge\u00e7mi\u015f Y\u0131llar Zararlar\u0131(-)": {
|
||||
"Ge\u00e7mi\u015f Y\u0131llar Zararlar\u0131(-)": {}
|
||||
},
|
||||
"K\u00e2r Yedekleri": {
|
||||
"Di\u011fer K\u00e2r Yedekleri": {},
|
||||
"Ola\u011fan\u00fcst\u00fc Yedekler": {},
|
||||
"Stat\u00fc Yedekleri": {},
|
||||
"Yasal Yedekler": {},
|
||||
"\u00d6zel Fonlar": {}
|
||||
},
|
||||
"Sermaye Yedekleri": {
|
||||
"Di\u011fer Sermaye Yedekleri": {},
|
||||
"Hisse Senedi \u0130ptal K\u00e2rlar\u0131": {},
|
||||
"Hisse Senetleri \u0130hra\u00e7 Primleri": {},
|
||||
"Maddi Duran Varl\u0131k Yeniden De\u011ferlenme Art\u0131\u015flar\u0131": {},
|
||||
"Maliyet Art\u0131\u015flar\u0131 Fonu": {},
|
||||
"\u0130\u015ftirakler Yeniden De\u011ferleme Art\u0131\u015flar\u0131": {}
|
||||
},
|
||||
"root_type": "",
|
||||
"\u00d6denmi\u015f Sermaye": {
|
||||
"Sermaye": {},
|
||||
"\u00d6denmi\u015f Sermaye(-)": {
|
||||
"account_type": "Payable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1525,7 +1525,8 @@
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
"Clients cr\u00e9diteurs": {
|
||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
||||
"account_number": "4191"
|
||||
"account_number": "4191",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||
"account_number": "4196"
|
||||
@@ -3141,4 +3142,4 @@
|
||||
"account_number": "7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,23 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
enable_immutable_ledger: function (frm) {
|
||||
if (!frm.doc.enable_immutable_ledger) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = __("Enabling this will change the way how cancelled transactions are handled.");
|
||||
msg += " ";
|
||||
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||
msg += "<br>";
|
||||
msg += "Do you still want to enable immutable ledger?";
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
() => {},
|
||||
() => {
|
||||
frm.set_value("enable_immutable_ledger", 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"enable_immutable_ledger",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
@@ -54,6 +55,8 @@
|
||||
"post_change_gl_entries",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
@@ -70,7 +73,9 @@
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
"receivable_payable_remarks_length",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -105,7 +110,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
|
||||
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
|
||||
"fieldname": "check_supplier_invoice_uniqueness",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@@ -454,6 +459,36 @@
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well",
|
||||
"fieldname": "enable_immutable_ledger",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Immutable Ledger"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gjcc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this option to calculate daily depreciation by considering the total number of days in the entire depreciation period, (including leap years) while using daily pro-rata based depreciation",
|
||||
"fieldname": "calculate_depr_using_total_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -461,7 +496,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.568638",
|
||||
"modified": "2024-07-26 06:48:52.714630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -33,12 +33,15 @@ class AccountsSettings(Document):
|
||||
book_deferred_entries_based_on: DF.Literal["Days", "Months"]
|
||||
book_deferred_entries_via_journal_entry: DF.Check
|
||||
book_tax_discount_loss: DF.Check
|
||||
calculate_depr_using_total_days: DF.Check
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
|
||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -96,8 +96,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||
if (!frm.doc.bank_account) {
|
||||
frappe.msgprint(__("Please select Bank Account"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
@@ -118,7 +119,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.922473",
|
||||
"modified": "2024-04-28 14:40:50.910884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
|
||||
@@ -495,12 +495,12 @@ def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
|
||||
@@ -540,14 +540,14 @@ def get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
exact_match=None,
|
||||
common_filters=None,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
exact_match=None,
|
||||
account_from_to=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
common_filters=None,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
@@ -719,7 +719,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -130,52 +130,66 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
},
|
||||
|
||||
show_import_status(frm) {
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
let successful_records = import_log.filter((log) => log.success);
|
||||
let failed_records = import_log.filter((log) => !log.success);
|
||||
if (successful_records.length === 0) return;
|
||||
if (frm.doc.status == "Pending") return;
|
||||
|
||||
let message;
|
||||
if (failed_records.length === 0) {
|
||||
let message_args = [successful_records.length];
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __("Successfully imported {0} records.", message_args)
|
||||
: __("Successfully imported {0} record.", message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __("Successfully updated {0} records.", message_args)
|
||||
: __("Successfully updated {0} record.", message_args);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records.length, import_log.length];
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __(
|
||||
"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
)
|
||||
: __(
|
||||
"Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __(
|
||||
"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
)
|
||||
: __(
|
||||
"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
);
|
||||
}
|
||||
}
|
||||
frm.dashboard.set_headline(message);
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.get_import_status",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
let successful_records = cint(r.message.success);
|
||||
let failed_records = cint(r.message.failed);
|
||||
let total_records = cint(r.message.total_records);
|
||||
|
||||
if (!total_records) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (failed_records === 0) {
|
||||
let message_args = [successful_records];
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __("Successfully imported {0} records.", message_args)
|
||||
: __("Successfully imported {0} record.", message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __("Successfully updated {0} records.", message_args)
|
||||
: __("Successfully updated {0} record.", message_args);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records, total_records];
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __(
|
||||
"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
)
|
||||
: __(
|
||||
"Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __(
|
||||
"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
)
|
||||
: __(
|
||||
"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
|
||||
message_args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
frm.dashboard.set_headline(message);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
@@ -297,7 +311,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||
|
||||
show_import_preview(frm, preview_data) {
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
let import_log = preview_data.import_log;
|
||||
|
||||
if (frm.import_preview && frm.import_preview.doctype === frm.doc.reference_doctype) {
|
||||
frm.import_preview.preview_data = preview_data;
|
||||
@@ -336,6 +350,15 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
);
|
||||
},
|
||||
|
||||
export_import_log(frm) {
|
||||
open_url_post(
|
||||
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_import_log",
|
||||
{
|
||||
data_import_name: frm.doc.name,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
show_import_warnings(frm, preview_data) {
|
||||
let columns = preview_data.columns;
|
||||
let warnings = JSON.parse(frm.doc.template_warnings || "[]");
|
||||
@@ -411,49 +434,50 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
frm.trigger("show_import_log");
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
let logs = import_log;
|
||||
frm.toggle_display("import_log", false);
|
||||
frm.toggle_display("import_log_section", logs.length > 0);
|
||||
render_import_log(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.get_import_logs",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
let logs = r.message;
|
||||
|
||||
if (logs.length === 0) {
|
||||
frm.get_field("import_log_preview").$wrapper.empty();
|
||||
return;
|
||||
}
|
||||
if (logs.length === 0) return;
|
||||
|
||||
let rows = logs
|
||||
.map((log) => {
|
||||
let html = "";
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
html = __("Successfully imported {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
} else {
|
||||
html = __("Successfully updated {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = log.messages
|
||||
.map(JSON.parse)
|
||||
.map((m) => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : "";
|
||||
let message = m.message ? `<div>${m.message}</div>` : "";
|
||||
return title + message;
|
||||
})
|
||||
.join("");
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
frm.toggle_display("import_log_section", true);
|
||||
|
||||
let rows = logs
|
||||
.map((log) => {
|
||||
let html = "";
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === "Insert New Records") {
|
||||
html = __("Successfully imported {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
} else {
|
||||
html = __("Successfully updated {0}", [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = JSON.parse(log.messages || "[]")
|
||||
.map((m) => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : "";
|
||||
let message = m.message ? `<div>${m.message}</div>` : "";
|
||||
return title + message;
|
||||
})
|
||||
.join("");
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__("Show Traceback")}
|
||||
</button>
|
||||
@@ -462,16 +486,16 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
let indicator_color = log.success ? "green" : "red";
|
||||
let title = log.success ? __("Success") : __("Failure");
|
||||
}
|
||||
let indicator_color = log.success ? "green" : "red";
|
||||
let title = log.success ? __("Success") : __("Failure");
|
||||
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return "";
|
||||
}
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${log.row_indexes.join(", ")}</td>
|
||||
return `<tr>
|
||||
<td>${JSON.parse(log.row_indexes).join(", ")}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
@@ -479,16 +503,16 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
})
|
||||
.join("");
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__("No failed logs")}
|
||||
</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
frm.get_field("import_log_preview").$wrapper.html(`
|
||||
frm.get_field("import_log_preview").$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__("Row Number")}</th>
|
||||
@@ -498,5 +522,34 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
${rows}
|
||||
</table>
|
||||
`);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
frm.toggle_display("import_log_section", false);
|
||||
|
||||
if (frm.is_new() || frm.import_in_progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_count",
|
||||
args: {
|
||||
doctype: "Data Import Log",
|
||||
filters: {
|
||||
data_import: frm.doc.name,
|
||||
},
|
||||
},
|
||||
callback: function (r) {
|
||||
let count = r.message;
|
||||
if (count < 5000) {
|
||||
frm.trigger("render_import_log");
|
||||
} else {
|
||||
frm.toggle_display("import_log_section", false);
|
||||
frm.add_custom_button(__("Export Import Log"), () => frm.trigger("export_import_log"));
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"bank_account",
|
||||
"bank",
|
||||
"column_break_4",
|
||||
"custom_delimiters",
|
||||
"delimiter_options",
|
||||
"google_sheets_url",
|
||||
"refresh_google_sheet",
|
||||
"html_5",
|
||||
@@ -24,7 +26,6 @@
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"statement_import_log",
|
||||
"show_failed_logs",
|
||||
"import_log_preview",
|
||||
"reference_doctype",
|
||||
@@ -194,15 +195,23 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "statement_import_log",
|
||||
"fieldtype": "Code",
|
||||
"label": "Statement Import Log",
|
||||
"options": "JSON"
|
||||
"default": "0",
|
||||
"fieldname": "custom_delimiters",
|
||||
"fieldtype": "Check",
|
||||
"label": "Custom delimiters"
|
||||
},
|
||||
{
|
||||
"default": ",;\\t|",
|
||||
"depends_on": "custom_delimiters",
|
||||
"description": "If your CSV uses a different delimiter, add that character here, ensuring no spaces or additional characters are included.",
|
||||
"fieldname": "delimiter_options",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delimiter options"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:38.098765",
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
|
||||
@@ -31,13 +31,14 @@ class BankStatementImport(DataImport):
|
||||
bank: DF.Link | None
|
||||
bank_account: DF.Link
|
||||
company: DF.Link
|
||||
custom_delimiters: DF.Check
|
||||
delimiter_options: DF.Data | None
|
||||
google_sheets_url: DF.Data | None
|
||||
import_file: DF.Attach | None
|
||||
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
|
||||
mute_emails: DF.Check
|
||||
reference_doctype: DF.Link
|
||||
show_failed_logs: DF.Check
|
||||
statement_import_log: DF.Code | None
|
||||
status: DF.Literal["Pending", "Success", "Partial Success", "Error"]
|
||||
submit_after_import: DF.Check
|
||||
template_options: DF.Code | None
|
||||
@@ -121,6 +122,11 @@ def download_errored_template(data_import_name):
|
||||
data_import.export_errored_rows()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name):
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
@@ -242,6 +248,47 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_status(docname):
|
||||
import_status = {}
|
||||
|
||||
data_import = frappe.get_doc("Bank Statement Import", docname)
|
||||
import_status["status"] = data_import.status
|
||||
|
||||
logs = frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["count(*) as count", "success"],
|
||||
filters={"data_import": docname},
|
||||
group_by="success",
|
||||
)
|
||||
|
||||
total_payload_count = 0
|
||||
|
||||
for log in logs:
|
||||
total_payload_count += log.get("count", 0)
|
||||
if log.get("success"):
|
||||
import_status["success"] = log.get("count")
|
||||
else:
|
||||
import_status["failed"] = log.get("count")
|
||||
|
||||
import_status["total_records"] = total_payload_count
|
||||
|
||||
return import_status
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_logs(docname: str):
|
||||
frappe.has_permission("Bank Statement Import")
|
||||
|
||||
return frappe.get_all(
|
||||
"Data Import Log",
|
||||
fields=["success", "docname", "messages", "exception", "row_indexes"],
|
||||
filters={"data_import": docname},
|
||||
limit_page_length=5000,
|
||||
order_by="log_index",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upload_bank_statement(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if "bank_ac_no" in or_filters:
|
||||
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
|
||||
@@ -47,9 +47,11 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 8:
|
||||
if no_of_columns != 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
_(
|
||||
"Columns are not according to template. Please compare the uploaded file with standard template"
|
||||
),
|
||||
title=(_("Wrong Template")),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, format_date, getdate
|
||||
from frappe.utils import add_days, flt, format_date, getdate
|
||||
|
||||
|
||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
|
||||
self.validate_child_cost_centers()
|
||||
|
||||
def validate_total_allocation_percentage(self):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"customer",
|
||||
"column_break_4",
|
||||
"coupon_code",
|
||||
"from_external_ecomm_platform",
|
||||
"pricing_rule",
|
||||
"uses",
|
||||
"valid_from",
|
||||
@@ -61,11 +62,12 @@
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval !doc.from_external_ecomm_platform",
|
||||
"fieldname": "pricing_rule",
|
||||
"fieldtype": "Link",
|
||||
"label": "Pricing Rule",
|
||||
"options": "Pricing Rule",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: !doc.from_external_ecomm_platform",
|
||||
"options": "Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "uses",
|
||||
@@ -114,13 +116,20 @@
|
||||
"options": "Coupon Code",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "from_external_ecomm_platform",
|
||||
"fieldtype": "Check",
|
||||
"label": "From External Ecomm Platform"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:47.220931",
|
||||
"modified": "2024-06-28 06:17:01.833399",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Coupon Code",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -177,4 +186,4 @@
|
||||
"states": [],
|
||||
"title_field": "coupon_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ class CouponCode(Document):
|
||||
coupon_type: DF.Literal["Promotional", "Gift Card"]
|
||||
customer: DF.Link | None
|
||||
description: DF.TextEditor | None
|
||||
from_external_ecomm_platform: DF.Check
|
||||
maximum_use: DF.Int
|
||||
pricing_rule: DF.Link
|
||||
pricing_rule: DF.Link | None
|
||||
used: DF.Int
|
||||
valid_from: DF.Date | None
|
||||
valid_upto: DF.Date | None
|
||||
|
||||
@@ -142,3 +142,39 @@ class TestCouponCode(unittest.TestCase):
|
||||
|
||||
so.submit()
|
||||
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
|
||||
|
||||
def test_coupon_without_max_use(self):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
update_coupon_code_count,
|
||||
validate_coupon_code,
|
||||
)
|
||||
|
||||
coupon = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Coupon Code",
|
||||
"coupon_name": "_Test Coupon Without Max Use",
|
||||
"coupon_code": "TESTUNLIMITED",
|
||||
"from_external_ecomm_platform": 1, # avoids requirement for pricing rule
|
||||
"valid_from": frappe.utils.nowdate(),
|
||||
"maximum_use": 0,
|
||||
"used": 0,
|
||||
}
|
||||
)
|
||||
coupon.insert(ignore_permissions=True)
|
||||
|
||||
# Validate initial state
|
||||
self.assertEqual(coupon.used, 0)
|
||||
self.assertEqual(coupon.maximum_use, 0)
|
||||
|
||||
# Use coupon multiple times
|
||||
for _ in range(5):
|
||||
validate_coupon_code(coupon.name)
|
||||
update_coupon_code_count(coupon.name, "used")
|
||||
coupon.reload()
|
||||
|
||||
# Check that the coupon is still valid and usage count increased
|
||||
self.assertEqual(coupon.used, 5)
|
||||
validate_coupon_code(coupon.name) # This should not raise an error
|
||||
|
||||
# Clean up
|
||||
coupon.delete()
|
||||
|
||||
@@ -148,7 +148,19 @@ class Dunning(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = [
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
@@ -181,10 +193,13 @@ def resolve_dunning(doc, state):
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
|
||||
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
|
||||
|
||||
dunning.status = "Resolved" if resolve else "Unresolved"
|
||||
dunning.save()
|
||||
new_status = "Resolved" if resolve else "Unresolved"
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
|
||||
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
},
|
||||
|
||||
make_jv: function (frm) {
|
||||
let revaluation_journal = null;
|
||||
let zero_balance_journal = null;
|
||||
frappe.call({
|
||||
method: "make_jv_entries",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: "Making Journal Entries...",
|
||||
freeze_message: __("Creating Journal Entries..."),
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let response = r.message;
|
||||
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
|
||||
frappe.msgprint(__("Journals have been created"));
|
||||
frappe.msgprint(__("Journal entries have been created"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -127,6 +127,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -25,30 +25,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
refresh: function (frm) {
|
||||
erpnext.toggle_naming_series();
|
||||
|
||||
if (frm.doc.repost_required && frm.doc.docstatus === 1) {
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
frm.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted."));
|
||||
frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Ledger"),
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"stock_entry",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
"repost_required"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -544,15 +543,6 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -567,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:09:58.366953",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -47,9 +47,7 @@ class JournalEntry(AccountsController):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import (
|
||||
JournalEntryAccount,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -194,15 +192,13 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"accounts": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
@@ -225,6 +221,7 @@ class JournalEntry(AccountsController):
|
||||
self.unlink_inter_company_jv()
|
||||
self.unlink_asset_adjustment_entry()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation(1)
|
||||
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
@@ -442,6 +439,25 @@ class JournalEntry(AccountsController):
|
||||
if status:
|
||||
inv_disc_doc.set_status(status=status)
|
||||
|
||||
def update_booked_depreciation(self, cancel=0):
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
if cancel:
|
||||
fb_row.total_number_of_booked_depreciations -= 1
|
||||
else:
|
||||
fb_row.total_number_of_booked_depreciations += 1
|
||||
fb_row.db_update()
|
||||
break
|
||||
|
||||
def unlink_advance_entry_reference(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
@@ -457,7 +473,7 @@ class JournalEntry(AccountsController):
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
@@ -1034,6 +1050,17 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
|
||||
@@ -454,12 +454,9 @@ class TestJournalEntry(unittest.TestCase):
|
||||
# Change cost center for bank account - _Test Cost Center for BS Account
|
||||
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
||||
jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
jv.save()
|
||||
|
||||
# Check if repost flag gets set on update after submit
|
||||
self.assertTrue(jv.repost_required)
|
||||
jv.repost_accounting_entries()
|
||||
|
||||
# Check GL entries after reposting
|
||||
jv.load_from_db()
|
||||
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
|
||||
@@ -481,6 +478,43 @@ class TestJournalEntry(unittest.TestCase):
|
||||
for field in self.fields:
|
||||
self.assertEqual(self.expected_gle[i][field], gl_entries[i][field])
|
||||
|
||||
def test_negative_debit_and_credit_with_same_account_head(self):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Bank - _TC", 100 * -1, save=True)
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit": 100 * -1,
|
||||
"credit": 100 * -1,
|
||||
"debit_in_account_currency": 100 * -1,
|
||||
"credit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
jv.flags.ignore_validate = True
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
gl_map = jv.build_gl_map()
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||
self.assertEqual(row.credit_in_account_currency, 100 * -1)
|
||||
|
||||
gl_map = process_gl_map(gl_map, False)
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -36,7 +36,17 @@ class LoyaltyProgram(Document):
|
||||
to_date: DF.Date | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def validate(self):
|
||||
self.validate_lowest_tier()
|
||||
|
||||
def validate_lowest_tier(self):
|
||||
tiers = sorted(self.collection_rules, key=lambda x: x.min_spent)
|
||||
if tiers and tiers[0].min_spent != 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"The lowest tier must have a minimum spent amount of 0. Customers need to be part of a tier as soon as they are enrolled in the program."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_loyalty_details(
|
||||
@@ -79,17 +89,17 @@ def get_loyalty_program_details_with_points(
|
||||
):
|
||||
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)
|
||||
loyalty_details = get_loyalty_details(
|
||||
customer, loyalty_program.name, expiry_date, company, include_expired_entry
|
||||
)
|
||||
lp_details.update(loyalty_details)
|
||||
|
||||
tier_spent_level = sorted(
|
||||
[d.as_dict() for d in loyalty_program.collection_rules],
|
||||
key=lambda rule: rule.min_spent,
|
||||
reverse=True,
|
||||
)
|
||||
for i, d in enumerate(tier_spent_level):
|
||||
if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent:
|
||||
if i == 0 or (lp_details.total_spent + current_transaction_amount) >= d.min_spent:
|
||||
lp_details.tier_name = d.tier_name
|
||||
lp_details.collection_factor = d.collection_factor
|
||||
else:
|
||||
@@ -173,6 +183,8 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
|
||||
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
|
||||
ref_doc.loyalty_amount = loyalty_amount
|
||||
if not ref_doc.loyalty_points and ref_doc.loyalty_points != points_to_redeem:
|
||||
ref_doc.loyalty_points = points_to_redeem
|
||||
|
||||
if ref_doc.doctype == "Sales Invoice":
|
||||
ref_doc.loyalty_program = loyalty_program
|
||||
|
||||
@@ -7,6 +7,7 @@ import frappe
|
||||
from frappe.utils import cint, flt, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_details,
|
||||
get_loyalty_program_details_with_points,
|
||||
)
|
||||
from erpnext.accounts.party import get_dashboard_info
|
||||
@@ -38,6 +39,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
|
||||
self.assertEqual(lpe.get("loyalty_program_tier"), "Bronce") # is always in the first tier
|
||||
self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
|
||||
self.assertEqual(lpe.loyalty_points, earned_points)
|
||||
|
||||
@@ -79,6 +81,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
si_original = create_sales_invoice_record()
|
||||
si_original.insert()
|
||||
si_original.submit()
|
||||
customer.reload()
|
||||
|
||||
earned_points = get_points_earned(si_original)
|
||||
|
||||
@@ -101,8 +104,8 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
si_redeem.loyalty_points = earned_points
|
||||
si_redeem.insert()
|
||||
si_redeem.submit()
|
||||
customer.reload()
|
||||
|
||||
customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
|
||||
earned_after_redemption = get_points_earned(si_redeem)
|
||||
|
||||
lpe_redeem = frappe.get_doc(
|
||||
@@ -197,6 +200,70 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
for d in company_wise_info:
|
||||
self.assertTrue(d.get("loyalty_points"))
|
||||
|
||||
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
|
||||
def test_tier_selection(self, mock_get_loyalty_details):
|
||||
# Create a new loyalty program with multiple tiers
|
||||
loyalty_program = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Loyalty Program",
|
||||
"loyalty_program_name": "Test Tier Selection",
|
||||
"auto_opt_in": 1,
|
||||
"from_date": today(),
|
||||
"loyalty_program_type": "Multiple Tier Program",
|
||||
"conversion_factor": 1,
|
||||
"expiry_duration": 10,
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [
|
||||
{"tier_name": "Gold", "collection_factor": 1000, "min_spent": 20000},
|
||||
{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
|
||||
{"tier_name": "Bronze", "collection_factor": 1000, "min_spent": 0},
|
||||
],
|
||||
}
|
||||
)
|
||||
loyalty_program.insert()
|
||||
|
||||
# Test cases with different total_spent and current_transaction_amount combinations
|
||||
test_cases = [
|
||||
(0, 6000, "Bronze"),
|
||||
(0, 15000, "Silver"),
|
||||
(0, 25000, "Gold"),
|
||||
(4000, 500, "Bronze"),
|
||||
(8000, 3000, "Silver"),
|
||||
(18000, 3000, "Gold"),
|
||||
(22000, 5000, "Gold"),
|
||||
]
|
||||
|
||||
for total_spent, current_transaction_amount, expected_tier in test_cases:
|
||||
with self.subTest(total_spent=total_spent, current_transaction_amount=current_transaction_amount):
|
||||
# Mock the get_loyalty_details function to update the total_spent
|
||||
def side_effect(*args, **kwargs):
|
||||
result = get_loyalty_details(*args, **kwargs)
|
||||
result.update({"total_spent": total_spent})
|
||||
return result
|
||||
|
||||
mock_get_loyalty_details.side_effect = side_effect
|
||||
|
||||
lp_details = get_loyalty_program_details_with_points(
|
||||
"Test Loyalty Customer",
|
||||
loyalty_program=loyalty_program.name,
|
||||
company="_Test Company",
|
||||
current_transaction_amount=current_transaction_amount,
|
||||
)
|
||||
|
||||
# Get the selected tier based on the current implementation
|
||||
selected_tier = lp_details.tier_name
|
||||
|
||||
self.assertEqual(
|
||||
selected_tier,
|
||||
expected_tier,
|
||||
f"Expected tier {expected_tier} for total_spent {total_spent} and current_transaction_amount {current_transaction_amount}, but got {selected_tier}",
|
||||
)
|
||||
|
||||
# Clean up
|
||||
loyalty_program.delete()
|
||||
|
||||
|
||||
def get_points_earned(self):
|
||||
def get_returned_amount():
|
||||
@@ -285,7 +352,7 @@ def create_records():
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}],
|
||||
"collection_rules": [{"tier_name": "Bronce", "collection_factor": 1000, "min_spent": 0}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -316,6 +383,7 @@ def create_records():
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [
|
||||
{"tier_name": "Bronze", "collection_factor": 1000, "min_spent": 0},
|
||||
{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
|
||||
{"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000},
|
||||
],
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
{
|
||||
"fieldname": "min_spent",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Minimum Total Spent"
|
||||
},
|
||||
{
|
||||
@@ -37,7 +38,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:03.536071",
|
||||
"modified": "2024-09-05 07:41:25.694041",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Loyalty Program Collection",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:05.597711",
|
||||
"modified": "2024-08-16 19:22:42.942264",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Mode of Payment",
|
||||
@@ -74,5 +74,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
"states": [],
|
||||
"translated_doctype": 1
|
||||
}
|
||||
@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -165,8 +173,25 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
@@ -213,7 +238,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
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.toggle_display(
|
||||
@@ -280,7 +305,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.set_currency_labels(
|
||||
@@ -395,6 +420,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +658,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"party",
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -88,6 +89,7 @@
|
||||
"custom_remarks",
|
||||
"remarks",
|
||||
"base_in_words",
|
||||
"is_opening",
|
||||
"column_break_16",
|
||||
"letter_head",
|
||||
"print_heading",
|
||||
@@ -751,6 +753,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -766,6 +769,26 @@
|
||||
"label": "In Words",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "company.reconcile_on_advance_payment_date",
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Reconcile on Advance Payment Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "No",
|
||||
"depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Opening",
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -779,7 +802,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:25:07.366347",
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -37,7 +37,6 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -158,14 +157,12 @@ class PaymentEntry(AccountsController):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_liability_account()
|
||||
self.validate_advance_account_currency()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
@@ -179,6 +176,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()
|
||||
self.set_total_in_words()
|
||||
|
||||
@@ -197,12 +195,15 @@ class PaymentEntry(AccountsController):
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
if self.party_type not in ("Customer", "Supplier"):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||
):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
# Important to set this flag for the gl building logic to work properly
|
||||
@@ -214,6 +215,7 @@ class PaymentEntry(AccountsController):
|
||||
if (account_type == "Payable" and self.party_type == "Customer") or (
|
||||
account_type == "Receivable" and self.party_type == "Supplier"
|
||||
):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
if self.references:
|
||||
@@ -223,6 +225,7 @@ class PaymentEntry(AccountsController):
|
||||
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
|
||||
if reference_types - allowed_types:
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
liability_account = get_party_account(
|
||||
@@ -241,22 +244,6 @@ class PaymentEntry(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def validate_advance_account_currency(self):
|
||||
if self.book_advance_payments_in_separate_party_account is True:
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
|
||||
)
|
||||
)
|
||||
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
|
||||
frappe.throw(
|
||||
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
|
||||
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
@@ -858,9 +845,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(
|
||||
@@ -904,7 +889,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:
|
||||
@@ -912,12 +916,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()
|
||||
@@ -1206,107 +1213,82 @@ class PaymentEntry(AccountsController):
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
amount = self.base_paid_amount
|
||||
else:
|
||||
amount = self.base_received_amount
|
||||
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
amount_in_account_currency = amount * exchange_rate
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: amount,
|
||||
dr_or_cr + "_in_account_currency": amount_in_account_currency,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
else:
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(
|
||||
d.reference_doctype, d.reference_name, "cost_center"
|
||||
)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
||||
d
|
||||
)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1321,7 +1303,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
||||
"""
|
||||
If 'entry' is passed, GL enties only for that reference is added.
|
||||
If 'entry' is passed, GL entries only for that reference is added.
|
||||
"""
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
references = [x for x in self.get("references")]
|
||||
@@ -1333,8 +1315,6 @@ class PaymentEntry(AccountsController):
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Payment Entry",
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
@@ -1343,13 +1323,21 @@ class PaymentEntry(AccountsController):
|
||||
if reference.reference_doctype == "Sales Invoice":
|
||||
return "credit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Purchase Invoice":
|
||||
return "debit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Payment Entry":
|
||||
# reference.account_type and reference.payment_type is only available for Reverse payments
|
||||
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||
return "credit", self.party_account
|
||||
else:
|
||||
return "debit", self.party_account
|
||||
|
||||
return "debit", reference.account
|
||||
if reference.reference_doctype == "Journal Entry":
|
||||
if self.party_type == "Customer" and self.payment_type == "Receive":
|
||||
return "credit", reference.account
|
||||
else:
|
||||
return "debit", reference.account
|
||||
|
||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||
args_dict = {
|
||||
@@ -1362,17 +1350,20 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
if self.reconcile_on_advance_payment_date:
|
||||
posting_date = self.posting_date
|
||||
else:
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
{
|
||||
@@ -1391,7 +1382,7 @@ class PaymentEntry(AccountsController):
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = 0
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
args_dict["account"] = self.party_account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
{
|
||||
@@ -1847,7 +1838,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
|
||||
@@ -82,7 +82,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
@@ -1477,6 +1477,68 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_advance_as_liability_against_order(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as _make_purchase_invoice,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": advance_account,
|
||||
},
|
||||
)
|
||||
|
||||
po = create_purchase_order(supplier="_Test Supplier")
|
||||
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
|
||||
pe.save().submit()
|
||||
|
||||
pre_reconciliation_gle = [
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.expected_gle = pre_reconciliation_gle
|
||||
self.check_gl_entries()
|
||||
|
||||
# Make Purchase Invoice against the order
|
||||
pi = _make_purchase_invoice(po.name)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": pe.doctype,
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 5000,
|
||||
"allocated_amount": 5000,
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
|
||||
# # assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
|
||||
]
|
||||
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
|
||||
def check_pl_entries(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
pl_entries = (
|
||||
@@ -1667,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_opening_flag_for_advance_as_liability(self):
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
parent_account="Current Assets - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# Enable Advance in separate party account
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": advance_account,
|
||||
},
|
||||
)
|
||||
# Advance Payment
|
||||
adv = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
adv.is_opening = "Yes"
|
||||
adv.save() # use save() to trigger set_liability_account()
|
||||
adv.submit()
|
||||
|
||||
gl_with_opening_set = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
|
||||
)
|
||||
# 'Is Opening' can be 'Yes' for Advances in separate party account
|
||||
self.assertNotEqual(gl_with_opening_set, [])
|
||||
|
||||
# Disable Advance in separate party account
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 0,
|
||||
"default_advance_received_account": None,
|
||||
},
|
||||
)
|
||||
payment = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
payment.is_opening = "Yes"
|
||||
payment.save()
|
||||
payment.submit()
|
||||
gl_with_opening_set = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
|
||||
)
|
||||
# 'Is Opening' should always be 'No' for normal advance payments
|
||||
self.assertEqual(gl_with_opening_set, [])
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"label": "Currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_channel == 'Email' || (!doc.payment_channel)",
|
||||
"fieldname": "payment_request_message",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@@ -70,12 +70,12 @@
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone"
|
||||
"options": "\nEmail\nPhone\nOther"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:09.836254",
|
||||
"modified": "2024-03-29 18:53:09.836254",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
@@ -97,4 +97,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +509,11 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"unlink_advance_payment_on_cancelation_of_order": 1,
|
||||
},
|
||||
)
|
||||
def test_advance_payment_unlink_on_order_cancellation(self):
|
||||
transaction_date = nowdate()
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
if (typeof r.message === "string") {
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||
this.frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
this.frm.refresh();
|
||||
},
|
||||
|
||||
@@ -267,6 +267,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}%"))
|
||||
|
||||
@@ -109,6 +109,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
"account_currency": "INR",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
# 'Receivable' account for capturing advance received, under 'Liabilities' group
|
||||
{
|
||||
"attribute": "advance_receivable_account",
|
||||
"account_name": "Advance Received",
|
||||
"parent_account": "Current Liabilities - _PR",
|
||||
"account_currency": "INR",
|
||||
"account_type": "Receivable",
|
||||
},
|
||||
]
|
||||
|
||||
for x in accounts:
|
||||
@@ -1525,6 +1533,318 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_date(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 1500
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.posting_date = add_days(nowdate(), -1)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
pi = self.create_purchase_invoice(qty=10, rate=100)
|
||||
self.assertNotEqual(pe.posting_date, pi.posting_date)
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": self.advance_receivable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
je = self.create_journal_entry(self.debit_to, self.bank, amount)
|
||||
je.accounts[0].cost_center = self.main_cc.name
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je.accounts[1].cost_center = self.main_cc.name
|
||||
je = je.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=amount).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.default_advance_account = self.advance_receivable_account
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
expected_gle = [
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": je.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(gl_entries, expected_gle)
|
||||
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||
order_by="account, against_voucher_no, amount",
|
||||
)
|
||||
expected_ple = [
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": amount,
|
||||
},
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": je.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_supplier(self):
|
||||
self.supplier = make_supplier("_Test Supplier")
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
je = self.create_journal_entry(self.creditors, self.bank, -amount)
|
||||
je.accounts[0].cost_center = self.main_cc.name
|
||||
je.accounts[0].party_type = "Supplier"
|
||||
je.accounts[0].party = self.supplier
|
||||
je.accounts[1].cost_center = self.main_cc.name
|
||||
je = je.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.paid_from = self.bank
|
||||
pe.paid_to = self.creditors
|
||||
pe.party = self.supplier
|
||||
pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0},
|
||||
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
expected_gle = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.creditors,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": je.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(gl_entries, expected_gle)
|
||||
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||
order_by="account, against_voucher_no, amount",
|
||||
)
|
||||
expected_ple = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": amount,
|
||||
},
|
||||
{
|
||||
"account": self.creditors,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": je.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
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):
|
||||
|
||||
119
erpnext/accounts/doctype/payment_request/payment_gateway_v1.py
Normal file
119
erpnext/accounts/doctype/payment_request/payment_gateway_v1.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Compatipility methods for v1 implementations of payment gateways
|
||||
|
||||
"""
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import deprecated, flt
|
||||
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
def _get_payment_controller(*args, **kwargs):
|
||||
with payment_app_import_guard():
|
||||
try:
|
||||
from payments.utils import get_payment_controller
|
||||
except Exception:
|
||||
from payments.utils import get_payment_gateway_controller as get_payment_controller
|
||||
|
||||
return get_payment_controller(*args, **kwargs)
|
||||
|
||||
|
||||
def get_request_amount(self):
|
||||
data_of_completed_requests = frappe.get_all(
|
||||
"Integration Request",
|
||||
filters={
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_docname": self.name,
|
||||
"status": "Completed",
|
||||
},
|
||||
pluck="data",
|
||||
)
|
||||
if not data_of_completed_requests:
|
||||
return self.grand_total
|
||||
request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
|
||||
return request_amounts
|
||||
|
||||
|
||||
def request_phone_payment(self, controller):
|
||||
request_amount = get_request_amount(self)
|
||||
|
||||
payment_record = dict(
|
||||
reference_doctype="Payment Request",
|
||||
reference_docname=self.name,
|
||||
payment_reference=self.reference_name,
|
||||
request_amount=request_amount,
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway,
|
||||
)
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
controller.request_for_payment(**payment_record)
|
||||
|
||||
|
||||
def payment_gateway_validation(self, controller):
|
||||
try:
|
||||
if hasattr(controller, "on_payment_request_submission"):
|
||||
return controller.on_payment_request_submission(self)
|
||||
else:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_payment_url(self, controller):
|
||||
if self.reference_doctype != "Fees":
|
||||
data = frappe.db.get_value(
|
||||
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
|
||||
)
|
||||
else:
|
||||
data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1)
|
||||
data.update({"company": frappe.defaults.get_defaults().company})
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
|
||||
if hasattr(controller, "validate_minimum_transaction_amount"):
|
||||
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)
|
||||
|
||||
return controller.get_payment_url(
|
||||
**{
|
||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||
"title": data.company.encode("utf-8"),
|
||||
"description": self.subject.encode("utf-8"),
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": self.name,
|
||||
"payer_email": self.email_to or frappe.session.user,
|
||||
"payer_name": frappe.safe_encode(data.customer_name),
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def set_payment_request_url(self, controller):
|
||||
if self.payment_account and self.payment_gateway and payment_gateway_validation(self, controller):
|
||||
self.payment_url = get_payment_url(self, controller)
|
||||
|
||||
|
||||
def v1_gateway_before_submit(self, payment_gateway):
|
||||
try:
|
||||
controller = _get_payment_controller(payment_gateway)
|
||||
except Exception:
|
||||
frappe.warnings.warn(f"{payment_gateway} is not a valid gateway; this is normal during tests.")
|
||||
return False
|
||||
if self.payment_channel == "Phone":
|
||||
request_phone_payment(self, controller)
|
||||
else:
|
||||
set_payment_request_url(self, controller)
|
||||
if not (self.mute_email or self.flags.mute_email):
|
||||
self.send_email()
|
||||
self.make_communication_entry()
|
||||
|
||||
|
||||
def v1_create_subscription(payment_provider, gateway_controller, data):
|
||||
if payment_provider == "stripe":
|
||||
with payment_app_import_guard():
|
||||
from payments.payment_gateways.stripe_integration import create_stripe_subscription
|
||||
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
@@ -10,6 +10,7 @@
|
||||
"failed_reason",
|
||||
"column_break_2",
|
||||
"naming_series",
|
||||
"company",
|
||||
"mode_of_payment",
|
||||
"party_details",
|
||||
"party_type",
|
||||
@@ -49,13 +50,14 @@
|
||||
"message",
|
||||
"message_examples",
|
||||
"mute_email",
|
||||
"payment_url",
|
||||
"section_break_7",
|
||||
"payment_gateway",
|
||||
"payment_account",
|
||||
"payment_channel",
|
||||
"payment_order",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"column_break_pnyv",
|
||||
"payment_url"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -145,6 +147,7 @@
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
@@ -260,7 +263,7 @@
|
||||
"label": "Recipient Message And Payment Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Print Format"
|
||||
@@ -272,7 +275,7 @@
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@@ -309,18 +312,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel != \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel == \"Email\" || (!doc.payment_channel)",
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)",
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
@@ -341,7 +344,7 @@
|
||||
{
|
||||
"fieldname": "payment_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Payment URL",
|
||||
"length": 500,
|
||||
"options": "URL",
|
||||
"read_only": 1
|
||||
@@ -372,7 +375,7 @@
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone",
|
||||
"options": "\nEmail\nPhone\nOther",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -399,13 +402,24 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pnyv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:11.120742",
|
||||
"modified": "2024-08-07 16:39:54.288002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -443,4 +457,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
@@ -16,15 +14,10 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
def _get_payment_gateway_controller(*args, **kwargs):
|
||||
with payment_app_import_guard():
|
||||
from payments.utils import get_payment_gateway_controller
|
||||
|
||||
return get_payment_gateway_controller(*args, **kwargs)
|
||||
from .payment_gateway_v1 import v1_create_subscription, v1_gateway_before_submit
|
||||
|
||||
|
||||
class PaymentRequest(Document):
|
||||
@@ -85,6 +78,7 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -158,124 +152,26 @@ class PaymentRequest(Document):
|
||||
# set advance payment status
|
||||
ref_doc.set_advance_payment_status()
|
||||
|
||||
def on_submit(self):
|
||||
def before_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
self.status = "Initiated"
|
||||
elif self.payment_request_type == "Inward":
|
||||
self.db_set("status", "Requested")
|
||||
self.status = "Requested"
|
||||
|
||||
if self.payment_request_type == "Inward":
|
||||
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
|
||||
if (
|
||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||
) or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
if send_mail and self.payment_channel != "Phone":
|
||||
self.set_payment_request_url()
|
||||
self.send_email()
|
||||
self.make_communication_entry()
|
||||
|
||||
elif self.payment_channel == "Phone":
|
||||
self.request_phone_payment()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
request_amount = self.get_request_amount()
|
||||
|
||||
payment_record = dict(
|
||||
reference_doctype="Payment Request",
|
||||
reference_docname=self.name,
|
||||
payment_reference=self.reference_name,
|
||||
request_amount=request_amount,
|
||||
sender=self.email_to,
|
||||
currency=self.currency,
|
||||
payment_gateway=self.payment_gateway,
|
||||
)
|
||||
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
controller.request_for_payment(**payment_record)
|
||||
|
||||
def get_request_amount(self):
|
||||
data_of_completed_requests = frappe.get_all(
|
||||
"Integration Request",
|
||||
filters={
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_docname": self.name,
|
||||
"status": "Completed",
|
||||
},
|
||||
pluck="data",
|
||||
)
|
||||
|
||||
if not data_of_completed_requests:
|
||||
return self.grand_total
|
||||
|
||||
request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
|
||||
return request_amounts
|
||||
if self.payment_request_type == "Inward" and self.payment_gateway:
|
||||
return v1_gateway_before_submit(self, self.payment_gateway)
|
||||
|
||||
def on_cancel(self):
|
||||
self.check_if_payment_entry_exists()
|
||||
self.set_as_cancelled()
|
||||
|
||||
def make_invoice(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
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
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
|
||||
si.allocate_advances_automatically = True
|
||||
si = si.insert(ignore_permissions=True)
|
||||
si.submit()
|
||||
|
||||
def payment_gateway_validation(self):
|
||||
try:
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
if hasattr(controller, "on_payment_request_submission"):
|
||||
return controller.on_payment_request_submission(self)
|
||||
else:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def set_payment_request_url(self):
|
||||
if self.payment_account and self.payment_channel != "Phone":
|
||||
self.payment_url = self.get_payment_url()
|
||||
|
||||
if self.payment_url:
|
||||
self.db_set("payment_url", self.payment_url)
|
||||
|
||||
def get_payment_url(self):
|
||||
if self.reference_doctype != "Fees":
|
||||
data = frappe.db.get_value(
|
||||
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
|
||||
)
|
||||
else:
|
||||
data = frappe.db.get_value(
|
||||
self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
|
||||
)
|
||||
data.update({"company": frappe.defaults.get_defaults().company})
|
||||
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
controller.validate_transaction_currency(self.currency)
|
||||
|
||||
if hasattr(controller, "validate_minimum_transaction_amount"):
|
||||
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)
|
||||
|
||||
return controller.get_payment_url(
|
||||
**{
|
||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||
"title": data.company.encode("utf-8"),
|
||||
"description": self.subject.encode("utf-8"),
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": self.name,
|
||||
"payer_email": self.email_to or frappe.session.user,
|
||||
"payer_name": frappe.safe_encode(data.customer_name),
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
}
|
||||
)
|
||||
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
|
||||
si.allocate_advances_automatically = True
|
||||
si = si.insert(ignore_permissions=True)
|
||||
si.submit()
|
||||
|
||||
def set_as_paid(self):
|
||||
if self.payment_channel == "Phone":
|
||||
@@ -283,7 +179,8 @@ class PaymentRequest(Document):
|
||||
|
||||
else:
|
||||
payment_entry = self.create_payment_entry()
|
||||
self.make_invoice()
|
||||
if self.make_sales_invoice:
|
||||
self.make_invoice()
|
||||
|
||||
return payment_entry
|
||||
|
||||
@@ -343,6 +240,17 @@ class PaymentRequest(Document):
|
||||
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)})
|
||||
|
||||
@@ -369,7 +277,14 @@ class PaymentRequest(Document):
|
||||
)
|
||||
],
|
||||
}
|
||||
enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
enqueue(
|
||||
method=frappe.sendmail,
|
||||
queue="short",
|
||||
timeout=300,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
**email_args,
|
||||
)
|
||||
|
||||
def get_message(self):
|
||||
"""return message with payment gateway link"""
|
||||
@@ -412,15 +327,8 @@ class PaymentRequest(Document):
|
||||
)
|
||||
comm.insert(ignore_permissions=True)
|
||||
|
||||
def get_payment_success_url(self):
|
||||
return self.payment_success_url
|
||||
|
||||
def create_subscription(self, payment_provider, gateway_controller, data):
|
||||
if payment_provider == "stripe":
|
||||
with payment_app_import_guard():
|
||||
from payments.payment_gateways.stripe_integration import create_stripe_subscription
|
||||
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
def create_subscription(self, *args, **kwargs):
|
||||
return v1_create_subscription(*args, **kwargs)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@@ -428,7 +336,9 @@ def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
args = frappe._dict(args)
|
||||
if args.dt not in [
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
|
||||
if ref_doc.doctype not in [
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Sales Invoice",
|
||||
@@ -436,22 +346,23 @@ def make_payment_request(**args):
|
||||
"POS Invoice",
|
||||
"Fees",
|
||||
]:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
frappe.throw(
|
||||
_("Payment Requests cannot be created against: {0}").format(frappe.bold(ref_doc.doctype))
|
||||
)
|
||||
|
||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
if args.loyalty_points and args.dt == "Sales Order":
|
||||
|
||||
if args.loyalty_points and ref_doc.doctype == "Sales Order":
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
|
||||
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
|
||||
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)
|
||||
loyalty_amount: Document = validate_loyalty_points(
|
||||
ref_doc, int(args.loyalty_points)
|
||||
) # sets fields on ref_doc
|
||||
loyalty_amount.db_update()
|
||||
grand_total = grand_total - loyalty_amount
|
||||
|
||||
bank_account = (
|
||||
@@ -460,10 +371,10 @@ def make_payment_request(**args):
|
||||
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
|
||||
)
|
||||
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
@@ -494,11 +405,21 @@ def make_payment_request(**args):
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
"subject": _("Payment Request for {0}").format(args.dn),
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"reference_doctype": ref_doc.doctype,
|
||||
"reference_name": ref_doc.name,
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
"make_sales_invoice": (
|
||||
args.make_sales_invoice # new standard
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
),
|
||||
"mute_email": (
|
||||
args.mute_email # new standard
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -513,17 +434,15 @@ def make_payment_request(**args):
|
||||
for dimension in get_accounting_dimensions():
|
||||
pr.update({dimension: ref_doc.get(dimension)})
|
||||
|
||||
if args.order_type == "Shopping Cart" or args.mute_email:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
pr.insert(ignore_permissions=True)
|
||||
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
pr.submit()
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
frappe.db.commit()
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = pr.get_payment_url()
|
||||
frappe.local.response["location"] = pr.payment_url
|
||||
|
||||
if args.return_doc:
|
||||
return pr
|
||||
@@ -556,7 +475,10 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt == "Fees":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
return grand_total
|
||||
if grand_total > 0:
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
@@ -586,19 +508,14 @@ def get_gateway_details(args): # nosemgrep
|
||||
Return gateway and payment account of default payment gateway
|
||||
"""
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||
if gateway_account:
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||
|
||||
return gateway_account
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
|
||||
def get_payment_gateway_account(args):
|
||||
def get_payment_gateway_account(filter):
|
||||
return frappe.db.get_value(
|
||||
"Payment Gateway Account",
|
||||
args,
|
||||
["name", "payment_gateway", "payment_account", "message"],
|
||||
filter,
|
||||
["name", "payment_gateway", "payment_account", "payment_channel", "message"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,18 +2,27 @@
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
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
|
||||
|
||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||
|
||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||
PAYMENT_URL = "https://example.com/payment"
|
||||
|
||||
payment_gateways = [
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"},
|
||||
{"doctype": "Payment Gateway", "gateway": "_Test Gateway Other"},
|
||||
]
|
||||
|
||||
payment_method = [
|
||||
{
|
||||
@@ -29,13 +38,28 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"currency": "USD",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway Other",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Other",
|
||||
"currency": "USD",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway Phone",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Phone",
|
||||
"currency": "USD",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
for payment_gateway in payment_gateways:
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
|
||||
for method in payment_method:
|
||||
if not frappe.db.get_value(
|
||||
@@ -45,6 +69,28 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
send_email = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
|
||||
return_value=None,
|
||||
)
|
||||
self.send_email = send_email.start()
|
||||
self.addCleanup(send_email.stop)
|
||||
get_payment_url = patch(
|
||||
# this also shadows one (1) call to _get_payment_gateway_controller
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
|
||||
return_value=PAYMENT_URL,
|
||||
)
|
||||
self.get_payment_url = get_payment_url.start()
|
||||
self.addCleanup(get_payment_url.stop)
|
||||
_get_payment_gateway_controller = patch(
|
||||
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
|
||||
)
|
||||
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
|
||||
self.addCleanup(_get_payment_gateway_controller.stop)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
so_inr.disable_rounded_total = 1
|
||||
@@ -75,6 +121,99 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
self.assertEqual(pr.reference_name, si_usd.name)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
|
||||
def test_payment_channels(self):
|
||||
so = make_sales_order(currency="USD")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Other - USD",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.payment_channel, "Other")
|
||||
self.assertEqual(pr.mute_email, True)
|
||||
|
||||
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||
self.assertEqual(self.send_email.call_count, 0)
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
|
||||
pr.cancel()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=False,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.flags.mute_email = True # but temporarily prohibit sending
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||
self.assertEqual(self.send_email.call_count, 0) # hence: no increment
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 2)
|
||||
pr.cancel()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Phone - USD",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Phone")
|
||||
self.assertEqual(pr.mute_email, True)
|
||||
|
||||
self.assertIsNone(pr.payment_url)
|
||||
self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
|
||||
pr.cancel()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, False)
|
||||
|
||||
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||
self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
|
||||
pr.cancel()
|
||||
|
||||
so = make_sales_order(currency="USD", do_not_save=True)
|
||||
# no-op; for optical consistency with how a webshop SO would look like
|
||||
so.order_type = "Shopping Cart"
|
||||
so.save()
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
make_sales_invoice=True,
|
||||
mute_email=True,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
pr.reload()
|
||||
|
||||
self.assertEqual(pr.payment_channel, "Email")
|
||||
self.assertEqual(pr.mute_email, True)
|
||||
|
||||
self.assertEqual(pr.payment_url, PAYMENT_URL)
|
||||
self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
|
||||
self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
|
||||
pr.cancel()
|
||||
|
||||
def test_payment_entry_against_purchase_invoice(self):
|
||||
si_usd = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
@@ -260,3 +399,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)
|
||||
|
||||
@@ -136,18 +136,28 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.year_start_date, -1)
|
||||
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
if not frappe.db.exists(
|
||||
"GL Entry",
|
||||
{"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0},
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
):
|
||||
return
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
|
||||
@@ -17,6 +17,10 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.selling.page.point_of_sale.point_of_sale import get_items
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
@@ -179,6 +183,94 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
accounting_dimension_department.save()
|
||||
disable_dimension()
|
||||
|
||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||
frappe.flags.print_message = False
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
item_doc = make_item(
|
||||
"_Test Item With Batch FOR POS Merge Test",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-PM-POS-MERGE-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
)
|
||||
|
||||
item_code = item_doc.name
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
use_serial_batch_fields=0,
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
pcv_doc.submit()
|
||||
|
||||
piv_merge = frappe.db.get_value("POS Invoice Merge Log", {"pos_closing_entry": pcv_doc.name}, "name")
|
||||
|
||||
self.assertTrue(piv_merge)
|
||||
piv_merge_doc = frappe.get_doc("POS Invoice Merge Log", piv_merge)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[0].pos_invoice)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[1].pos_invoice)
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(pos_inv.consolidated_invoice)
|
||||
pos_inv2.load_from_db()
|
||||
self.assertTrue(pos_inv2.consolidated_invoice)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 0.0)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
frappe.flags.print_message = True
|
||||
|
||||
pcv_doc.reload()
|
||||
pcv_doc.cancel()
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pos_inv.reload()
|
||||
pos_inv2.reload()
|
||||
|
||||
pos_inv.cancel()
|
||||
pos_inv2.cancel()
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 10.0)
|
||||
|
||||
|
||||
def init_user_and_profile(**args):
|
||||
user = "test@example.com"
|
||||
|
||||
@@ -188,7 +188,7 @@ class POSInvoice(SalesInvoice):
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
)
|
||||
|
||||
# run on validate method of selling controller
|
||||
@@ -228,7 +228,10 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.submit_serial_batch_bundle()
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
for table_name in ["items", "packed_items"]:
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.submit_serial_batch_bundle(table_name)
|
||||
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
@@ -282,10 +285,11 @@ class POSInvoice(SalesInvoice):
|
||||
{"is_cancelled": 1, "voucher_no": ""},
|
||||
)
|
||||
|
||||
frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
|
||||
def submit_serial_batch_bundle(self):
|
||||
for item in self.items:
|
||||
def submit_serial_batch_bundle(self, table_name):
|
||||
for item in self.get(table_name):
|
||||
if item.serial_and_batch_bundle:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
|
||||
@@ -354,10 +358,16 @@ class POSInvoice(SalesInvoice):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
error_msg = ""
|
||||
if d.get("has_serial_no") and not d.serial_and_batch_bundle:
|
||||
if d.get("has_serial_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.serial_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
|
||||
|
||||
elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
|
||||
elif d.get("has_batch_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.batch_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
|
||||
|
||||
if error_msg:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
@@ -781,8 +780,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1.submit()
|
||||
pos_inv1.reload()
|
||||
|
||||
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
@@ -958,7 +955,7 @@ def create_pos_invoice(**args):
|
||||
pos_inv.set_missing_values()
|
||||
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
type_of_transaction = args.type_of_transaction or "Outward"
|
||||
|
||||
if pos_inv.is_return:
|
||||
@@ -999,6 +996,9 @@ def create_pos_invoice(**args):
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
"serial_no": args.serial_no if args.use_serial_batch_fields else None,
|
||||
"batch_no": args.batch_no if args.use_serial_batch_fields else None,
|
||||
}
|
||||
# append in pos invoice items without item_code by checking flag without_item_code
|
||||
if args.without_item_code:
|
||||
@@ -1024,6 +1024,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.insert()
|
||||
if not args.do_not_submit:
|
||||
pos_inv.submit()
|
||||
if args.use_serial_batch_fields:
|
||||
pos_inv.reload()
|
||||
else:
|
||||
pos_inv.payment_schedule = []
|
||||
else:
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"column_break_19",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"distributed_discount_amount",
|
||||
"base_rate_with_margin",
|
||||
"section_break1",
|
||||
"rate",
|
||||
@@ -634,7 +635,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
@@ -655,7 +655,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
@@ -827,7 +826,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.serial_and_batch_bundle",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -849,11 +848,17 @@
|
||||
{
|
||||
"fieldname": "column_break_ciit",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:15.336778",
|
||||
"modified": "2024-05-07 15:56:53.343317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
||||
@@ -39,6 +39,7 @@ class POSInvoiceItem(SalesInvoiceItem):
|
||||
description: DF.TextEditor
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Percent
|
||||
distributed_discount_amount: DF.Currency
|
||||
dn_detail: DF.Data | None
|
||||
enable_deferred_revenue: DF.Check
|
||||
expense_account: DF.Link | None
|
||||
|
||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg = _(
|
||||
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||
msg += " "
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
"The 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(
|
||||
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
@@ -131,6 +130,7 @@ class POSInvoiceMergeLog(Document):
|
||||
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.serial_and_batch_bundle_reference_for_pos_invoice()
|
||||
self.cancel_linked_invoices()
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
@@ -191,6 +191,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
@@ -312,6 +313,12 @@ class POSInvoiceMergeLog(Document):
|
||||
doc.set_status(update=True)
|
||||
doc.save()
|
||||
|
||||
def serial_and_batch_bundle_reference_for_pos_invoice(self):
|
||||
for d in self.pos_invoices:
|
||||
pos_invoice = frappe.get_doc("POS Invoice", d.pos_invoice)
|
||||
for table_name in ["items", "packed_items"]:
|
||||
pos_invoice.set_serial_and_batch_bundle(table_name)
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
if not si_name:
|
||||
@@ -392,7 +399,7 @@ def unconsolidate_pos_invoices(closing_entry):
|
||||
"POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name"
|
||||
)
|
||||
|
||||
if len(merge_logs) >= 10:
|
||||
if len(closing_entry.pos_transactions) >= 10:
|
||||
closing_entry.set_status(update=True, status="Queued")
|
||||
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
|
||||
else:
|
||||
@@ -481,7 +488,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -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": "2024-03-27 13:10:17.521896",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
|
||||
PricingRuleItemCode,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||
PricingRuleItemGroup,
|
||||
)
|
||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
||||
free_item_rate: DF.Currency
|
||||
free_item_uom: DF.Link | None
|
||||
free_qty: DF.Float
|
||||
has_priority: DF.Check
|
||||
is_cumulative: DF.Check
|
||||
is_recursive: DF.Check
|
||||
item_groups: DF.Table[PricingRuleItemGroup]
|
||||
@@ -140,6 +139,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
|
||||
@@ -156,6 +156,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)
|
||||
@@ -180,7 +186,8 @@ class PricingRule(Document):
|
||||
if not self.priority:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||
frappe.bold(_("Priority")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -188,7 +195,7 @@ class PricingRule(Document):
|
||||
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"))
|
||||
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
@@ -303,6 +310,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"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
@@ -573,6 +584,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)
|
||||
|
||||
@@ -6,7 +6,9 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
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.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -929,6 +931,30 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_pricing_rule_for_transaction_with_condition(self):
|
||||
make_item("PR Transaction Condition")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
make_pricing_rule(
|
||||
selling=1,
|
||||
min_qty=0,
|
||||
price_or_product_discount="Product",
|
||||
apply_on="Transaction",
|
||||
free_item="PR Transaction Condition",
|
||||
free_qty=1,
|
||||
free_item_rate=10,
|
||||
condition="customer=='_Test Customer 1'",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
|
||||
self.assertEqual(len(si.items), 2)
|
||||
self.assertEqual(si.items[1].rate, 10)
|
||||
|
||||
si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
|
||||
self.assertEqual(len(si1.items), 1)
|
||||
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
@@ -1102,7 +1128,253 @@ class TestPricingRule(unittest.TestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 4)
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Amount",
|
||||
"discount_amount": 100,
|
||||
"apply_multiple_pricing_rules": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_amount, 200)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_priority_of_multiple_pricing_rules(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"has_priority": 1,
|
||||
"priority": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"has_priority": 1,
|
||||
"priority": 3,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_percentage, 20)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_pricing_rules_with_and_without_apply_multiple(self):
|
||||
item = make_item("PR Item 99")
|
||||
|
||||
test_records = [
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test discount on item group",
|
||||
"name": "_Test discount on item group",
|
||||
"apply_on": "Item Group",
|
||||
"item_groups": [
|
||||
{
|
||||
"item_group": "Products",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 60,
|
||||
"has_priority": 1,
|
||||
"company": "_Test Company",
|
||||
"apply_multiple_pricing_rules": True,
|
||||
},
|
||||
{
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test fixed rate on item code",
|
||||
"name": "_Test fixed rate on item code",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": item.name,
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 25,
|
||||
"has_priority": 1,
|
||||
"company": "_Test Company",
|
||||
"apply_multiple_pricing_rules": False,
|
||||
},
|
||||
]
|
||||
|
||||
for item_group_priority, item_code_priority in [(2, 4), (4, 2)]:
|
||||
item_group_rule = frappe.get_doc(test_records[0].copy())
|
||||
item_group_rule.priority = item_group_priority
|
||||
item_group_rule.insert()
|
||||
|
||||
item_code_rule = frappe.get_doc(test_records[1].copy())
|
||||
item_code_rule.priority = item_code_priority
|
||||
item_code_rule.insert()
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", item=item.name, do_not_submit=True)
|
||||
si.save()
|
||||
self.assertEqual(len(si.pricing_rules), 1)
|
||||
# Item Code rule should've applied as it has higher priority
|
||||
expected_rule = item_group_rule if item_group_priority > item_code_priority else item_code_rule
|
||||
self.assertEqual(si.pricing_rules[0].pricing_rule, expected_rule.name)
|
||||
|
||||
si.delete()
|
||||
item_group_rule.delete()
|
||||
item_code_rule.delete()
|
||||
|
||||
def test_validation_on_mixed_condition_with_recursion(self):
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=10,
|
||||
selling=1,
|
||||
priority=2,
|
||||
min_qty=4,
|
||||
title="_Test Pricing Rule with Min Qty - 2",
|
||||
)
|
||||
pricing_rule.mixed_conditions = True
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
@@ -1132,6 +1404,7 @@ def make_pricing_rule(**args):
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
@@ -32,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
||||
|
||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and pricing_rules[0].has_priority:
|
||||
continue
|
||||
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
break
|
||||
|
||||
@@ -170,12 +174,9 @@ def _get_pricing_rules(apply_on, args, values):
|
||||
|
||||
|
||||
def apply_multiple_pricing_rules(pricing_rules):
|
||||
apply_multiple_rule = [
|
||||
d.apply_multiple_pricing_rules for d in pricing_rules if d.apply_multiple_pricing_rules
|
||||
]
|
||||
|
||||
if not apply_multiple_rule:
|
||||
return False
|
||||
for d in pricing_rules:
|
||||
if not d.apply_multiple_pricing_rules:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -485,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
@@ -560,6 +561,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
|
||||
if pricing_rules:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
||||
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
|
||||
|
||||
if not pricing_rules:
|
||||
remove_free_item(doc)
|
||||
@@ -653,7 +655,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = round(qty)
|
||||
qty = math.floor(qty)
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
@@ -729,7 +731,7 @@ def validate_coupon_code(coupon_name):
|
||||
elif coupon.valid_upto:
|
||||
if coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.used >= coupon.maximum_use:
|
||||
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
|
||||
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
||||
|
||||
|
||||
@@ -737,7 +739,10 @@ def update_coupon_code_count(coupon_name, transaction_type):
|
||||
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
||||
if coupon:
|
||||
if transaction_type == "used":
|
||||
if coupon.used < coupon.maximum_use:
|
||||
if not coupon.maximum_use:
|
||||
coupon.used = coupon.used + 1
|
||||
coupon.save(ignore_permissions=True)
|
||||
elif coupon.used < coupon.maximum_use:
|
||||
coupon.used = coupon.used + 1
|
||||
coupon.save(ignore_permissions=True)
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -502,7 +504,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
@@ -22,11 +22,11 @@
|
||||
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
{% endif %}
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
{{ _("to") }}
|
||||
{{ frappe.format(filters.to_date, 'Date')}}</b>
|
||||
</h5>
|
||||
{{ _("Date: {0} to {1}").format(
|
||||
frappe.format(filters.from_date, "Date"),
|
||||
frappe.format(filters.to_date, 'Date')
|
||||
) }}
|
||||
</h5>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
||||
<br>{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
{% endif %}
|
||||
@@ -83,17 +83,20 @@
|
||||
</table>
|
||||
<br>
|
||||
{% if ageing %}
|
||||
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
|
||||
{{ _("up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
||||
<h4 class="text-center">
|
||||
{{ _("Ageing Report based on {0} up to {1}").format(
|
||||
ageing.ageing_based_on,
|
||||
frappe.format(filters.to_date, "Date")
|
||||
) }}
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%">0 - 30 Days</th>
|
||||
<th style="width: 20%">30 - 60 Days</th>
|
||||
<th style="width: 20%">60 - 90 Days</th>
|
||||
<th style="width: 20%">90 - 120 Days</th>
|
||||
<th style="width: 20%">Above 120 Days</th>
|
||||
<th style="width: 20%">{{ _("0 - 30 Days") }}</th>
|
||||
<th style="width: 20%">{{ _("30 - 60 Days") }}</th>
|
||||
<th style="width: 20%">{{ _("60 - 90 Days") }}</th>
|
||||
<th style="width: 20%">{{ _("90 - 120 Days") }}</th>
|
||||
<th style="width: 20%">{{ _("Above 120 Days") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"ignore_cr_dr_notes",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -383,10 +384,16 @@
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:19.075913",
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
@@ -133,6 +134,9 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.ignore_cr_dr_notes:
|
||||
filters.update({"ignore_cr_dr_notes": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
col, res = get_soa(filters)
|
||||
@@ -158,7 +162,7 @@ def set_ageing(doc, entry):
|
||||
ageing_filters = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"report_date": doc.to_date,
|
||||
"report_date": doc.posting_date,
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
|
||||
@@ -6,228 +6,302 @@
|
||||
.print-format td {
|
||||
vertical-align:middle !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<div id="header-html" class="hidden-pdf">
|
||||
{% if letter_head.content %}
|
||||
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||
{{ letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h4 class="text-center">
|
||||
{{ filters.customer_name }}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) %}
|
||||
{{ _("Tax Id: ") }}{{ filters.tax_id }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h5 class="text-center">
|
||||
{{ _(filters.ageing_based_on) }}
|
||||
{{ _("Until") }}
|
||||
{{ frappe.format(filters.report_date, 'Date') }}
|
||||
</h5>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="pull-left">
|
||||
{% if(filters.payment_terms) %}
|
||||
<strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if(filters.credit_limit) %}
|
||||
<strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% set balance_row = data.slice(-1).pop() %}
|
||||
{% for i in report.columns %}
|
||||
{% if i.fieldname == 'age' %}
|
||||
{% set elem = i %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set start = report.columns.findIndex(elem) %}
|
||||
{% set range1 = report.columns[start].label %}
|
||||
{% set range2 = report.columns[start+1].label %}
|
||||
{% set range3 = report.columns[start+2].label %}
|
||||
{% set range4 = report.columns[start+3].label %}
|
||||
{% set range5 = report.columns[start+4].label %}
|
||||
{% set range6 = report.columns[start+5].label %}
|
||||
|
||||
{% if(balance_row) %}
|
||||
<table class="table table-bordered table-condensed">
|
||||
<caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
|
||||
<colgroup>
|
||||
<col style="width: 30mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _(" ") }}</th>
|
||||
<th>{{ _(range1) }}</th>
|
||||
<th>{{ _(range2) }}</th>
|
||||
<th>{{ _(range3) }}</th>
|
||||
<th>{{ _(range4) }}</th>
|
||||
<th>{{ _(range5) }}</th>
|
||||
<th>{{ _(range6) }}</th>
|
||||
<th>{{ _("Total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ _("Total Outstanding") }}</td>
|
||||
<td class="text-right">
|
||||
{{ format_number(balance_row["age"], null, 2) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>{{ _("Future Payments") }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<tr class="cvs-footer">
|
||||
<th class="text-left">{{ _("Cheques Required") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
{% endif %}
|
||||
<div id="header-html" class="hidden-pdf">
|
||||
{% if letter_head.content %}
|
||||
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||
{% endif %}
|
||||
<table class="table table-bordered">
|
||||
</div>
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||
{{ letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h4 class="text-center">
|
||||
{{ filters.customer_name }}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) %}
|
||||
{{ _("Tax Id: {0}").format(filters.tax_id) }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h5 class="text-center">
|
||||
{{ _("{0} until {1}").format(
|
||||
_(filters.ageing_based_on),
|
||||
frappe.format(filters.report_date, 'Date')
|
||||
) }}
|
||||
</h5>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="pull-left">
|
||||
{% if(filters.payment_terms) %}
|
||||
<strong>{{ _("Payment Terms:") }}</strong> {{ filters.payment_terms }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if(filters.credit_limit) %}
|
||||
<strong>{{ _("Credit Limit:") }}</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% set balance_row = data.slice(-1).pop() %}
|
||||
{% for i in report.columns %}
|
||||
{% if i.fieldname == 'age' %}
|
||||
{% set elem = i %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set start = report.columns.findIndex(elem) %}
|
||||
{% set range1 = report.columns[start].label %}
|
||||
{% set range2 = report.columns[start+1].label %}
|
||||
{% set range3 = report.columns[start+2].label %}
|
||||
{% set range4 = report.columns[start+3].label %}
|
||||
{% set range5 = report.columns[start+4].label %}
|
||||
{% set range6 = report.columns[start+5].label %}
|
||||
|
||||
{% if(balance_row) %}
|
||||
<table class="table table-bordered table-condensed">
|
||||
<caption class="text-right">{{ _("Amount in {0}").format(data[0]["currency"] ~ "") }}</caption>
|
||||
<colgroup>
|
||||
<col style="width: 30mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
<col style="width: 18mm;">
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
<th style="width: 10%">{{ _("Date") }}</th>
|
||||
<th style="width: 4%">{{ _("Age (Days)") }}</th>
|
||||
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<th style="width: 14%">{{ _("Reference") }}</th>
|
||||
<th style="width: 10%">{{ _("Sales Person") }}</th>
|
||||
{% else %}
|
||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 20%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks") }}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
|
||||
<th style="width: 10%; text-align: right">
|
||||
{% if report.report_name == "Accounts Receivable" %}
|
||||
{{ _('Credit Note') }}
|
||||
{% else %}
|
||||
{{ _('Debit Note') }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
|
||||
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
|
||||
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th style="width: 40%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks")}}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
|
||||
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
|
||||
<th style="width: 15%">
|
||||
{% if report.report_name == "Accounts Receivable Summary" %}
|
||||
{{ _('Credit Note Amount') }}
|
||||
{% else %}
|
||||
{{ _('Debit Note Amount') }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
|
||||
{% endif %}
|
||||
<th>{{ _(" ") }}</th>
|
||||
<th>{{ _(range1) }}</th>
|
||||
<th>{{ _(range2) }}</th>
|
||||
<th>{{ _(range3) }}</th>
|
||||
<th>{{ _(range4) }}</th>
|
||||
<th>{{ _(range5) }}</th>
|
||||
<th>{{ _(range6) }}</th>
|
||||
<th>{{ _("Total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(data|length) %}
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
{% if(data[i]["party"]) %}
|
||||
<td>{{ (data[i]["posting_date"]) }}</td>
|
||||
<td style="text-align: right">{{ data[i]["age"] }}</td>
|
||||
<td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
{{ data[i]["voucher_type"] }}
|
||||
<br>
|
||||
{% endif %}
|
||||
{{ data[i]["voucher_no"] }}
|
||||
</td>
|
||||
<tr>
|
||||
<td>{{ _("Total Outstanding") }}</td>
|
||||
<td class="text-right">
|
||||
{{ format_number(balance_row["age"], null, 2) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>{{ _("Future Payments") }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<tr class="cvs-footer">
|
||||
<th class="text-left">{{ _("Cheques Required") }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td>{{ data[i]["sales_person"] }}</td>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
<th style="width: 10%">{{ _("Date") }}</th>
|
||||
<th style="width: 4%">{{ _("Age (Days)") }}</th>
|
||||
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<th style="width: 14%">{{ _("Reference") }}</th>
|
||||
<th style="width: 10%">{{ _("Sales Person") }}</th>
|
||||
{% else %}
|
||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 20%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks") }}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
|
||||
<th style="width: 10%; text-align: right">
|
||||
{% if report.report_name == "Accounts Receivable" %}
|
||||
{{ _("Credit Note") }}
|
||||
{% else %}
|
||||
{{ _("Debit Note") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
|
||||
{% endif %}
|
||||
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
|
||||
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
|
||||
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th style="width: 40%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks")}}
|
||||
{% else %}
|
||||
{{ _("Party") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
|
||||
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
|
||||
<th style="width: 15%">
|
||||
{% if report.report_name == "Accounts Receivable Summary" %}
|
||||
{{ _("Credit Note Amount") }}
|
||||
{% else %}
|
||||
{{ _("Debit Note Amount") }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(data|length) %}
|
||||
<tr>
|
||||
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
|
||||
{% if(data[i]["party"]) %}
|
||||
<td>{{ (data[i]["posting_date"]) }}</td>
|
||||
<td style="text-align: right">{{ data[i]["age"] }}</td>
|
||||
<td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
{{ data[i]["voucher_type"] }}
|
||||
<br>
|
||||
{% endif %}
|
||||
{{ data[i]["voucher_no"] }}
|
||||
</td>
|
||||
|
||||
{% if not (filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td>{{ data[i]["sales_person"] }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if not (filters.show_future_payments) %}
|
||||
<td>
|
||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||
{{ data[i]["party"] }}
|
||||
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["customer_name"] }}
|
||||
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["supplier_name"] }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if data[i]["remarks"] %}
|
||||
{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if(data[i]["party"] or " ") %}
|
||||
{% if not(data[i]["is_total_row"]) %}
|
||||
<td>
|
||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||
{% if(not(filters.customer | filters.supplier)) %}
|
||||
{{ data[i]["party"] }}
|
||||
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["customer_name"] }}
|
||||
@@ -235,130 +309,62 @@
|
||||
<br> {{ data[i]["supplier_name"] }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if data[i]["remarks"] %}
|
||||
{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<br>{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
|
||||
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% if(report.report_name == "Accounts Receivable") %}
|
||||
<td style="text-align: right">
|
||||
{{ data[i]["po_no"] }}</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if(data[i]["party"] or " ") %}
|
||||
{% if not(data[i]["is_total_row"]) %}
|
||||
<td>
|
||||
{% if(not(filters.customer | filters.supplier)) %}
|
||||
{{ data[i]["party"] }}
|
||||
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["customer_name"] }}
|
||||
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
|
||||
<br> {{ data[i]["supplier_name"] }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>{{ _("Remarks") }}:
|
||||
{{ data[i]["remarks"] }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td><b>{{ _("Total") }}</b></td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
<td><b>{{ _("Total") }}</b></td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
|
||||
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{% if ageing %}
|
||||
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
|
||||
{{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}}
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">30 Days</th>
|
||||
<th style="width: 25%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 25%">120 Days</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if terms_and_conditions %}
|
||||
<div>
|
||||
{{ terms_and_conditions }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
{% if ageing %}
|
||||
<h4 class="text-center">
|
||||
{{ _("Ageing Report based on {0} up to {1}").format(
|
||||
ageing.ageing_based_on,
|
||||
frappe.format(filters.report_date, "Date")
|
||||
) }}
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">{{ _("0 - 30 Days") }}</th>
|
||||
<th style="width: 25%">{{ _("30 - 60 Days") }}</th>
|
||||
<th style="width: 25%">{{ _("60 - 90 Days") }}</th>
|
||||
<th style="width: 25%">{{ _("90 - 120 Days") }}</th>
|
||||
<th style="width: 20%">{{ _("Above 120 Days") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if terms_and_conditions %}
|
||||
<div>
|
||||
{{ terms_and_conditions }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-right text-muted">{{ _("Printed on {0}").format(frappe.utils.now()) }}</p>
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Promotional Scheme", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("for_price_list", "price_discount_slabs", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
selling: doc.selling,
|
||||
buying: doc.buying,
|
||||
currency: doc.currency,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.trigger("toggle_reqd_apply_on");
|
||||
|
||||
@@ -51,6 +51,7 @@ price_discount_fields = [
|
||||
"discount_percentage",
|
||||
"validate_applied_rule",
|
||||
"apply_multiple_pricing_rules",
|
||||
"for_price_list",
|
||||
]
|
||||
|
||||
product_discount_fields = [
|
||||
@@ -63,6 +64,7 @@ product_discount_fields = [
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
"round_free_qty",
|
||||
]
|
||||
|
||||
|
||||
@@ -146,6 +148,7 @@ class PromotionalScheme(Document):
|
||||
|
||||
self.validate_applicable_for()
|
||||
self.validate_pricing_rules()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if self.applicable_for:
|
||||
@@ -163,7 +166,7 @@ class PromotionalScheme(Document):
|
||||
docnames = []
|
||||
|
||||
# If user has changed applicable for
|
||||
if self._doc_before_save.applicable_for == self.applicable_for:
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
@@ -177,6 +180,7 @@ class PromotionalScheme(Document):
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
pricing_rules = (
|
||||
frappe.get_all(
|
||||
"Pricing Rule",
|
||||
@@ -188,6 +192,15 @@ class PromotionalScheme(Document):
|
||||
)
|
||||
self.update_pricing_rules(pricing_rules)
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions:
|
||||
if self.product_discount_slabs:
|
||||
for slab in self.product_discount_slabs:
|
||||
if slab.is_recursive:
|
||||
frappe.throw(
|
||||
_("Recursive Discounts with Mixed condition is not supported by the system")
|
||||
)
|
||||
|
||||
def update_pricing_rules(self, pricing_rules):
|
||||
rules = {}
|
||||
count = 0
|
||||
|
||||
@@ -129,6 +129,25 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
[pr.min_qty, pr.free_item, pr.free_qty, pr.recurse_for], [12, "_Test Item 2", 1, 12]
|
||||
)
|
||||
|
||||
def test_validation_on_recurse_with_mixed_condition(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.set("price_discount_slabs", [])
|
||||
ps.set(
|
||||
"product_discount_slabs",
|
||||
[
|
||||
{
|
||||
"rule_description": "12+1",
|
||||
"min_qty": 12,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 12,
|
||||
}
|
||||
],
|
||||
)
|
||||
ps.mixed_conditions = True
|
||||
self.assertRaises(frappe.ValidationError, ps.save)
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"rate",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_11",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -120,6 +121,13 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.rate_or_discount!=\"Rate\"",
|
||||
"fieldname": "for_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -169,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:22.448265",
|
||||
"modified": "2024-07-23 12:33:46.574950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
|
||||
@@ -19,6 +19,7 @@ class PromotionalSchemePriceDiscount(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
for_price_list: DF.Link | None
|
||||
max_amount: DF.Currency
|
||||
max_qty: DF.Float
|
||||
min_amount: DF.Currency
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"column_break_9",
|
||||
"free_item_uom",
|
||||
"free_item_rate",
|
||||
"round_free_qty",
|
||||
"section_break_12",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -181,12 +182,18 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:22.605892",
|
||||
"modified": "2024-07-22 17:25:07.880984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
@@ -195,4 +202,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
round_free_qty: DF.Check
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
{
|
||||
"fieldname": "cost_center_name",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:23.244686",
|
||||
"modified": "2024-05-03 17:16:51.666461",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "PSOA Cost Center",
|
||||
|
||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
cost_center_name: DF.Link | None
|
||||
cost_center_name: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -77,31 +77,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) {
|
||||
this.frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
this.frm
|
||||
.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted."));
|
||||
me.frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0) {
|
||||
if (doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -489,10 +464,12 @@ function hide_fields(doc) {
|
||||
|
||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
if (cur_frm.fields_dict["items"]) {
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
}
|
||||
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
@@ -679,7 +656,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
if (frm.doc.supplier) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
if (!frm.doc.__onload.enable_apply_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"auto_repeat",
|
||||
@@ -364,7 +363,8 @@
|
||||
"description": "Once set, this invoice will be on hold till the set date",
|
||||
"fieldname": "release_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Release Date"
|
||||
"label": "Release Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_17",
|
||||
@@ -1604,15 +1604,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
@@ -1640,7 +1631,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-11 11:28:42.802211",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -159,7 +159,6 @@ class PurchaseInvoice(BuyingController):
|
||||
rejected_warehouse: DF.Link | None
|
||||
release_date: DF.Date | None
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -348,6 +347,22 @@ class PurchaseInvoice(BuyingController):
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
||||
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
||||
po = qb.DocType("Purchase Order")
|
||||
po_with_tds = (
|
||||
qb.from_(po)
|
||||
.select(po.name)
|
||||
.where(
|
||||
po.docstatus.eq(1)
|
||||
& (po.name.isin(purchase_orders))
|
||||
& (po.apply_tds.eq(1))
|
||||
& (po.tax_withholding_category.notnull())
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
@@ -363,16 +378,16 @@ class PurchaseInvoice(BuyingController):
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold("Credit To")),
|
||||
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold(_("Credit To"))),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
||||
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
@@ -449,7 +464,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_received_but_not_billed = None
|
||||
self.asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -532,26 +547,60 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
account = None
|
||||
if not item.pr_detail and item.po_detail:
|
||||
receipt_item = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
{
|
||||
"purchase_order": item.purchase_order,
|
||||
"purchase_order_item": item.po_detail,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["name", "parent"],
|
||||
as_dict=1,
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
if receipt_item:
|
||||
item.pr_detail = receipt_item.name
|
||||
item.purchase_receipt = receipt_item.parent
|
||||
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
"asset_received_but_not_billed"
|
||||
)
|
||||
|
||||
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||
arbnb_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"account": self.asset_received_but_not_billed,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
if arbnb_booked_in_pr:
|
||||
account = self.asset_received_but_not_billed
|
||||
|
||||
if not account:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||
form_link, self.company
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = account
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -559,7 +608,7 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
validate_account_head(item.idx, item.expense_account, self.company, "Expense")
|
||||
|
||||
def set_against_expense_account(self):
|
||||
def set_against_expense_account(self, force=False):
|
||||
against_accounts = []
|
||||
for item in self.get("items"):
|
||||
if item.expense_account and (item.expense_account not in against_accounts):
|
||||
@@ -567,6 +616,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.against_expense_account = ",".join(against_accounts)
|
||||
|
||||
def force_set_against_expense_account(self):
|
||||
self.set_against_expense_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
@@ -582,7 +635,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Order Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
@@ -603,7 +656,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Receipt Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
@@ -649,6 +702,19 @@ class PurchaseInvoice(BuyingController):
|
||||
where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
|
||||
}
|
||||
)
|
||||
self.status_updater.append(
|
||||
{
|
||||
"source_dt": "Purchase Invoice Item",
|
||||
"target_dt": "Material Request Item",
|
||||
"join_field": "material_request_item",
|
||||
"target_field": "received_qty",
|
||||
"target_parent_dt": "Material Request",
|
||||
"target_parent_field": "per_received",
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "material_request",
|
||||
}
|
||||
)
|
||||
if cint(self.is_return):
|
||||
self.status_updater.append(
|
||||
{
|
||||
@@ -708,6 +774,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -729,18 +796,17 @@ class PurchaseInvoice(BuyingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
@@ -1063,7 +1129,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if item.get("purchase_receipt"):
|
||||
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
@@ -1147,7 +1213,7 @@ class PurchaseInvoice(BuyingController):
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
@@ -1175,6 +1241,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
@@ -1191,13 +1258,20 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
@@ -1207,7 +1281,7 @@ class PurchaseInvoice(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1637,6 +1711,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
@@ -1678,8 +1755,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
@@ -1694,7 +1769,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
self.set("advance_tax", [])
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
|
||||
@@ -10,13 +10,19 @@ import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
@@ -82,6 +88,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# teardown
|
||||
pi.delete()
|
||||
|
||||
def test_update_received_qty_in_material_request(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
"""
|
||||
Test if the received_qty in Material Request is updated correctly when
|
||||
a Purchase Invoice with update_stock=True is submitted.
|
||||
"""
|
||||
mr = make_material_request(item_code="_Test Item", qty=10)
|
||||
mr.save()
|
||||
mr.submit()
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Create a Purchase Invoice with update_stock=True
|
||||
pi = make_purchase_invoice(po.name)
|
||||
pi.update_stock = True
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Check if the received quantity is updated in Material Request
|
||||
mr.reload()
|
||||
self.assertEqual(mr.items[0].received_qty, 10)
|
||||
|
||||
def test_gl_entries_without_perpetual_inventory(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
pi = frappe.copy_doc(test_records[0])
|
||||
@@ -1984,18 +2015,15 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
@change_settings("Buying Settings", {"supplier_group": None})
|
||||
def test_purchase_invoice_without_supplier_group(self):
|
||||
@@ -2168,6 +2196,112 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
|
||||
self.assertEqual(row.rejected_serial_no, serial_nos[2])
|
||||
|
||||
def test_make_pr_and_pi_from_po(self):
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category
|
||||
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
create_asset_category()
|
||||
|
||||
item = create_item(
|
||||
item_code="_Test_Item", is_stock_item=0, is_fixed_asset=1, asset_category="Computers"
|
||||
)
|
||||
po = create_purchase_order(item_code=item.item_code)
|
||||
pr = create_pr_against_po(po.name, 10)
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
pr_gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s
|
||||
order by account asc""",
|
||||
pr.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pr_expected_values = [
|
||||
["Asset Received But Not Billed - _TC", 0, 5000],
|
||||
["CWIP Account - _TC", 5000, 0],
|
||||
]
|
||||
|
||||
for i, gle in enumerate(pr_gl_entries):
|
||||
self.assertEqual(pr_expected_values[i][0], gle.account)
|
||||
self.assertEqual(pr_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pr_expected_values[i][2], gle.credit)
|
||||
|
||||
pi_gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
order by account asc""",
|
||||
pi.name,
|
||||
as_dict=1,
|
||||
)
|
||||
pi_expected_values = [
|
||||
["Asset Received But Not Billed - _TC", 5000, 0],
|
||||
["Creditors - _TC", 0, 5000],
|
||||
]
|
||||
|
||||
for i, gle in enumerate(pi_gl_entries):
|
||||
self.assertEqual(pi_expected_values[i][0], gle.account)
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 6300
|
||||
pr.plc_conversion_rate = 1
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.conversion_rate, 6300)
|
||||
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=3000,
|
||||
distribute_charges_based_on="Qty",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 1.1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"column_break_30",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"distributed_discount_amount",
|
||||
"base_rate_with_margin",
|
||||
"sec_break2",
|
||||
"rate",
|
||||
@@ -57,6 +58,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -105,6 +107,8 @@
|
||||
"purchase_receipt",
|
||||
"pr_detail",
|
||||
"sales_invoice_item",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"item_weight_details",
|
||||
"weight_per_unit",
|
||||
"total_weight",
|
||||
@@ -835,7 +839,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
@@ -934,12 +938,50 @@
|
||||
{
|
||||
"fieldname": "column_break_vbbb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Material Request",
|
||||
"no_copy": 1,
|
||||
"options": "Material Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Material Request Item",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "pr_detail",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:24.204495",
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -34,6 +34,7 @@ class PurchaseInvoiceItem(Document):
|
||||
description: DF.TextEditor | None
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Percent
|
||||
distributed_discount_amount: DF.Currency
|
||||
enable_deferred_expense: DF.Check
|
||||
expense_account: DF.Link | None
|
||||
from_warehouse: DF.Link | None
|
||||
@@ -52,6 +53,8 @@ class PurchaseInvoiceItem(Document):
|
||||
manufacturer_part_no: DF.Data | None
|
||||
margin_rate_or_amount: DF.Float
|
||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
net_amount: DF.Currency
|
||||
net_rate: DF.Currency
|
||||
page_break: DF.Check
|
||||
@@ -77,6 +80,7 @@ class PurchaseInvoiceItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,14 +53,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.013542",
|
||||
"modified": "2024-06-03 17:30:37.012593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -71,7 +70,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -167,6 +167,10 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
doc.force_set_against_income_account()
|
||||
else:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.287007",
|
||||
"modified": "2024-06-06 13:56:37.908879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
@@ -30,13 +30,17 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
"select": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,13 +98,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.740806",
|
||||
"modified": "2024-06-03 17:31:04.472279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -114,7 +115,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,31 +68,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) {
|
||||
this.frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
this.frm
|
||||
.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted"));
|
||||
me.frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
}
|
||||
@@ -161,7 +136,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
const payment_is_overdue = doc.payment_schedule
|
||||
.map((row) => Date.parse(row.due_date) < Date.now())
|
||||
.reduce((prev, current) => prev || current);
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -505,9 +480,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
const for_validate = me.frm.doc.is_return ? true : false;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "set_missing_values",
|
||||
args: {
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
@@ -592,49 +571,6 @@ cur_frm.cscript["Make Delivery Note"] = function () {
|
||||
});
|
||||
};
|
||||
|
||||
cur_frm.fields_dict.cash_bank_account.get_query = function (doc) {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Cash", "Bank"]],
|
||||
["Account", "root_type", "=", "Asset"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict.write_off_account.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Write off cost center
|
||||
//-----------------------
|
||||
cur_frm.fields_dict.write_off_cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Cost Center in Details Table
|
||||
// -----------------------------
|
||||
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.cscript.income_account = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "income_account");
|
||||
};
|
||||
@@ -647,28 +583,6 @@ cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
|
||||
};
|
||||
|
||||
cur_frm.set_query("debit_to", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: "Receivable",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
cur_frm.set_query("asset", "items", function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Asset", "item_code", "=", d.item_code],
|
||||
["Asset", "docstatus", "=", 1],
|
||||
["Asset", "status", "in", ["Submitted", "Partially Depreciated", "Fully Depreciated"]],
|
||||
["Asset", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("customer", "tax_id", "tax_id");
|
||||
@@ -678,71 +592,132 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
frm.set_df_property("packed_items", "cannot_add_rows", true);
|
||||
frm.set_df_property("packed_items", "cannot_delete_rows", true);
|
||||
|
||||
frm.set_query("account_for_change_amount", function () {
|
||||
frm.set_query("cash_bank_account", function (doc) {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Cash", "Bank"]],
|
||||
["Account", "root_type", "=", "Asset"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", ["Cash", "Bank"]],
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("unrealized_profit_loss_account", function () {
|
||||
frm.set_query("debit_to", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
account_type: "Receivable",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("asset", "items", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Asset", "item_code", "=", row.item_code],
|
||||
["Asset", "docstatus", "=", 1],
|
||||
["Asset", "status", "in", ["Submitted", "Partially Depreciated", "Fully Depreciated"]],
|
||||
["Asset", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account_for_change_amount", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", ["Cash", "Bank"]],
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("unrealized_profit_loss_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
root_type: "Liability",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("adjustment_against", function () {
|
||||
frm.set_query("adjustment_against", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
company: doc.company,
|
||||
customer: doc.customer,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("additional_discount_account", function () {
|
||||
frm.set_query("additional_discount_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
report_type: "Profit and Loss",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("income_account", "items", function () {
|
||||
frm.set_query("income_account", "items", function (doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
(frm.custom_make_buttons = {
|
||||
frm.custom_make_buttons = {
|
||||
"Delivery Note": "Delivery",
|
||||
"Sales Invoice": "Return / Credit Note",
|
||||
"Payment Request": "Payment Request",
|
||||
"Payment Entry": "Payment",
|
||||
}),
|
||||
(frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet",
|
||||
filters: { project: doc.project },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// discount account
|
||||
frm.fields_dict["items"].grid.get_field("discount_account").get_query = function (doc) {
|
||||
frm.set_query("time_sheet", "timesheets", function (doc, cdt, cdn) {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet",
|
||||
filters: { project: doc.project },
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("discount_account", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
@@ -750,9 +725,9 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["items"].grid.get_field("deferred_revenue_account").get_query = function (doc) {
|
||||
frm.set_query("deferred_revenue_account", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
root_type: "Liability",
|
||||
@@ -760,7 +735,7 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("company_address", function (doc) {
|
||||
if (!doc.company) {
|
||||
@@ -789,25 +764,23 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
});
|
||||
|
||||
// set get_query for loyalty redemption account
|
||||
frm.fields_dict["loyalty_redemption_account"].get_query = function () {
|
||||
frm.set_query("loyalty_redemption_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
// set get_query for loyalty redemption cost center
|
||||
frm.fields_dict["loyalty_redemption_cost_center"].get_query = function () {
|
||||
frm.set_query("loyalty_redemption_cost_center", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
},
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
|
||||
@@ -215,7 +215,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -1575,12 +1574,14 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.language",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Print Language",
|
||||
"length": 6,
|
||||
"options": "Language",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -2042,7 +2043,7 @@
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
@@ -2126,15 +2127,6 @@
|
||||
"label": "Write Off",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "incoterm",
|
||||
"fieldtype": "Link",
|
||||
@@ -2203,7 +2195,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:30:26.272441",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -136,7 +136,7 @@ class SalesInvoice(SellingController):
|
||||
is_pos: DF.Check
|
||||
is_return: DF.Check
|
||||
items: DF.Table[SalesInvoiceItem]
|
||||
language: DF.Data | None
|
||||
language: DF.Link | None
|
||||
letter_head: DF.Link | None
|
||||
loyalty_amount: DF.Currency
|
||||
loyalty_points: DF.Int
|
||||
@@ -166,7 +166,6 @@ class SalesInvoice(SellingController):
|
||||
project: DF.Link | None
|
||||
redeem_loyalty_points: DF.Check
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -316,17 +315,6 @@ class SalesInvoice(SellingController):
|
||||
if not self.is_opening:
|
||||
self.is_opening = "No"
|
||||
|
||||
if self.redeem_loyalty_points:
|
||||
lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
|
||||
self.loyalty_redemption_account = (
|
||||
lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account
|
||||
)
|
||||
self.loyalty_redemption_cost_center = (
|
||||
lp.cost_center
|
||||
if not self.loyalty_redemption_cost_center
|
||||
else self.loyalty_redemption_cost_center
|
||||
)
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
@@ -345,12 +333,7 @@ class SalesInvoice(SellingController):
|
||||
if self.is_pos and self.is_return:
|
||||
self.verify_payment_amount_is_negative()
|
||||
|
||||
if (
|
||||
self.redeem_loyalty_points
|
||||
and self.loyalty_program
|
||||
and self.loyalty_points
|
||||
and not self.is_consolidated
|
||||
):
|
||||
if self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
@@ -393,6 +376,9 @@ class SalesInvoice(SellingController):
|
||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||
|
||||
if not tax_withholding_details:
|
||||
@@ -452,6 +438,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -524,7 +511,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
@@ -565,7 +552,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if self.coupon_code:
|
||||
update_coupon_code_count(self.coupon_code, "cancelled")
|
||||
@@ -718,24 +704,23 @@ class SalesInvoice(SellingController):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
@@ -873,7 +858,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = (
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||
+ " "
|
||||
)
|
||||
msg += _(
|
||||
@@ -884,7 +869,7 @@ class SalesInvoice(SellingController):
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = (
|
||||
_("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
||||
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||
)
|
||||
+ " "
|
||||
)
|
||||
@@ -955,6 +940,10 @@ class SalesInvoice(SellingController):
|
||||
against_acc.append(d.income_account)
|
||||
self.against_income_account = ",".join(against_acc)
|
||||
|
||||
def force_set_against_income_account(self):
|
||||
self.set_against_income_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_income_account", self.against_income_account)
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
@@ -1210,6 +1199,8 @@ class SalesInvoice(SellingController):
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
@@ -1315,6 +1306,10 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
@@ -1373,37 +1368,33 @@ class SalesInvoice(SellingController):
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -1437,7 +1428,7 @@ class SalesInvoice(SellingController):
|
||||
asset.set_status("Sold" if self.docstatus == 1 else None)
|
||||
|
||||
def make_loyalty_point_redemption_gle(self, gl_entries):
|
||||
if cint(self.redeem_loyalty_points):
|
||||
if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1478,6 +1469,10 @@ class SalesInvoice(SellingController):
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1491,7 +1486,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
@@ -1765,7 +1760,8 @@ class SalesInvoice(SellingController):
|
||||
loyalty_program=self.loyalty_program,
|
||||
include_expired_entry=True,
|
||||
)
|
||||
frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.db_set("loyalty_program_tier", lp_details.tier_name)
|
||||
|
||||
def get_returned_amount(self):
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -2070,7 +2066,7 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
"doctype": "Sales Team",
|
||||
"field_map": {"incentives": "incentives"},
|
||||
@@ -2183,6 +2179,11 @@ def make_inter_company_purchase_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def make_regional_gl_entries(gl_entries, doc):
|
||||
return gl_entries
|
||||
|
||||
|
||||
def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
@@ -2640,6 +2641,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
|
||||
# update outstanding
|
||||
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
|
||||
|
||||
target.validate()
|
||||
|
||||
return get_mapped_doc(
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
@@ -1783,6 +1785,49 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_gle_in_transaction_currency(self):
|
||||
# create multi currency sales invoice with 2 items with same income account
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_submit=True,
|
||||
)
|
||||
# add 2nd item with same income account
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 80,
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select transaction_currency, transaction_exchange_rate,
|
||||
debit_in_transaction_currency, credit_in_transaction_currency
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
expected_gle = {
|
||||
"transaction_currency": "USD",
|
||||
"transaction_exchange_rate": 50,
|
||||
"debit_in_transaction_currency": 0,
|
||||
"credit_in_transaction_currency": 180,
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
for field in expected_gle:
|
||||
self.assertEqual(expected_gle[field], gle[field])
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
@@ -2176,13 +2221,14 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.50],
|
||||
["_Test Account VAT - _TC", 0.0, 114.50],
|
||||
[si.debit_to, 1501, 0.0],
|
||||
["Round Off - _TC", 0.20, 0.0],
|
||||
["Sales - _TC", 0.0, 1272.20],
|
||||
]
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = {
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.50],
|
||||
"_Test Account VAT - _TC": [0.0, 114.50],
|
||||
si.debit_to: [1501, 0.0],
|
||||
round_off_account: [0.20, 0.0],
|
||||
"Sales - _TC": [0.0, 1272.20],
|
||||
}
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
@@ -2193,10 +2239,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
for gle in gl_entries:
|
||||
expected_account_values = expected_values[gle.account]
|
||||
self.assertEqual(expected_account_values[0], gle.debit)
|
||||
self.assertEqual(expected_account_values[1], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
@@ -2245,6 +2291,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
@@ -2252,7 +2299,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.01, 0.0],
|
||||
[round_off_account, 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2281,8 +2328,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
if round_off_gle:
|
||||
self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
|
||||
self.assertEqual(round_off_gle.location, "Block 1")
|
||||
|
||||
disable_dimension()
|
||||
|
||||
@@ -2906,13 +2954,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
@@ -2922,9 +2966,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
si.load_from_db()
|
||||
self.assertFalse(si.repost_required)
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
@@ -3056,6 +3097,84 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
cust_doc.save()
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||
)
|
||||
si.department = "All Departments"
|
||||
si.save().submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"department": "All Departments",
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
dim.disabled = True
|
||||
dim.save()
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3605,6 +3724,65 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
|
||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||
|
||||
def test_loyalty_points_redemption_with_shopping_cart(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import (
|
||||
create_records,
|
||||
create_sales_invoice_record,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Set up loyalty program
|
||||
create_records()
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
create_sales_invoice_record(10).insert().submit()
|
||||
|
||||
# Create a sales order
|
||||
so = make_sales_order(qty=10, do_not_save=True, customer="Test Loyalty Customer")
|
||||
so.name = "_T-Sales Order LP-0001"
|
||||
so.order_type = "Shopping Cart"
|
||||
so.loyalty_points = 50
|
||||
so.loyalty_amount = 50
|
||||
so.insert()
|
||||
so.submit()
|
||||
|
||||
# Create sales invoice from the sales order
|
||||
si = make_sales_invoice(so.name)
|
||||
from frappe.model.trace import traced_field_context
|
||||
|
||||
with traced_field_context(si.__class__, "loyalty_program", forbidden_values=[None]):
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
# Check if loyalty points are applied correctly
|
||||
self.assertEqual(si.loyalty_program, "Test Single Loyalty")
|
||||
self.assertEqual(si.loyalty_points, 50)
|
||||
self.assertEqual(si.loyalty_amount, 50)
|
||||
|
||||
# Check GL entries for loyalty points redemption
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||
fields=["account", "debit", "credit"],
|
||||
)
|
||||
|
||||
loyalty_account = frappe.db.get_value("Loyalty Program", "Test Single Loyalty", "expense_account")
|
||||
expected_gl_entries = [
|
||||
{"account": si.debit_to, "debit": si.grand_total, "credit": 0},
|
||||
{"account": si.items[0].income_account, "debit": 0, "credit": si.net_total},
|
||||
{"account": loyalty_account, "debit": 50, "credit": 0},
|
||||
]
|
||||
|
||||
for entry in expected_gl_entries:
|
||||
self.assertTrue(
|
||||
any(
|
||||
gl_entry.account == entry["account"]
|
||||
and gl_entry.debit == entry["debit"]
|
||||
and gl_entry.credit == entry["credit"]
|
||||
for gl_entry in gl_entries
|
||||
)
|
||||
)
|
||||
|
||||
def test_pulling_advance_based_on_debit_to(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
|
||||
@@ -3690,9 +3868,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
@@ -3731,6 +3909,40 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.update_outstanding_for_self = False
|
||||
pos_return.save().submit()
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gle)
|
||||
.select(gle.against_voucher)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
|
||||
)
|
||||
.run(as_list=1)
|
||||
)
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0][0], pos_return.return_against)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"column_break_19",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"distributed_discount_amount",
|
||||
"base_rate_with_margin",
|
||||
"section_break1",
|
||||
"rate",
|
||||
@@ -253,7 +254,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
|
||||
"fieldname": "discount_and_margin",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
@@ -870,7 +871,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -921,12 +923,18 @@
|
||||
{
|
||||
"fieldname": "column_break_ytgd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.139679",
|
||||
"modified": "2024-06-02 06:14:40.009020",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user