mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-09 08:22:25 +00:00
Compare commits
703 Commits
future_sub
...
michelleal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce21653266 | ||
|
|
755c162093 | ||
|
|
e177ece008 | ||
|
|
47c2127a31 | ||
|
|
efadf94cf3 | ||
|
|
b1c23ba85c | ||
|
|
82b02d6c9b | ||
|
|
8040544216 | ||
|
|
34ad9d33ff | ||
|
|
113362e2d9 | ||
|
|
165ec2ed53 | ||
|
|
faff84c6e5 | ||
|
|
4a4e1d36f2 | ||
|
|
c930f8ba9d | ||
|
|
c8af544ef3 | ||
|
|
0f0b19688a | ||
|
|
4253caf910 | ||
|
|
213b2ba942 | ||
|
|
e78e948b7c | ||
|
|
56620785a0 | ||
|
|
751a25c4b7 | ||
|
|
05b92c5321 | ||
|
|
9ade269b7a | ||
|
|
991069bfbc | ||
|
|
3617b41b95 | ||
|
|
03f3ab522f | ||
|
|
3ffac73598 | ||
|
|
6d2a36c4f9 | ||
|
|
5040e3ca7d | ||
|
|
da22a8df42 | ||
|
|
511a0b9f37 | ||
|
|
5d58eb67a6 | ||
|
|
bb8c9b5a58 | ||
|
|
59d5beee20 | ||
|
|
bb877f4a6b | ||
|
|
cfe2ae604b | ||
|
|
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 | ||
|
|
8eff168d76 | ||
|
|
25dac1f18e | ||
|
|
cb522f8f22 | ||
|
|
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 | ||
|
|
89c458e229 | ||
|
|
ffc5bbfa4c | ||
|
|
92a6fc8c2e | ||
|
|
8ccf8c0ea2 | ||
|
|
972329cc16 | ||
|
|
35981b8730 | ||
|
|
6e1fc33d01 | ||
|
|
c8e85f4bb7 | ||
|
|
bf2c9a7f41 | ||
|
|
be2648245b | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
a9910817f5 | ||
|
|
64e63887be | ||
|
|
c696d13a5e | ||
|
|
8a397a4506 | ||
|
|
1cce7064e0 | ||
|
|
3025501500 | ||
|
|
a2ba0f51a0 | ||
|
|
46d33d27cf | ||
|
|
8141eacb02 | ||
|
|
194f1dc674 | ||
|
|
28f195023e | ||
|
|
2e5b85c329 | ||
|
|
6d539e0fc0 | ||
|
|
1b072228d3 | ||
|
|
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 | ||
|
|
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 | ||
|
|
5f7e06ba6f | ||
|
|
d791fa7460 | ||
|
|
01888c98bc | ||
|
|
581af4eced | ||
|
|
7ef4dbcaf6 | ||
|
|
4e5bc82a17 | ||
|
|
5c9ce575f6 | ||
|
|
a2f878e1d1 | ||
|
|
ee140369a3 | ||
|
|
7032a108a8 | ||
|
|
db1f7fcf63 | ||
|
|
eccd5b4c5d | ||
|
|
05e8b6c9f6 | ||
|
|
d721de13aa | ||
|
|
9941469fca | ||
|
|
c6a3403270 | ||
|
|
0b36cbe307 | ||
|
|
8b3d46610e | ||
|
|
53642e7417 | ||
|
|
baa3fee1bf | ||
|
|
3c9640df27 | ||
|
|
b39f7b1c2f | ||
|
|
ff7e0eaba5 |
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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ ERPNext as a monolith includes the following areas for managing businesses:
|
||||
1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management)
|
||||
1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software)
|
||||
1. [Website Management](https://erpnext.com/open-source-website-builder-software)
|
||||
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
|
||||
1. [Customize ERPNext](https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext)
|
||||
1. [And More](https://erpnext.com/docs/user/manual/en/)
|
||||
|
||||
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/setup/setup_wizard/data/uom_data.json,erpnext.gettext.extractors.uom_data.extract
|
||||
**/setup/doctype/incoterm/incoterms.csv,erpnext.gettext.extractors.incoterms.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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -165,8 +165,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);
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
@@ -260,7 +261,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 +273,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 +310,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",
|
||||
@@ -372,7 +373,7 @@
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone",
|
||||
"options": "\nEmail\nPhone\nOther",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -405,7 +406,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:11.120742",
|
||||
"modified": "2024-06-20 13:54:55.245774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -443,4 +444,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,28 +158,20 @@ 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":
|
||||
if self.payment_channel == "Phone":
|
||||
self.request_phone_payment()
|
||||
else:
|
||||
self.set_payment_request_url()
|
||||
if not (self.mute_email or self.flags.mute_email):
|
||||
self.send_email()
|
||||
self.make_communication_entry()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
@@ -220,14 +212,12 @@ class PaymentRequest(Document):
|
||||
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()
|
||||
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:
|
||||
@@ -240,12 +230,9 @@ class PaymentRequest(Document):
|
||||
return False
|
||||
|
||||
def set_payment_request_url(self):
|
||||
if self.payment_account and self.payment_channel != "Phone":
|
||||
if self.payment_account and self.payment_gateway and self.payment_gateway_validation():
|
||||
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(
|
||||
@@ -283,7 +270,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
|
||||
|
||||
@@ -369,7 +357,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,9 +407,6 @@ 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():
|
||||
@@ -499,6 +491,15 @@ def make_payment_request(**args):
|
||||
"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,10 +514,8 @@ 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()
|
||||
|
||||
@@ -586,19 +585,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,6 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -13,7 +14,13 @@ 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 +36,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):
|
||||
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 +67,25 @@ 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 test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
so_inr.disable_rounded_total = 1
|
||||
@@ -75,6 +116,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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -634,7 +634,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 +654,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 +825,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",
|
||||
@@ -853,7 +851,7 @@
|
||||
],
|
||||
"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",
|
||||
|
||||
@@ -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:
|
||||
@@ -131,6 +131,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 +192,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 +314,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 +400,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 +489,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)
|
||||
@@ -303,6 +309,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 +583,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -158,7 +158,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,
|
||||
|
||||
@@ -340,10 +340,11 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -352,6 +353,7 @@
|
||||
<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>
|
||||
|
||||
@@ -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):
|
||||
@@ -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(
|
||||
@@ -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,7 +1258,10 @@ 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):
|
||||
@@ -1207,7 +1277,7 @@ class PurchaseInvoice(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1637,6 +1707,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 +1751,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 +1765,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,56 @@ 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 set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -105,6 +106,8 @@
|
||||
"purchase_receipt",
|
||||
"pr_detail",
|
||||
"sales_invoice_item",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"item_weight_details",
|
||||
"weight_per_unit",
|
||||
"total_weight",
|
||||
@@ -934,12 +937,44 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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",
|
||||
|
||||
@@ -52,6 +52,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 +79,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
|
||||
@@ -393,6 +392,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 +454,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()
|
||||
|
||||
@@ -565,7 +568,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 +720,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
|
||||
@@ -955,6 +956,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 +1215,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 +1322,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 +1384,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):
|
||||
@@ -2183,6 +2190,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 +2652,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,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
@@ -1783,6 +1784,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 +2220,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 +2238,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 +2290,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 +2298,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 +2327,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 +2953,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 +2965,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 +3096,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
|
||||
|
||||
@@ -3690,9 +3808,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()
|
||||
|
||||
|
||||
@@ -870,7 +870,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -926,7 +927,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.139679",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
||||
|
||||
# Process subscription and create first invoice
|
||||
# Subscription status will be unpaid since due date has already passed
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-04-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multi_currency_subscription(self):
|
||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process()
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
@@ -476,7 +476,7 @@ class TestSubscription(FrappeTestCase):
|
||||
start_date="2021-01-01",
|
||||
submit_invoice=0,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
party="_Test Subscription Customer",
|
||||
party="_Test Subscription Customer John Doe",
|
||||
)
|
||||
|
||||
# create invoices for the first two moths
|
||||
@@ -565,10 +565,16 @@ def create_parties():
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer"
|
||||
customer.billing_currency = "USD"
|
||||
customer.default_currency = "USD"
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer John Doe"
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable - _TC"})
|
||||
customer.insert()
|
||||
|
||||
|
||||
def reset_settings():
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
@@ -20,6 +21,7 @@
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
@@ -28,7 +30,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:52.419915",
|
||||
"modified": "2024-04-30 10:26:48.218294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Account",
|
||||
|
||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
||||
if d.get("account") in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||
|
||||
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||
existing_accounts.append(d.get("account"))
|
||||
|
||||
def validate_thresholds(self):
|
||||
@@ -265,6 +268,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -272,7 +280,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
to_date=tax_details.to_date,
|
||||
party_type=party_type,
|
||||
)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
||||
tax_deducted_on_advances = 0
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
@@ -282,6 +291,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if taxable_vouchers:
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
||||
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
||||
# rest of the below logic to function properly
|
||||
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
||||
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
||||
if tax_deducted_on_advances:
|
||||
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
@@ -358,12 +375,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
AND ja.party in %s
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
AND j.company = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -376,6 +395,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
payment_entry_filters = {
|
||||
"party_type": party_type,
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
||||
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
@@ -418,7 +451,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
@@ -443,6 +476,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
return sum(entries)
|
||||
|
||||
|
||||
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||
"""
|
||||
Only applies for Taxes deducted on Advance Payments
|
||||
"""
|
||||
advance_tax_from_across_fiscal_year = sum(
|
||||
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
||||
)
|
||||
return advance_tax_from_across_fiscal_year
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
@@ -456,6 +499,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils import today
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||
|
||||
|
||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
# create relevant supplier, etc
|
||||
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
make_pan_no_field()
|
||||
|
||||
def tearDown(self):
|
||||
cancel_invoices()
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_cumulative_threshold_tds(self):
|
||||
frappe.db.set_value(
|
||||
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
|
||||
def add_company_to_fy(fy, company):
|
||||
if not [x.company for x in fy.companies if x.company == company]:
|
||||
fy.append("companies", {"company": company})
|
||||
fy.save()
|
||||
|
||||
# setup previous fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company=test_company)
|
||||
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
|
||||
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
|
||||
add_company_to_fy(self.prev_fy, test_company)
|
||||
else:
|
||||
# make previous fiscal year
|
||||
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
|
||||
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
|
||||
self.prev_fy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year_start_date": start,
|
||||
"year_end_date": end,
|
||||
"companies": [{"company": test_company}],
|
||||
}
|
||||
)
|
||||
self.prev_fy.save()
|
||||
|
||||
# setup tax withholding category for previous fiscal year
|
||||
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||
cat.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": self.prev_fy.year_start_date,
|
||||
"to_date": self.prev_fy.year_end_date,
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000,
|
||||
},
|
||||
)
|
||||
cat.save()
|
||||
|
||||
def test_tds_across_fiscal_year(self):
|
||||
"""
|
||||
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||
--||-----FY 2023-----||-----FY 2024-----||--
|
||||
--||-----Advance-----||---Inv1---Inv2---||--
|
||||
"""
|
||||
self.set_previous_fy_and_tax_category()
|
||||
supplier = "Test TDS Supplier"
|
||||
# Cumulative threshold 30000 and tax rate 10%
|
||||
category = "Cumulative Threshold TDS"
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
supplier,
|
||||
{
|
||||
"tax_withholding_category": category,
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
|
||||
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
|
||||
po.transaction_date = po_and_advance_posting_date
|
||||
po.taxes = []
|
||||
po.apply_tds = False
|
||||
po.tax_withholding_category = None
|
||||
po.save().submit()
|
||||
|
||||
# Partial advance
|
||||
payment = get_payment_entry(po.doctype, po.name)
|
||||
payment.posting_date = po_and_advance_posting_date
|
||||
payment.paid_amount = 60000
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = category
|
||||
payment.references = []
|
||||
payment.taxes = []
|
||||
payment.save().submit()
|
||||
|
||||
self.assertEqual(len(payment.taxes), 1)
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
|
||||
# Multiple partial invoices
|
||||
payment.reload()
|
||||
pi1 = make_purchase_invoice(source_name=po.name)
|
||||
pi1.apply_tds = True
|
||||
pi1.tax_withholding_category = category
|
||||
pi1.items[0].qty = 3
|
||||
pi1.items[0].rate = 10000
|
||||
advances = pi1.get_advance_entries()
|
||||
pi1.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi1.save().submit()
|
||||
pi1.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
|
||||
|
||||
pi2 = make_purchase_invoice(source_name=po.name)
|
||||
pi2.apply_tds = True
|
||||
pi2.tax_withholding_category = category
|
||||
pi2.items[0].qty = 3
|
||||
pi2.items[0].rate = 10000
|
||||
advances = pi2.get_advance_entries()
|
||||
pi2.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
pi2.save().submit()
|
||||
pi2.reload()
|
||||
payment.reload()
|
||||
self.assertEqual(pi2.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
|
||||
@@ -107,7 +107,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
||||
def test_02_unreconcile_one_payment_among_multi_payments(self):
|
||||
"""
|
||||
Scenario: 2 payments, both split against 2 different invoices
|
||||
Unreconcile only one payment from one invoice
|
||||
|
||||
@@ -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
|
||||
@@ -163,7 +165,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||
from frappe.utils.dashboard import cache_source
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -240,10 +241,16 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||
entry.debit_in_account_currency
|
||||
)
|
||||
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||
entry.debit_in_transaction_currency
|
||||
)
|
||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||
entry.credit_in_account_currency
|
||||
)
|
||||
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||
entry.credit_in_transaction_currency
|
||||
)
|
||||
else:
|
||||
merged_gl_map.append(entry)
|
||||
|
||||
@@ -304,6 +311,18 @@ def check_if_in_list(gle, gl_map):
|
||||
def toggle_debit_credit_if_negative(gl_map):
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
|
||||
entry.credit *= -1
|
||||
entry.debit *= -1
|
||||
|
||||
if (
|
||||
flt(entry.debit_in_account_currency) < 0
|
||||
and flt(entry.credit_in_account_currency) < 0
|
||||
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
|
||||
):
|
||||
entry.credit_in_account_currency *= -1
|
||||
entry.debit_in_account_currency *= -1
|
||||
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
@@ -574,6 +593,8 @@ def make_reverse_gl_entries(
|
||||
and make reverse gl entries by swapping debit and credit
|
||||
"""
|
||||
|
||||
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
||||
|
||||
if not gl_entries:
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
@@ -605,7 +626,6 @@ def make_reverse_gl_entries(
|
||||
for x in gl_entries:
|
||||
query = (
|
||||
frappe.qb.update(gle)
|
||||
.set(gle.is_cancelled, True)
|
||||
.set(gle.modified, now())
|
||||
.set(gle.modified_by, frappe.session.user)
|
||||
.where(
|
||||
@@ -620,9 +640,14 @@ def make_reverse_gl_entries(
|
||||
& (gle.voucher_detail_no == x.voucher_detail_no)
|
||||
)
|
||||
)
|
||||
|
||||
if not immutable_ledger_enabled:
|
||||
query = query.set(gle.is_cancelled, True)
|
||||
|
||||
query.run()
|
||||
else:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
@@ -641,6 +666,10 @@ def make_reverse_gl_entries(
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
if immutable_ledger_enabled:
|
||||
new_gle["is_cancelled"] = 0
|
||||
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||
|
||||
if new_gle["debit"] or new_gle["credit"]:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
@@ -733,3 +762,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
|
||||
def is_immutable_ledger_enabled():
|
||||
return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||
|
||||
@@ -188,7 +188,9 @@ def set_address_details(
|
||||
*,
|
||||
ignore_permissions=False,
|
||||
):
|
||||
billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
|
||||
)
|
||||
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
||||
|
||||
if filters.account_type:
|
||||
conditions["account_type"] = filters.account_type
|
||||
return conditions
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -139,6 +139,7 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -253,7 +254,7 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if ple.cost_center:
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
@@ -288,13 +289,13 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
@@ -501,8 +502,9 @@ class ReceivablePayableReport:
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
# If no or single payment terms, no need to split the row
|
||||
if len(payment_terms_details) <= 1:
|
||||
# If single payment terms, no need to split the row
|
||||
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||
return
|
||||
|
||||
for d in payment_terms_details:
|
||||
@@ -1027,20 +1029,6 @@ class ReceivablePayableReport:
|
||||
fieldtype="Link",
|
||||
options="Contact",
|
||||
)
|
||||
if self.filters.party_type == "Customer":
|
||||
self.add_column(
|
||||
_("Customer Name"),
|
||||
fieldname="customer_name",
|
||||
fieldtype="Link",
|
||||
options="Customer",
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
_("Supplier Name"),
|
||||
fieldname="supplier_name",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
|
||||
@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
def create_payment_entry(self, docname, do_not_submit=False):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
if not do_not_submit:
|
||||
pe.submit()
|
||||
return pe
|
||||
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
@@ -955,3 +957,69 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def test_accounts_receivable_output_for_minor_outstanding(self):
|
||||
"""
|
||||
AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 99.99, 0.01]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
def test_cost_center_on_report_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.cost_center = self.cost_center
|
||||
si.save().submit()
|
||||
|
||||
new_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "East Wing",
|
||||
"parent_cost_center": self.company + " - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_cc.save()
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
pe = self.create_payment_entry(si.name, do_not_submit=True)
|
||||
pe.cost_center = new_cc.name
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [si.name, si.cost_center, 60]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
|
||||
@@ -75,11 +76,24 @@ def get_data(filters):
|
||||
asset_data = assets_details.get(d.against_voucher)
|
||||
if asset_data:
|
||||
if not asset_data.get("accumulated_depreciation_amount"):
|
||||
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
|
||||
"opening_accumulated_depreciation"
|
||||
)
|
||||
AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
|
||||
DepreciationSchedule = DocType("Depreciation Schedule")
|
||||
query = (
|
||||
frappe.qb.from_(DepreciationSchedule)
|
||||
.join(AssetDepreciationSchedule)
|
||||
.on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
|
||||
.select(DepreciationSchedule.accumulated_depreciation_amount)
|
||||
.where(
|
||||
(AssetDepreciationSchedule.asset == d.against_voucher)
|
||||
& (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
|
||||
|
||||
row = frappe._dict(asset_data)
|
||||
row.update(
|
||||
|
||||
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT asset_category,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
from `tabAsset` a
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by asset_category
|
||||
and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
|
||||
@@ -109,7 +109,7 @@ def get_provisional_profit_loss(
|
||||
):
|
||||
provisional_profit_loss = {}
|
||||
total_row = {}
|
||||
if asset and (liability or equity):
|
||||
if asset:
|
||||
total = total_row_total = 0
|
||||
currency = currency or frappe.get_cached_value("Company", company, "default_currency")
|
||||
total_row = {
|
||||
@@ -122,14 +122,16 @@ def get_provisional_profit_loss(
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
effective_liability = 0.0
|
||||
total_assets = flt(asset[0].get(key))
|
||||
effective_liability = 0.00
|
||||
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
|
||||
provisional_profit_loss[key] = flt(asset[0].get(key)) - effective_liability
|
||||
total_row[key] = effective_liability + provisional_profit_loss[key]
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||
|
||||
if provisional_profit_loss[key]:
|
||||
has_value = True
|
||||
|
||||
@@ -104,7 +104,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
report_summary = get_bs_summary(
|
||||
report_summary, primitive_summary = get_bs_summary(
|
||||
companies,
|
||||
asset,
|
||||
liability,
|
||||
@@ -175,7 +175,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
report_summary = get_pl_summary(
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
)
|
||||
|
||||
@@ -266,6 +266,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
filters.end_date = fiscal_year.year_end_date
|
||||
|
||||
for company in companies:
|
||||
filters.company = company
|
||||
amount = get_account_type_based_gl_data(company, filters)
|
||||
|
||||
if amount and account_type == "Depreciation":
|
||||
|
||||
@@ -58,9 +58,9 @@ class Deferred_Item:
|
||||
For a given GL/Journal posting, get balance based on item type
|
||||
"""
|
||||
if self.type == "Deferred Sale Item":
|
||||
return entry.debit - entry.credit
|
||||
return flt(entry.debit) - flt(entry.credit)
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
return -(entry.credit - entry.debit)
|
||||
return -(flt(entry.credit) - flt(entry.debit))
|
||||
return 0
|
||||
|
||||
def get_item_total(self):
|
||||
@@ -147,7 +147,7 @@ class Deferred_Item:
|
||||
actual = 0
|
||||
for posting in self.gle_entries:
|
||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
|
||||
period_sum += self.get_amount(posting)
|
||||
if posting.posted == "posted":
|
||||
actual += self.get_amount(posting)
|
||||
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
qb.from_(inv_item)
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.join(gle)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
|
||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_zero_amount(self):
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 12
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=3910,
|
||||
price_list_rate=3910,
|
||||
warehouse=self.warehouse,
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-12-30"
|
||||
pi.items[0].service_end_date = "2022-12-30"
|
||||
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||
pi.items[0].expense_account = self.expense_account
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2022-01-01",
|
||||
end_date="2022-01-31",
|
||||
type="Expense",
|
||||
company=self.company,
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2022-01-01",
|
||||
"period_end_date": "2022-01-31",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
|
||||
# fetch the invoice from deferred invoices list
|
||||
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||
# make sure the list isn't empty
|
||||
self.assertTrue(inv)
|
||||
# calculate the total deferred expense for the period
|
||||
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||
# make sure the total deferred expense is greater than 0
|
||||
self.assertLess(deferred_exp, 0)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user