mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-30 22:38:35 +00:00
Compare commits
562 Commits
dembug
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
927cd173cf | ||
|
|
216563d1e3 | ||
|
|
b2bb4d9020 | ||
|
|
fcdf1b4241 | ||
|
|
62c3915ecb | ||
|
|
9078e3798d | ||
|
|
e803ed72e6 | ||
|
|
5bee30ff2f | ||
|
|
464e3339fe | ||
|
|
912f9bfd8e | ||
|
|
1c124b3c90 | ||
|
|
d2fad44e89 | ||
|
|
999f1cf96d | ||
|
|
8fb09decd2 | ||
|
|
93f461c6f3 | ||
|
|
48f9769c05 | ||
|
|
e965c44908 | ||
|
|
0c1e8bbfbb | ||
|
|
453bcd7513 | ||
|
|
ded0aab680 | ||
|
|
079cf772aa | ||
|
|
2dbef23244 | ||
|
|
967f5ac7f3 | ||
|
|
fcf374928f | ||
|
|
ac640a22b7 | ||
|
|
221f1468cb | ||
|
|
da4b28bf2f | ||
|
|
5a6e06a140 | ||
|
|
45d72cbefc | ||
|
|
fd3fec3912 | ||
|
|
936d7d4342 | ||
|
|
2394e76e7d | ||
|
|
f62aa8fc57 | ||
|
|
8284efdf66 | ||
|
|
3b2879d3a1 | ||
|
|
b88305a95f | ||
|
|
987a95d2b5 | ||
|
|
c3b45c933a | ||
|
|
1ec182430d | ||
|
|
8e6959dfad | ||
|
|
a91fe5cbb3 | ||
|
|
8c5aea15ac | ||
|
|
f68730f444 | ||
|
|
cd4ba69262 | ||
|
|
78dedc0773 | ||
|
|
75379b79fc | ||
|
|
83cafb892c | ||
|
|
a1cbcda4c5 | ||
|
|
df83e427a3 | ||
|
|
7f8d08c8eb | ||
|
|
8e42764274 | ||
|
|
65c45a3f5b | ||
|
|
941085000a | ||
|
|
17a2f44290 | ||
|
|
24394765a6 | ||
|
|
fa31a0282e | ||
|
|
38e1054eb7 | ||
|
|
2bb79197aa | ||
|
|
73e82b7afa | ||
|
|
4821c44227 | ||
|
|
44c1425e73 | ||
|
|
4b87610d95 | ||
|
|
448a5db20f | ||
|
|
5fed3866b6 | ||
|
|
f05c1d7e7b | ||
|
|
32f616ad87 | ||
|
|
93a8440908 | ||
|
|
be813b5bba | ||
|
|
ac9e5c0163 | ||
|
|
4487edb255 | ||
|
|
8234e659c8 | ||
|
|
927eae79d5 | ||
|
|
8cfab57fc8 | ||
|
|
74621eeb1b | ||
|
|
1eea76cbad | ||
|
|
3333331a3d | ||
|
|
dfc3dc4944 | ||
|
|
7c7aa831ec | ||
|
|
70260c8c86 | ||
|
|
c46af238d4 | ||
|
|
7e61b67ba8 | ||
|
|
5e8124900f | ||
|
|
3788339ea0 | ||
|
|
fe47ca8566 | ||
|
|
844f1636c0 | ||
|
|
f3d598881c | ||
|
|
2fcc5e3f55 | ||
|
|
fe43975cdd | ||
|
|
c2b4a79b5b | ||
|
|
98cc1558d3 | ||
|
|
f206034a79 | ||
|
|
6f1bc5225a | ||
|
|
5506b44b6f | ||
|
|
5880f1d5c6 | ||
|
|
d94802067b | ||
|
|
6c8cb9717d | ||
|
|
cdd41373b6 | ||
|
|
419b149d05 | ||
|
|
019f6422be | ||
|
|
947a4fb091 | ||
|
|
03d515208a | ||
|
|
9e4c82e055 | ||
|
|
60a5f4f30d | ||
|
|
bee26e046e | ||
|
|
e6b6849940 | ||
|
|
6e67a1ba7c | ||
|
|
f0a6399056 | ||
|
|
c81ce29e4c | ||
|
|
0a364ed1c9 | ||
|
|
dbac8cfc94 | ||
|
|
43b13b91be | ||
|
|
914ad357fd | ||
|
|
876082ea2f | ||
|
|
706cb64279 | ||
|
|
7324dcb7c8 | ||
|
|
fd4c4f98fa | ||
|
|
a81867bc43 | ||
|
|
6886f5ef41 | ||
|
|
72f8fc8a7b | ||
|
|
5f554e1ec3 | ||
|
|
2f77a8bed1 | ||
|
|
b839094551 | ||
|
|
a007dc285d | ||
|
|
3e990153a4 | ||
|
|
591ad7641f | ||
|
|
4bf85d1a5a | ||
|
|
6ab2106322 | ||
|
|
c522071b58 | ||
|
|
5df8609b33 | ||
|
|
4dcac56486 | ||
|
|
da1b4cb9ab | ||
|
|
8d8f3afb39 | ||
|
|
8af9dcb33e | ||
|
|
b95b13ecd8 | ||
|
|
6591e76a63 | ||
|
|
c33c6b6560 | ||
|
|
0fdaa541f1 | ||
|
|
0bae273d41 | ||
|
|
494310293c | ||
|
|
3a4ae8c463 | ||
|
|
110412d8ad | ||
|
|
675b3330d9 | ||
|
|
8296e28689 | ||
|
|
3977a7a06e | ||
|
|
1c725dee5d | ||
|
|
e8871257a2 | ||
|
|
a11b8db389 | ||
|
|
3c8214827c | ||
|
|
2bd596ee3d | ||
|
|
0069581aa3 | ||
|
|
cbcdc5e7e5 | ||
|
|
0b9c28620f | ||
|
|
8fa39bec61 | ||
|
|
03290ef45f | ||
|
|
71a507d30b | ||
|
|
e6176db2c9 | ||
|
|
bcad6cb757 | ||
|
|
95f6b586ff | ||
|
|
c636cc33b3 | ||
|
|
9b5c4a0144 | ||
|
|
1bb6cd33a3 | ||
|
|
4178154b19 | ||
|
|
35388e7a04 | ||
|
|
91f6c65c0a | ||
|
|
7137331355 | ||
|
|
5e2879e9ea | ||
|
|
06a93ffc80 | ||
|
|
862ca7ddd3 | ||
|
|
e4530232e4 | ||
|
|
ed7ad79c2c | ||
|
|
ec9af4ae29 | ||
|
|
5c002df014 | ||
|
|
814be2b80a | ||
|
|
b0bb3ca798 | ||
|
|
7e752756cf | ||
|
|
613c2a0379 | ||
|
|
e2b586d5a3 | ||
|
|
b0d197119f | ||
|
|
8785342fce | ||
|
|
0809e00455 | ||
|
|
6077c248b0 | ||
|
|
0207d2d7b6 | ||
|
|
7b955b2ea6 | ||
|
|
772776ad8a | ||
|
|
0df18daf10 | ||
|
|
8f3cc6af16 | ||
|
|
88e68bb803 | ||
|
|
b80b5574d3 | ||
|
|
2d32ddacc3 | ||
|
|
0cdd346f8f | ||
|
|
3d5b52f3bf | ||
|
|
c83398471c | ||
|
|
820b32eb8a | ||
|
|
fc8663421b | ||
|
|
33d03b1542 | ||
|
|
fd8c78628f | ||
|
|
0c9d0ea1f4 | ||
|
|
99e721e622 | ||
|
|
5ff540bd82 | ||
|
|
7d47869f4b | ||
|
|
f8472c32d9 | ||
|
|
325c4e3536 | ||
|
|
52c687ecc9 | ||
|
|
ac3259b8f1 | ||
|
|
366ae85d85 | ||
|
|
19c01b1457 | ||
|
|
133e0417b8 | ||
|
|
d0479036bb | ||
|
|
54a8d41f88 | ||
|
|
e72687d3eb | ||
|
|
fb7bba8cd0 | ||
|
|
ab486d2515 | ||
|
|
2d7a576da5 | ||
|
|
6a2f2ae3fe | ||
|
|
723e902470 | ||
|
|
f6fca3acec | ||
|
|
218670a720 | ||
|
|
fee3846144 | ||
|
|
417bf49a8d | ||
|
|
54279ee21d | ||
|
|
98e727a061 | ||
|
|
79d4852d7d | ||
|
|
07adfadd58 | ||
|
|
7f34b490f4 | ||
|
|
85378f9d1a | ||
|
|
899c18df18 | ||
|
|
848b98e6d6 | ||
|
|
b57a2612ce | ||
|
|
0edba914fa | ||
|
|
bfc01441a0 | ||
|
|
fb3f08a441 | ||
|
|
18127603fe | ||
|
|
cd87ad0613 | ||
|
|
ffd10d1fe9 | ||
|
|
d1c927530e | ||
|
|
c92ec312b9 | ||
|
|
3947b5943e | ||
|
|
ab2d6fdefd | ||
|
|
a20116816e | ||
|
|
7e24395e00 | ||
|
|
8720d412bd | ||
|
|
51a65899ec | ||
|
|
a44be73a98 | ||
|
|
2482a3a205 | ||
|
|
716edeb465 | ||
|
|
fee318a275 | ||
|
|
f82837a4a2 | ||
|
|
9950e4aa0c | ||
|
|
1e4b9fbdf0 | ||
|
|
4c39dc5493 | ||
|
|
2394dd71e6 | ||
|
|
9422ce5aee | ||
|
|
2c8e3f3409 | ||
|
|
423decb93c | ||
|
|
daf9c9d34a | ||
|
|
d762180231 | ||
|
|
81978a0bd8 | ||
|
|
7b6e8b9c29 | ||
|
|
3cdaa80526 | ||
|
|
adfbfe5b32 | ||
|
|
827afbfa2e | ||
|
|
1fb3a4333d | ||
|
|
4b434d01f3 | ||
|
|
cd0faf12a4 | ||
|
|
1362e2b6bf | ||
|
|
018df3135a | ||
|
|
0a25fe981b | ||
|
|
60da21be55 | ||
|
|
3cdc636111 | ||
|
|
bd2c3711c0 | ||
|
|
589325e14b | ||
|
|
cd20d7aa8e | ||
|
|
99c3fb6ba5 | ||
|
|
2a258c1629 | ||
|
|
4b345cf495 | ||
|
|
33ce49730a | ||
|
|
95fda47b6c | ||
|
|
75567dc9ff | ||
|
|
1607aa1a44 | ||
|
|
1a83316112 | ||
|
|
707c01487e | ||
|
|
5be2e71a35 | ||
|
|
5676d60ed3 | ||
|
|
3e35c48cf7 | ||
|
|
f976115a2b | ||
|
|
e2db3b9cfc | ||
|
|
2c94867b0e | ||
|
|
a913c9b202 | ||
|
|
fe51535392 | ||
|
|
701fc02050 | ||
|
|
5643385c22 | ||
|
|
5a1851dfe3 | ||
|
|
437444fe04 | ||
|
|
00f9a36980 | ||
|
|
41649cf52d | ||
|
|
592704cfd0 | ||
|
|
b3ffb82586 | ||
|
|
e61ab48145 | ||
|
|
b8ee15269c | ||
|
|
ce7702cc19 | ||
|
|
9faf78d3e5 | ||
|
|
6b454ca9a7 | ||
|
|
9a34c4c1ec | ||
|
|
4dd37ba033 | ||
|
|
968e235a3f | ||
|
|
90eefac6f6 | ||
|
|
4c8dff942d | ||
|
|
378afd2f65 | ||
|
|
aaf720ab61 | ||
|
|
d042c841e4 | ||
|
|
409e512d47 | ||
|
|
3f2e93dcb6 | ||
|
|
79b5a3e1dd | ||
|
|
72b940e3d3 | ||
|
|
9ae8e94e6c | ||
|
|
d1086722bf | ||
|
|
d748b491ee | ||
|
|
5e8e7dd3d8 | ||
|
|
5a023dc8d4 | ||
|
|
9f20854bd9 | ||
|
|
a64a4f9b20 | ||
|
|
ac32c554af | ||
|
|
ceb4f249cb | ||
|
|
2609f9809d | ||
|
|
dd77070351 | ||
|
|
4d050441b3 | ||
|
|
8e77b26641 | ||
|
|
b08da0f6bd | ||
|
|
b1eb604363 | ||
|
|
7bc075376b | ||
|
|
7e7209e448 | ||
|
|
9964ddc0e9 | ||
|
|
9933d3c8ff | ||
|
|
813cfdfff6 | ||
|
|
0b1b964b77 | ||
|
|
d1fbeb11cb | ||
|
|
dc7e7118af | ||
|
|
26afba142e | ||
|
|
2c644ec2ef | ||
|
|
47c2c5377c | ||
|
|
3078578692 | ||
|
|
49570a5544 | ||
|
|
e88f96b92d | ||
|
|
e55d4030ec | ||
|
|
db38e7bf5a | ||
|
|
11f65f20a0 | ||
|
|
2ac8c92e7f | ||
|
|
fb285749dd | ||
|
|
887645e55f | ||
|
|
af97f42429 | ||
|
|
eabdd5992b | ||
|
|
29341245b8 | ||
|
|
b5afd901f3 | ||
|
|
2e8cde3378 | ||
|
|
d97e78e5d3 | ||
|
|
55733d4f18 | ||
|
|
bee2c04d0b | ||
|
|
53704b98b5 | ||
|
|
bad1ac9fbc | ||
|
|
4008ca5ddd | ||
|
|
9f3b8520fe | ||
|
|
d862e9b771 | ||
|
|
97acbb3134 | ||
|
|
a9bc395e98 | ||
|
|
42edb9f5b1 | ||
|
|
e82911041d | ||
|
|
3d7f1026ca | ||
|
|
54d234e05d | ||
|
|
78c7c1c631 | ||
|
|
8028dd2683 | ||
|
|
cd3f03696e | ||
|
|
6ec18fb40d | ||
|
|
5bf90dccbb | ||
|
|
9888c62e13 | ||
|
|
5088d8576f | ||
|
|
d92f9330fa | ||
|
|
d1329c2910 | ||
|
|
1758e125e0 | ||
|
|
85b6fdd067 | ||
|
|
c889bdba0f | ||
|
|
766b5fa5ed | ||
|
|
3923b784e3 | ||
|
|
7b8a099d95 | ||
|
|
a71718883e | ||
|
|
2f3281579a | ||
|
|
b2c3da135e | ||
|
|
95af63e305 | ||
|
|
f79eea2261 | ||
|
|
5257413a93 | ||
|
|
41dda35db7 | ||
|
|
50223c6bec | ||
|
|
2a400dd3f8 | ||
|
|
551fa500e8 | ||
|
|
fe43d20545 | ||
|
|
8e18c572f4 | ||
|
|
f07a71a882 | ||
|
|
c5f21a5686 | ||
|
|
4a7586cc01 | ||
|
|
3fbd2ca0d9 | ||
|
|
b26f0b6633 | ||
|
|
05579959f2 | ||
|
|
4481ca83ff | ||
|
|
9ff3101b2d | ||
|
|
8b6a20d501 | ||
|
|
be2593bb51 | ||
|
|
5207917993 | ||
|
|
84d379914c | ||
|
|
f4d1a54588 | ||
|
|
1c6a7830c3 | ||
|
|
4455312b73 | ||
|
|
36bae55299 | ||
|
|
dedb96d337 | ||
|
|
d73c17aa98 | ||
|
|
4f55356c79 | ||
|
|
993f40fa43 | ||
|
|
2e535955b3 | ||
|
|
dd923332cb | ||
|
|
7b0c21e989 | ||
|
|
6c32397313 | ||
|
|
5638dac414 | ||
|
|
e2a32b7257 | ||
|
|
7c6e279599 | ||
|
|
68fb1b28eb | ||
|
|
47f8a86003 | ||
|
|
5fb158a6f7 | ||
|
|
30e87c37dc | ||
|
|
f9200a9575 | ||
|
|
3697ba0772 | ||
|
|
1d81a9f933 | ||
|
|
da34d7923d | ||
|
|
04364d680f | ||
|
|
34a80bfcd3 | ||
|
|
76ba17808b | ||
|
|
397cd79e1e | ||
|
|
97e3770872 | ||
|
|
8f9b5aaae7 | ||
|
|
d13c03676b | ||
|
|
98d401bee4 | ||
|
|
ec487c14d9 | ||
|
|
2d45d0e5d5 | ||
|
|
ada272a29b | ||
|
|
8d66142865 | ||
|
|
84e0b41c4f | ||
|
|
19c8708e5e | ||
|
|
aa38895caf | ||
|
|
9163f60191 | ||
|
|
454067198e | ||
|
|
c94091d68f | ||
|
|
60efd3e219 | ||
|
|
0e7e9b5f0a | ||
|
|
87de5c7450 | ||
|
|
5f15b0b65b | ||
|
|
063a205e5a | ||
|
|
43ce185429 | ||
|
|
318a945d66 | ||
|
|
6628d290de | ||
|
|
4fb48b7f22 | ||
|
|
ed2764ab8d | ||
|
|
a7cd17ac8b | ||
|
|
484ecf2479 | ||
|
|
2af6fca7fa | ||
|
|
2bf10f68a8 | ||
|
|
cb91e8e69e | ||
|
|
9607b16dcf | ||
|
|
37a5767be5 | ||
|
|
195254756f | ||
|
|
a4453fb77b | ||
|
|
6edb454eea | ||
|
|
001d1eb3f9 | ||
|
|
3a2e816759 | ||
|
|
ce9c606f71 | ||
|
|
09d26a835f | ||
|
|
aea69af4ff | ||
|
|
fd442a36a9 | ||
|
|
9ee5fcc602 | ||
|
|
d9013e1054 | ||
|
|
9bb7d4e428 | ||
|
|
a3165c5719 | ||
|
|
601bc3026d | ||
|
|
aeedaae761 | ||
|
|
2e8723407f | ||
|
|
4b483e176d | ||
|
|
f7a93640b0 | ||
|
|
8f8cddb03c | ||
|
|
ea4526ad30 | ||
|
|
6c5a9f5a74 | ||
|
|
bbb5f8056b | ||
|
|
83bb3926b1 | ||
|
|
9fa1865cb7 | ||
|
|
88ab9be79c | ||
|
|
2936139c79 | ||
|
|
a4271aa5d1 | ||
|
|
7e7775aa44 | ||
|
|
e529f82392 | ||
|
|
eea0eff001 | ||
|
|
6eb3b0b1c2 | ||
|
|
53c282c86e | ||
|
|
959434601d | ||
|
|
c6c7d7832a | ||
|
|
edb254e43e | ||
|
|
455ef6f084 | ||
|
|
61efb2bb39 | ||
|
|
856ec08484 | ||
|
|
31005c5984 | ||
|
|
14ce2337df | ||
|
|
e0517852bc | ||
|
|
e4d3235b9c | ||
|
|
5ccf4a1783 | ||
|
|
1ef9f7f8fd | ||
|
|
376bdc75f4 | ||
|
|
c8e93e7a61 | ||
|
|
bb8d2c994c | ||
|
|
fb6c72a247 | ||
|
|
a8a8ac71b6 | ||
|
|
8b2c981fc3 | ||
|
|
108a91d788 | ||
|
|
1f1c01d88e | ||
|
|
90339511aa | ||
|
|
a9b761f862 | ||
|
|
4ec824d71e | ||
|
|
bb9dd7b8cc | ||
|
|
b291835ccd | ||
|
|
2e930eb97b | ||
|
|
00dadc1a89 | ||
|
|
b4ceda6f2c | ||
|
|
7ea73d8265 | ||
|
|
9e760e54a5 | ||
|
|
e614f07795 | ||
|
|
81d8f257aa | ||
|
|
dbb572eec1 | ||
|
|
2b453219fc | ||
|
|
ca44a31420 | ||
|
|
4620025dcd | ||
|
|
5df9a8ab99 | ||
|
|
731822efac | ||
|
|
a24d7e8ecd | ||
|
|
729ce1dc50 | ||
|
|
31dd32dcdf | ||
|
|
6850019649 | ||
|
|
2788739c1e | ||
|
|
9f77793f16 | ||
|
|
a0f17f8e73 | ||
|
|
dc5bff9008 | ||
|
|
8521796811 | ||
|
|
61d4593236 | ||
|
|
d7bf73cffa | ||
|
|
8de0fe78ea | ||
|
|
87f1f6e15c | ||
|
|
1f2d7da426 | ||
|
|
f996f71d16 | ||
|
|
839b79ffd0 | ||
|
|
ce99764772 | ||
|
|
058fdca981 | ||
|
|
2f80c4dee5 | ||
|
|
f46f1bead4 | ||
|
|
5dacfd5cda | ||
|
|
015fd4a05b | ||
|
|
a203e3ffaf | ||
|
|
22d38c2af4 | ||
|
|
185bbb4c20 | ||
|
|
547c8004eb | ||
|
|
d541259da9 |
4
.github/workflows/server-tests-mariadb.yml
vendored
4
.github/workflows/server-tests-mariadb.yml
vendored
@@ -133,7 +133,7 @@ jobs:
|
||||
run: cat ~/frappe-bench/bench_start.log || true
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v4
|
||||
|
||||
56
.mergify.yml
56
.mergify.yml
@@ -2,29 +2,27 @@ pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=frappe-pr-bot
|
||||
- author!=mergify[bot]
|
||||
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
- base=version-16
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=frappe-pr-bot
|
||||
- author!=mergify[bot]
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
- base=version-14
|
||||
- base=version-15
|
||||
- base=version-16
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
@@ -34,7 +32,6 @@ pull_request_rules:
|
||||
- develop
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-14-hotfix
|
||||
conditions:
|
||||
- label="backport version-14-hotfix"
|
||||
@@ -44,7 +41,6 @@ pull_request_rules:
|
||||
- version-14-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-15-hotfix
|
||||
conditions:
|
||||
- label="backport version-15-hotfix"
|
||||
@@ -54,18 +50,6 @@ pull_request_rules:
|
||||
- version-15-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-13-hotfix
|
||||
conditions:
|
||||
- label="backport version-13-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- status-success=linters
|
||||
@@ -96,6 +80,6 @@ pull_request_rules:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
{{ body }}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"allow_copy": 1,
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-30 12:49:46",
|
||||
"default_view": "Tree",
|
||||
"description": "Heads (or groups) against which Accounting Entries are made and balances are maintained.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
@@ -194,7 +195,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-19 15:19:11.095045",
|
||||
"modified": "2025-01-22 10:40:35.766017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||
|
||||
import erpnext
|
||||
@@ -479,6 +479,7 @@ def get_account_autoname(account_number, account_name, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
if not account:
|
||||
return
|
||||
@@ -540,6 +541,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
@@ -593,3 +595,27 @@ def sync_update_account_number_in_child(
|
||||
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
|
||||
def _ensure_idle_system():
|
||||
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
# We also lock inserts to GL entry table with for_update here.
|
||||
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||
except frappe.QueryTimeoutError:
|
||||
# wait=False fails immediately if there's an active transaction.
|
||||
last_gl_update = add_to_date(None, seconds=-1)
|
||||
|
||||
if last_gl_update > add_to_date(None, minutes=-5):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ frappe.treeview_settings["Account"] = {
|
||||
fieldtype: "Select",
|
||||
options: erpnext.utils.get_tree_options("company"),
|
||||
label: __("Company"),
|
||||
render_on_toolbar: true,
|
||||
default: erpnext.utils.get_tree_default("company"),
|
||||
on_change: function () {
|
||||
var me = frappe.treeview_settings["Account"].treeview;
|
||||
@@ -182,7 +183,9 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("Tree", "Cost Center", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
__("View"),
|
||||
"default",
|
||||
true
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
@@ -190,31 +193,12 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("Form", "Opening Invoice Creation Tool", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
__("View"),
|
||||
"default",
|
||||
true
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
__("Period Closing Voucher"),
|
||||
function () {
|
||||
frappe.set_route("List", "Period Closing Voucher", { company: get_company() });
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
|
||||
treeview.page.add_inner_button(
|
||||
__("Journal Entry"),
|
||||
function () {
|
||||
frappe.new_doc("Journal Entry", { company: get_company() });
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
treeview.page.add_inner_button(
|
||||
__("Company"),
|
||||
function () {
|
||||
frappe.new_doc("Company");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
treeview.page.add_divider_to_button_group(__("View"));
|
||||
|
||||
// financial statements
|
||||
for (let report of [
|
||||
@@ -231,7 +215,7 @@ frappe.treeview_settings["Account"] = {
|
||||
function () {
|
||||
frappe.set_route("query-report", report, { company: get_company() });
|
||||
},
|
||||
__("Financial Statements")
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"country_code": "ch",
|
||||
"name": "240812 Schulkontenrahmen VEB - DE",
|
||||
"tree": {
|
||||
"Aktiven": {
|
||||
"account_number": "1",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Umlaufvermögen": {
|
||||
"account_number": "10",
|
||||
"is_group": 1,
|
||||
"Flüssige Mittel": {
|
||||
"account_number": "100",
|
||||
"is_group": 1,
|
||||
"Kasse": {
|
||||
"account_number": "1000",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Bankguthaben": {
|
||||
"account_number": "1020",
|
||||
"account_type": "Bank"
|
||||
}
|
||||
},
|
||||
"Kurzfristig gehaltene Aktiven mit Börsenkurs": {
|
||||
"account_number": "106",
|
||||
"is_group": 1,
|
||||
"Wertschriften": {
|
||||
"account_number": "1060"
|
||||
},
|
||||
"Wertberichtigungen Wertschriften": {
|
||||
"account_number": "1069"
|
||||
}
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "110",
|
||||
"is_group": 1,
|
||||
"Forderungen aus Lieferungen und Leistungen (Debitoren)": {
|
||||
"account_number": "1100"
|
||||
},
|
||||
"Delkredere": {
|
||||
"account_number": "1109"
|
||||
}
|
||||
},
|
||||
"Übrige kurzfristige Forderungen": {
|
||||
"account_number": "114",
|
||||
"is_group": 1,
|
||||
"Vorschüsse und Darlehen": {
|
||||
"account_number": "1140"
|
||||
},
|
||||
"Wertberichtigungen Vorschüsse und Darlehen": {
|
||||
"account_number": "1149"
|
||||
},
|
||||
"Vorsteuer MWST Material, Waren, Dienstleistungen, Energie": {
|
||||
"account_number": "1170"
|
||||
},
|
||||
"Vorsteuer MWST Investitionen, übriger Betriebsaufwand": {
|
||||
"account_number": "1171"
|
||||
},
|
||||
"Verrechnungssteuer": {
|
||||
"account_number": "1176"
|
||||
},
|
||||
"Forderungen gegenüber Sozialversicherungen und Vorsorgeeinrichtungen": {
|
||||
"account_number": "1180"
|
||||
},
|
||||
"Quellensteuer": {
|
||||
"account_number": "1189"
|
||||
},
|
||||
"Sonstige kurzfristige Forderungen": {
|
||||
"account_number": "1190"
|
||||
},
|
||||
"Wertberichtigungen sonstige kurzfristige Forderungen": {
|
||||
"account_number": "1199"
|
||||
}
|
||||
},
|
||||
"Vorräte und nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "120",
|
||||
"is_group": 1,
|
||||
"Handelswaren": {
|
||||
"account_number": "1200"
|
||||
},
|
||||
"Rohstoffe": {
|
||||
"account_number": "1210"
|
||||
},
|
||||
"Werkstoffe": {
|
||||
"account_number": "1220"
|
||||
},
|
||||
"Hilfs- und Verbrauchsmaterial": {
|
||||
"account_number": "1230"
|
||||
},
|
||||
"Handelswaren in Konsignation": {
|
||||
"account_number": "1250"
|
||||
},
|
||||
"Fertige Erzeugnisse": {
|
||||
"account_number": "1260"
|
||||
},
|
||||
"Unfertige Erzeugnisse": {
|
||||
"account_number": "1270"
|
||||
},
|
||||
"Nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "1280"
|
||||
}
|
||||
},
|
||||
"Aktive Rechnungsabgrenzungen": {
|
||||
"account_number": "130",
|
||||
"is_group": 1,
|
||||
"Bezahlter Aufwand des Folgejahres": {
|
||||
"account_number": "1300"
|
||||
},
|
||||
"Noch nicht erhaltener Ertrag": {
|
||||
"account_number": "1301"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Anlagevermögen": {
|
||||
"account_number": "14",
|
||||
"is_group": 1,
|
||||
"Finanzanlagen": {
|
||||
"account_number": "140",
|
||||
"is_group": 1,
|
||||
"Wertschriften": {
|
||||
"account_number": "1400"
|
||||
},
|
||||
"Wertberichtigungen Wertschriften": {
|
||||
"account_number": "1409"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "1440"
|
||||
},
|
||||
"Hypotheken": {
|
||||
"account_number": "1441"
|
||||
},
|
||||
"Wertberichtigungen langfristige Forderungen": {
|
||||
"account_number": "1449"
|
||||
}
|
||||
},
|
||||
"Beteiligungen": {
|
||||
"account_number": "148",
|
||||
"is_group": 1,
|
||||
"Beteiligungen": {
|
||||
"account_number": "1480"
|
||||
},
|
||||
"Wertberichtigungen Beteiligungen": {
|
||||
"account_number": "1489"
|
||||
}
|
||||
},
|
||||
"Mobile Sachanlagen": {
|
||||
"account_number": "150",
|
||||
"is_group": 1,
|
||||
"Maschinen und Apparate": {
|
||||
"account_number": "1500"
|
||||
},
|
||||
"Wertberichtigungen Maschinen und Apparate": {
|
||||
"account_number": "1509"
|
||||
},
|
||||
"Mobiliar und Einrichtungen": {
|
||||
"account_number": "1510"
|
||||
},
|
||||
"Wertberichtigungen Mobiliar und Einrichtungen": {
|
||||
"account_number": "1519"
|
||||
},
|
||||
"Büromaschinen, Informatik, Kommunikationstechnologie": {
|
||||
"account_number": "1520"
|
||||
},
|
||||
"Wertberichtigungen Büromaschinen, Informatik, Kommunikationstechnologie": {
|
||||
"account_number": "1529"
|
||||
},
|
||||
"Fahrzeuge": {
|
||||
"account_number": "1530"
|
||||
},
|
||||
"Wertberichtigungen Fahrzeuge": {
|
||||
"account_number": "1539"
|
||||
},
|
||||
"Werkzeuge und Geräte": {
|
||||
"account_number": "1540"
|
||||
},
|
||||
"Wertberichtigungen Werkzeuge und Geräte": {
|
||||
"account_number": "1549"
|
||||
}
|
||||
},
|
||||
"Immobile Sachanlagen": {
|
||||
"account_number": "160",
|
||||
"is_group": 1,
|
||||
"Geschäftsliegenschaften": {
|
||||
"account_number": "1600"
|
||||
},
|
||||
"Wertberichtigungen Geschäftsliegenschaften": {
|
||||
"account_number": "1609"
|
||||
}
|
||||
},
|
||||
"Immaterielle Werte": {
|
||||
"account_number": "170",
|
||||
"is_group": 1,
|
||||
"Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
|
||||
"account_number": "1700"
|
||||
},
|
||||
"Wertberichtigungen Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
|
||||
"account_number": "1709"
|
||||
},
|
||||
"Goodwill": {
|
||||
"account_number": "1770"
|
||||
},
|
||||
"Wertberichtigungen Goodwill": {
|
||||
"account_number": "1779"
|
||||
}
|
||||
},
|
||||
"Nicht einbezahltes Grund-, Gesellschafter- oder Stiftungskapital": {
|
||||
"account_number": "180",
|
||||
"is_group": 1,
|
||||
"Nicht einbezahltes Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
|
||||
"account_number": "1850"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Passiven": {
|
||||
"account_number": "2",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Kurzfristiges Fremdkapital": {
|
||||
"account_number": "20",
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"account_number": "200",
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)": {
|
||||
"account_number": "2000"
|
||||
},
|
||||
"Erhaltene Anzahlungen": {
|
||||
"account_number": "2030"
|
||||
}
|
||||
},
|
||||
"Kurzfristige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "210",
|
||||
"is_group": 1,
|
||||
"Bankverbindlichkeiten": {
|
||||
"account_number": "2100"
|
||||
},
|
||||
"Verbindlichkeiten aus Finanzierungsleasing": {
|
||||
"account_number": "2120"
|
||||
},
|
||||
"Übrige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "2140"
|
||||
}
|
||||
},
|
||||
"Übrige kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "220",
|
||||
"is_group": 1,
|
||||
"Geschuldete MWST (Umsatzsteuer)": {
|
||||
"account_number": "2200"
|
||||
},
|
||||
"Abrechnungskonto MWST": {
|
||||
"account_number": "2201"
|
||||
},
|
||||
"Verrechnungssteuer": {
|
||||
"account_number": "2206"
|
||||
},
|
||||
"Direkte Steuern": {
|
||||
"account_number": "2208"
|
||||
},
|
||||
"Sonstige kurzfristige Verbindlichkeiten": {
|
||||
"account_number": "2210"
|
||||
},
|
||||
"Beschlossene Ausschüttungen": {
|
||||
"account_number": "2261"
|
||||
},
|
||||
"Sozialversicherungen und Vorsorgeeinrichtungen": {
|
||||
"account_number": "2270"
|
||||
},
|
||||
"Quellensteuer": {
|
||||
"account_number": "2279"
|
||||
}
|
||||
},
|
||||
"Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen": {
|
||||
"account_number": "230",
|
||||
"is_group": 1,
|
||||
"Noch nicht bezahlter Aufwand": {
|
||||
"account_number": "2300"
|
||||
},
|
||||
"Erhaltener Ertrag des Folgejahres": {
|
||||
"account_number": "2301"
|
||||
},
|
||||
"Kurzfristige Rückstellungen": {
|
||||
"account_number": "2330"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Langfristiges Fremdkapital": {
|
||||
"account_number": "24",
|
||||
"is_group": 1,
|
||||
"Langfristige verzinsliche Verbindlichkeiten": {
|
||||
"account_number": "240",
|
||||
"is_group": 1,
|
||||
"Bankverbindlichkeiten": {
|
||||
"account_number": "2400"
|
||||
},
|
||||
"Verbindlichkeiten aus Finanzierungsleasing": {
|
||||
"account_number": "2420"
|
||||
},
|
||||
"Obligationenanleihen": {
|
||||
"account_number": "2430"
|
||||
},
|
||||
"Darlehen": {
|
||||
"account_number": "2450"
|
||||
},
|
||||
"Hypotheken": {
|
||||
"account_number": "2451"
|
||||
}
|
||||
},
|
||||
"Übrige langfristige Verbindlichkeiten": {
|
||||
"account_number": "250",
|
||||
"is_group": 1,
|
||||
"Übrige langfristige Verbindlichkeiten (unverzinslich)": {
|
||||
"account_number": "2500"
|
||||
}
|
||||
},
|
||||
"Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen": {
|
||||
"account_number": "260",
|
||||
"is_group": 1,
|
||||
"Rückstellungen": {
|
||||
"account_number": "2600"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Eigenkapital (juristische Personen)": {
|
||||
"account_number": "28",
|
||||
"is_group": 1,
|
||||
"Grund-, Gesellschafter- oder Stiftungskapital": {
|
||||
"account_number": "280",
|
||||
"is_group": 1,
|
||||
"Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
|
||||
"account_number": "2800"
|
||||
}
|
||||
},
|
||||
"Reserven und Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "290",
|
||||
"is_group": 1,
|
||||
"Gesetzliche Kapitalreserve": {
|
||||
"account_number": "2900"
|
||||
},
|
||||
"Reserve für eigene Kapitalanteile": {
|
||||
"account_number": "2930"
|
||||
},
|
||||
"Aufwertungsreserve": {
|
||||
"account_number": "2940"
|
||||
},
|
||||
"Gesetzliche Gewinnreserve": {
|
||||
"account_number": "2950"
|
||||
},
|
||||
"Freiwillige Gewinnreserven": {
|
||||
"account_number": "2960"
|
||||
},
|
||||
"Gewinnvortrag oder Verlustvortrag": {
|
||||
"account_number": "2970"
|
||||
},
|
||||
"Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "2979"
|
||||
},
|
||||
"Eigene Aktien, Stammanteile oder Anteilscheine (Minusposten)": {
|
||||
"account_number": "2980"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Betrieblicher Ertrag aus Lieferungen und Leistungen": {
|
||||
"account_number": "3",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Produktionserlöse": {
|
||||
"account_number": "3000"
|
||||
},
|
||||
"Handelserlöse": {
|
||||
"account_number": "3200"
|
||||
},
|
||||
"Dienstleistungserlöse": {
|
||||
"account_number": "3400"
|
||||
},
|
||||
"Übrige Erlöse aus Lieferungen und Leistungen": {
|
||||
"account_number": "3600"
|
||||
},
|
||||
"Eigenleistungen": {
|
||||
"account_number": "3700"
|
||||
},
|
||||
"Eigenverbrauch": {
|
||||
"account_number": "3710"
|
||||
},
|
||||
"Erlösminderungen": {
|
||||
"account_number": "3800"
|
||||
},
|
||||
"Verluste Forderungen (Debitoren), Veränderung Delkredere": {
|
||||
"account_number": "3805"
|
||||
},
|
||||
"Bestandesänderungen unfertige Erzeugnisse": {
|
||||
"account_number": "3900"
|
||||
},
|
||||
"Bestandesänderungen fertige Erzeugnisse": {
|
||||
"account_number": "3901"
|
||||
},
|
||||
"Bestandesänderungen nicht fakturierte Dienstleistungen": {
|
||||
"account_number": "3940"
|
||||
}
|
||||
},
|
||||
"Aufwand für Material, Handelswaren, Dienstleistungen und Energie": {
|
||||
"account_number": "4",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Materialaufwand Produktion": {
|
||||
"account_number": "4000"
|
||||
},
|
||||
"Handelswarenaufwand": {
|
||||
"account_number": "4200"
|
||||
},
|
||||
"Aufwand für bezogene Dienstleistungen": {
|
||||
"account_number": "4400"
|
||||
},
|
||||
"Energieaufwand zur Leistungserstellung": {
|
||||
"account_number": "4500"
|
||||
},
|
||||
"Aufwandminderungen": {
|
||||
"account_number": "4900"
|
||||
}
|
||||
},
|
||||
"Personalaufwand": {
|
||||
"account_number": "5",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Lohnaufwand": {
|
||||
"account_number": "5000"
|
||||
},
|
||||
"Sozialversicherungsaufwand": {
|
||||
"account_number": "5700"
|
||||
},
|
||||
"Übriger Personalaufwand": {
|
||||
"account_number": "5800"
|
||||
},
|
||||
"Leistungen Dritter": {
|
||||
"account_number": "5900"
|
||||
}
|
||||
},
|
||||
"Übriger betrieblicher Aufwand, Abschreibungen und Wertberichtigungen sowie Finanzergebnis": {
|
||||
"account_number": "6",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Raumaufwand": {
|
||||
"account_number": "6000"
|
||||
},
|
||||
"Unterhalt, Reparaturen, Ersatz mobile Sachanlagen": {
|
||||
"account_number": "6100"
|
||||
},
|
||||
"Leasingaufwand mobile Sachanlagen": {
|
||||
"account_number": "6105"
|
||||
},
|
||||
"Fahrzeug- und Transportaufwand": {
|
||||
"account_number": "6200"
|
||||
},
|
||||
"Fahrzeugleasing und -mieten": {
|
||||
"account_number": "6260"
|
||||
},
|
||||
"Sachversicherungen, Abgaben, Gebühren, Bewilligungen": {
|
||||
"account_number": "6300"
|
||||
},
|
||||
"Energie- und Entsorgungsaufwand": {
|
||||
"account_number": "6400"
|
||||
},
|
||||
"Verwaltungsaufwand": {
|
||||
"account_number": "6500"
|
||||
},
|
||||
"Informatikaufwand inkl. Leasing": {
|
||||
"account_number": "6570"
|
||||
},
|
||||
"Werbeaufwand": {
|
||||
"account_number": "6600"
|
||||
},
|
||||
"Sonstiger betrieblicher Aufwand": {
|
||||
"account_number": "6700"
|
||||
},
|
||||
"Abschreibungen und Wertberichtigungen auf Positionen des Anlagevermögens": {
|
||||
"account_number": "6800"
|
||||
},
|
||||
"Finanzaufwand": {
|
||||
"account_number": "6900"
|
||||
},
|
||||
"Finanzertrag": {
|
||||
"account_number": "6950"
|
||||
}
|
||||
},
|
||||
"Betrieblicher Nebenerfolg": {
|
||||
"account_number": "7",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Ertrag Nebenbetrieb": {
|
||||
"account_number": "7000"
|
||||
},
|
||||
"Aufwand Nebenbetrieb": {
|
||||
"account_number": "7010"
|
||||
},
|
||||
"Ertrag betriebliche Liegenschaft": {
|
||||
"account_number": "7500"
|
||||
},
|
||||
"Aufwand betriebliche Liegenschaft": {
|
||||
"account_number": "7510"
|
||||
}
|
||||
},
|
||||
"Betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag": {
|
||||
"account_number": "8",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Betriebsfremder Aufwand": {
|
||||
"account_number": "8000"
|
||||
},
|
||||
"Betriebsfremder Ertrag": {
|
||||
"account_number": "8100"
|
||||
},
|
||||
"Ausserordentlicher, einmaliger oder periodenfremder Aufwand": {
|
||||
"account_number": "8500"
|
||||
},
|
||||
"Ausserordentlicher, einmaliger oder periodenfremder Ertrag": {
|
||||
"account_number": "8510"
|
||||
},
|
||||
"Direkte Steuern": {
|
||||
"account_number": "8900"
|
||||
}
|
||||
},
|
||||
"Abschluss": {
|
||||
"account_number": "9",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Jahresgewinn oder Jahresverlust": {
|
||||
"account_number": "9200"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only_depends_on": "eval:!doc.__islocal",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
||||
@@ -41,6 +41,11 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
self.validate_doctype()
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_doctype(self):
|
||||
if self.document_type in (
|
||||
*core_doctypes_list,
|
||||
"Accounting Dimension",
|
||||
@@ -49,6 +54,7 @@ class AccountingDimension(Document):
|
||||
"Accounting Dimension Detail",
|
||||
"Company",
|
||||
"Account",
|
||||
"Finance Book",
|
||||
):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
@@ -61,9 +67,6 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
@@ -102,6 +105,7 @@ class AccountingDimension(Document):
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
@@ -262,7 +266,7 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
WHERE p.name = c.parent AND p.disabled = 0""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
msg += " ";
|
||||
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||
msg += "<br>";
|
||||
msg += "Do you still want to enable immutable ledger?";
|
||||
msg += __("Do you still want to enable immutable ledger?");
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
|
||||
@@ -40,9 +40,14 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"stale_days",
|
||||
"auto_reconciliation_job_trigger",
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -72,6 +77,7 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"payment_request_settings",
|
||||
@@ -384,7 +390,7 @@
|
||||
{
|
||||
"fieldname": "section_break_jpd0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Reconciliations"
|
||||
"label": "Payment Reconciliation Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -489,6 +495,43 @@
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yuug",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_resa",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "15",
|
||||
"description": "Interval should be between 1 to 59 MInutes",
|
||||
"fieldname": "auto_reconciliation_job_trigger",
|
||||
"fieldtype": "Int",
|
||||
"label": "Auto Reconciliation Job Trigger"
|
||||
},
|
||||
{
|
||||
"default": "5",
|
||||
"description": "Documents Processed on each trigger. Queue Size should be between 5 and 100",
|
||||
"fieldname": "reconciliation_queue_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Reconciliation Queue Size"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Ignores legacy Is Opening field in GL Entry that allows adding opening balance post the system is in use while generating reports",
|
||||
"fieldname": "ignore_is_opening_check_for_reporting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Is Opening check for reporting"
|
||||
},
|
||||
{
|
||||
"default": "Payment",
|
||||
"description": "Only applies for Normal Payments",
|
||||
"fieldname": "exchange_gain_loss_posting_date",
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -496,7 +539,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-26 06:48:52.714630",
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
@@ -27,6 +28,7 @@ class AccountsSettings(Document):
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
automatically_fetch_payment_terms: DF.Check
|
||||
automatically_process_deferred_accounting_entry: DF.Check
|
||||
book_asset_depreciation_entry_automatically: DF.Check
|
||||
@@ -43,14 +45,17 @@ class AccountsSettings(Document):
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
@@ -90,6 +95,8 @@ class AccountsSettings(Document):
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
self.validate_and_sync_auto_reconcile_config()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
frappe.msgprint(
|
||||
@@ -114,3 +121,17 @@ class AccountsSettings(Document):
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
cint(self.auto_reconciliation_job_trigger) > 0
|
||||
and cint(self.auto_reconciliation_job_trigger) < 60
|
||||
):
|
||||
sync_auto_reconcile_config(self.auto_reconciliation_job_trigger)
|
||||
else:
|
||||
frappe.throw(_("Cron Interval should be between 1 and 59 Min"))
|
||||
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from frappe.utils import cint, flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -48,6 +48,7 @@ class BankClearance(Document):
|
||||
entries = []
|
||||
|
||||
# get entries from all the apps
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
|
||||
entries += (
|
||||
frappe.get_attr(method_name)(
|
||||
@@ -77,7 +78,7 @@ class BankClearance(Document):
|
||||
if not d.get("account_currency"):
|
||||
d.account_currency = default_currency
|
||||
|
||||
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
|
||||
formatted_amount = fmt_money(abs(amount), precision, d.account_currency)
|
||||
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
|
||||
d.posting_date = getdate(d.posting_date)
|
||||
|
||||
|
||||
@@ -19,10 +19,15 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.company) {
|
||||
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||
}
|
||||
|
||||
// Set default filter dates
|
||||
let today = frappe.datetime.get_today();
|
||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||
frm.doc.bank_statement_to_date = today;
|
||||
|
||||
frm.trigger("bank_account");
|
||||
},
|
||||
|
||||
@@ -98,7 +103,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
|
||||
make_reconciliation_tool(frm) {
|
||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
frm.trigger("get_cleared_balance").then(() => {
|
||||
if (
|
||||
frm.doc.bank_account &&
|
||||
@@ -114,12 +119,13 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_account_opening_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1),
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: (response) => {
|
||||
frm.set_value("account_opening_balance", response.message);
|
||||
@@ -129,12 +135,13 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
},
|
||||
|
||||
get_cleared_balance(frm) {
|
||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
till_date: frm.doc.bank_statement_to_date,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: (response) => {
|
||||
frm.cleared_balance = response.message;
|
||||
|
||||
@@ -79,10 +79,17 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_balance(bank_account, till_date):
|
||||
def get_account_balance(bank_account, till_date, company):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"account": account,
|
||||
"report_date": till_date,
|
||||
"include_pos_transactions": 1,
|
||||
"company": company,
|
||||
}
|
||||
)
|
||||
data = get_entries(filters)
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
@@ -94,11 +101,7 @@ def get_account_balance(bank_account, till_date):
|
||||
|
||||
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
|
||||
|
||||
bank_bal = (
|
||||
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
)
|
||||
|
||||
return bank_bal
|
||||
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -799,7 +802,6 @@ def get_je_matching_query(
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(je.docstatus == 1)
|
||||
.where(filter_by_date)
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
@@ -99,9 +99,9 @@ class BankStatementImport(DataImport):
|
||||
template_options=self.template_options,
|
||||
now=run_now,
|
||||
)
|
||||
return True
|
||||
return job_id
|
||||
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -113,7 +113,8 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).start_import()
|
||||
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
|
||||
return job_id is not None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -45,45 +45,41 @@ class AutoMatchbyAccountIBAN:
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
return None
|
||||
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
return self.match_account_in_party()
|
||||
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
or_filters = self.get_or_filters()
|
||||
"""
|
||||
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
|
||||
1. Get party from a matching (iban/account no) Bank Account
|
||||
2. If not found, get party from Employee with matching bank account details (iban/account no)
|
||||
"""
|
||||
if not (self.bank_party_account_number or self.bank_party_iban):
|
||||
# Nothing to match
|
||||
return None
|
||||
|
||||
for party in parties:
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
|
||||
)
|
||||
# Search for a matching Bank Account that has party set
|
||||
party_result = frappe.db.get_all(
|
||||
"Bank Account",
|
||||
or_filters=self.get_or_filters(),
|
||||
filters={"party_type": ("is", "set"), "party": ("is", "set")},
|
||||
fields=["party", "party_type"],
|
||||
limit_page_length=1,
|
||||
)
|
||||
if result := party_result[0] if party_result else None:
|
||||
return (result["party_type"], result["party"])
|
||||
|
||||
if party == "Employee" and not party_result:
|
||||
# Search in Bank Accounts first for Employee, and then Employee record
|
||||
if "bank_account_no" in or_filters:
|
||||
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
|
||||
# If no party is found, search in Employee (since it has bank account details)
|
||||
if employee_result := frappe.db.get_all(
|
||||
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
|
||||
):
|
||||
return ("Employee", employee_result[0])
|
||||
|
||||
party_result = frappe.db.get_all(
|
||||
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,
|
||||
party_result[0],
|
||||
)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def get_or_filters(self) -> dict:
|
||||
def get_or_filters(self, party: str | None = None) -> dict:
|
||||
"""Return OR filters for Bank Account and IBAN"""
|
||||
or_filters = {}
|
||||
if self.bank_party_account_number:
|
||||
or_filters["bank_account_no"] = self.bank_party_account_number
|
||||
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
|
||||
or_filters[bank_ac_field] = self.bank_party_account_number
|
||||
|
||||
if self.bank_party_iban:
|
||||
or_filters["iban"] = self.bank_party_iban
|
||||
@@ -103,8 +99,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
return self.match_party_name_desc_in_party()
|
||||
|
||||
def match_party_name_desc_in_party(self) -> tuple | None:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
@@ -113,7 +108,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
for party in parties:
|
||||
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
|
||||
field = party.lower() + "_name"
|
||||
field = f"{party.lower()}_name"
|
||||
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
|
||||
|
||||
for field in ["bank_party_name", "description"]:
|
||||
@@ -140,13 +135,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
if not party_name:
|
||||
return None, skip
|
||||
|
||||
return (
|
||||
party,
|
||||
party_name,
|
||||
), skip
|
||||
return ((party, party_name), skip) if party_name else (None, skip)
|
||||
|
||||
def process_fuzzy_result(self, result: list | None):
|
||||
"""
|
||||
@@ -164,8 +153,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
if len(result) == 1:
|
||||
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
|
||||
|
||||
second_result = result[1]
|
||||
if first_result[SCORE] > CUTOFF:
|
||||
second_result = result[1]
|
||||
# If multiple matches with the same score, return None but discontinue matching
|
||||
# Matches were found but were too close to distinguish between
|
||||
if first_result[SCORE] == second_result[SCORE]:
|
||||
@@ -177,8 +166,8 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
|
||||
def get_parties_in_order(deposit: float) -> list:
|
||||
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
|
||||
if flt(deposit) > 0:
|
||||
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
|
||||
|
||||
return parties
|
||||
return (
|
||||
["Customer", "Supplier", "Employee"] # most -> least likely to pay us
|
||||
if flt(deposit) > 0
|
||||
else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"allow_copy": 1,
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-23 19:57:17",
|
||||
"default_view": "Tree",
|
||||
"description": "Track separate Income and Expense for product verticals or divisions.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
@@ -125,7 +126,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-24 10:55:54.083042",
|
||||
"modified": "2025-01-22 10:46:42.904001",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
|
||||
@@ -129,7 +129,7 @@ class GLEntry(Document):
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if not (self.party_type and self.party):
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
|
||||
@@ -123,3 +123,20 @@ class TestGLEntry(IntegrationTestCase):
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
def test_validate_account_party_type_shareholder(self):
|
||||
jv = make_journal_entry(
|
||||
"Opening Balance Equity - _TC",
|
||||
"Cash - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Shareholder"
|
||||
break
|
||||
|
||||
jv.save().submit()
|
||||
self.assertEqual(1, jv.docstatus)
|
||||
|
||||
@@ -430,12 +430,6 @@ frappe.ui.form.on("Journal Entry Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
cost_center: function (frm, dt, dn) {
|
||||
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
|
||||
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
|
||||
account: function (frm, dt, dn) {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
|
||||
@@ -154,10 +154,9 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
|
||||
},
|
||||
)
|
||||
jv.insert()
|
||||
|
||||
if account_bal == stock_bal:
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.save)
|
||||
frappe.db.rollback()
|
||||
else:
|
||||
jv.submit()
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.stock.utils import get_default_stock_uom
|
||||
|
||||
|
||||
class OpeningInvoiceCreationTool(Document):
|
||||
@@ -172,7 +173,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
income_expense_account_field = (
|
||||
"income_account" if row.party_type == "Customer" else "expense_account"
|
||||
)
|
||||
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or "Nos"
|
||||
default_uom = get_default_stock_uom()
|
||||
rate = flt(row.outstanding_amount) / flt(row.qty)
|
||||
|
||||
item_dict = frappe._dict(
|
||||
|
||||
@@ -258,6 +258,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
},
|
||||
|
||||
validate: async function (frm) {
|
||||
await frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
|
||||
@@ -374,7 +378,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_df_property("total_allocated_amount", "options", currency_field);
|
||||
frm.set_df_property("unallocated_amount", "options", currency_field);
|
||||
frm.set_df_property("total_taxes_and_charges", "options", currency_field);
|
||||
frm.set_df_property("party_balance", "options", currency_field);
|
||||
|
||||
frm.set_currency_labels(
|
||||
["total_amount", "outstanding_amount", "allocated_amount"],
|
||||
@@ -422,15 +425,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
],
|
||||
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
@@ -478,13 +473,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
$.each(
|
||||
[
|
||||
"party",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"paid_from_account_currency",
|
||||
"paid_from_account_balance",
|
||||
"paid_to_account_currency",
|
||||
"paid_to_account_balance",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
],
|
||||
@@ -529,17 +521,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"paid_from_account_currency",
|
||||
r.message.party_account_currency
|
||||
);
|
||||
frm.set_value("paid_from_account_balance", r.message.account_balance);
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
frm.set_value("paid_to", r.message.party_account);
|
||||
frm.set_value(
|
||||
"paid_to_account_currency",
|
||||
r.message.party_account_currency
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.account_balance);
|
||||
}
|
||||
},
|
||||
() => frm.set_value("party_balance", r.message.party_balance),
|
||||
() => frm.set_value("party_name", r.message.party_name),
|
||||
() => frm.clear_table("references"),
|
||||
() => frm.events.hide_unhide_fields(frm),
|
||||
@@ -591,7 +580,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm,
|
||||
frm.doc.paid_from,
|
||||
"paid_from_account_currency",
|
||||
"paid_from_account_balance",
|
||||
function (frm) {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
@@ -607,7 +595,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm,
|
||||
frm.doc.paid_to,
|
||||
"paid_to_account_currency",
|
||||
"paid_to_account_balance",
|
||||
function (frm) {
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
@@ -623,13 +610,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
},
|
||||
|
||||
set_account_currency_and_balance: function (
|
||||
frm,
|
||||
account,
|
||||
currency_field,
|
||||
balance_field,
|
||||
callback_function
|
||||
) {
|
||||
set_account_currency_and_balance: function (frm, account, currency_field, callback_function) {
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (frm.doc.posting_date && account) {
|
||||
frappe.call({
|
||||
@@ -644,8 +625,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frappe.run_serially([
|
||||
() => frm.set_value(currency_field, r.message["account_currency"]),
|
||||
() => {
|
||||
frm.set_value(balance_field, r.message["account_balance"]);
|
||||
|
||||
if (
|
||||
frm.doc.payment_type == "Receive" &&
|
||||
currency_field == "paid_to_account_currency"
|
||||
@@ -812,27 +791,41 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
paid_amount: function (frm) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
|
||||
received_amount: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
|
||||
if (!frm.doc.paid_amount && frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
@@ -1670,37 +1663,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return current_tax_amount;
|
||||
},
|
||||
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
const { matched_payment_requests } = frappe.last_response;
|
||||
if (!matched_payment_requests) return;
|
||||
@@ -1879,8 +1841,6 @@ function prompt_for_missing_account(frm, account) {
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,22 +21,20 @@
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"advance_reconciliation_takes_effect_on",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
"contact_person",
|
||||
"contact_email",
|
||||
"payment_accounts_section",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_from_account_type",
|
||||
"paid_from_account_currency",
|
||||
"paid_from_account_balance",
|
||||
"column_break_18",
|
||||
"paid_to",
|
||||
"paid_to_account_type",
|
||||
"paid_to_account_currency",
|
||||
"paid_to_account_balance",
|
||||
"payment_amounts_section",
|
||||
"paid_amount",
|
||||
"paid_amount_after_tax",
|
||||
@@ -222,15 +220,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts"
|
||||
},
|
||||
{
|
||||
"depends_on": "party",
|
||||
"fieldname": "party_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Party Balance",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "eval:(in_list([\"Internal Transfer\", \"Pay\"], doc.payment_type) || doc.party)",
|
||||
@@ -252,15 +241,6 @@
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_from",
|
||||
"fieldname": "paid_from_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Balance (From)",
|
||||
"options": "paid_from_account_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -285,15 +265,6 @@
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_to",
|
||||
"fieldname": "paid_to_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Account Balance (To)",
|
||||
"options": "paid_to_account_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.paid_to && doc.paid_from)",
|
||||
"fieldname": "payment_amounts_section",
|
||||
@@ -783,6 +754,16 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Oldest Of Invoice Or Advance",
|
||||
"fetch_from": "company.reconciliation_takes_effect_on",
|
||||
"fieldname": "advance_reconciliation_takes_effect_on",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Advance Reconciliation Takes Effect On",
|
||||
"no_copy": 1,
|
||||
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -796,7 +777,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-07 11:19:19.320883",
|
||||
"modified": "2025-01-31 11:24:58.076393",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -842,4 +823,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||
get_party_account_based_on_invoice_discounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
@@ -71,12 +75,16 @@ class PaymentEntry(AccountsController):
|
||||
PaymentEntryReference,
|
||||
)
|
||||
|
||||
advance_reconciliation_takes_effect_on: DF.Literal[
|
||||
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"
|
||||
]
|
||||
amended_from: DF.Link | None
|
||||
apply_tax_withholding_amount: DF.Check
|
||||
auto_repeat: DF.Link | None
|
||||
bank: DF.ReadOnly | None
|
||||
bank_account: DF.Link | None
|
||||
bank_account_no: DF.ReadOnly | None
|
||||
base_in_words: DF.SmallText | None
|
||||
base_paid_amount: DF.Currency
|
||||
base_paid_amount_after_tax: DF.Currency
|
||||
base_received_amount: DF.Currency
|
||||
@@ -92,21 +100,20 @@ class PaymentEntry(AccountsController):
|
||||
custom_remarks: DF.Check
|
||||
deductions: DF.Table[PaymentEntryDeduction]
|
||||
difference_amount: DF.Currency
|
||||
in_words: DF.SmallText | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
letter_head: DF.Link | None
|
||||
mode_of_payment: DF.Link | None
|
||||
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
|
||||
paid_amount: DF.Currency
|
||||
paid_amount_after_tax: DF.Currency
|
||||
paid_from: DF.Link
|
||||
paid_from_account_balance: DF.Currency
|
||||
paid_from_account_currency: DF.Link
|
||||
paid_from_account_type: DF.Data | None
|
||||
paid_to: DF.Link
|
||||
paid_to_account_balance: DF.Currency
|
||||
paid_to_account_currency: DF.Link
|
||||
paid_to_account_type: DF.Data | None
|
||||
party: DF.DynamicLink | None
|
||||
party_balance: DF.Currency
|
||||
party_bank_account: DF.Link | None
|
||||
party_name: DF.Data | None
|
||||
party_type: DF.Link | None
|
||||
@@ -119,6 +126,7 @@ class PaymentEntry(AccountsController):
|
||||
purchase_taxes_and_charges_template: DF.Link | None
|
||||
received_amount: DF.Currency
|
||||
received_amount_after_tax: DF.Currency
|
||||
reconcile_on_advance_payment_date: DF.Check
|
||||
reference_date: DF.Date | None
|
||||
reference_no: DF.Data | None
|
||||
references: DF.Table[PaymentEntryReference]
|
||||
@@ -196,6 +204,23 @@ class PaymentEntry(AccountsController):
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
validate_docs_for_voucher_types(["Payment Entry"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||
return
|
||||
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"references": [], "taxes": [], "deductions": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_liability_account(self):
|
||||
# Auto setting liability account should only be done during 'draft' status
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
@@ -478,7 +503,6 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
for field in (
|
||||
"party",
|
||||
"party_balance",
|
||||
"total_allocated_amount",
|
||||
"base_total_allocated_amount",
|
||||
"unallocated_amount",
|
||||
@@ -506,25 +530,19 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
else:
|
||||
complete_contact_details(self)
|
||||
if not self.party_balance:
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
)
|
||||
|
||||
if not self.party_account:
|
||||
party_account = get_party_account(self.party_type, self.party, self.company)
|
||||
self.set(self.party_account_field, party_account)
|
||||
self.party_account = party_account
|
||||
|
||||
if self.paid_from and not self.paid_from_account_currency and not self.paid_from_account_balance:
|
||||
if self.paid_from and not self.paid_from_account_currency:
|
||||
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
|
||||
self.paid_from_account_currency = acc.account_currency
|
||||
self.paid_from_account_balance = acc.account_balance
|
||||
|
||||
if self.paid_to and not self.paid_to_account_currency and not self.paid_to_account_balance:
|
||||
if self.paid_to and not self.paid_to_account_currency:
|
||||
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
|
||||
self.paid_to_account_currency = acc.account_currency
|
||||
self.paid_to_account_balance = acc.account_balance
|
||||
|
||||
self.party_account_currency = (
|
||||
self.paid_from_account_currency
|
||||
@@ -635,7 +653,7 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or(_(d) for d in valid_reference_doctypes)
|
||||
comma_or([_(d) for d in valid_reference_doctypes])
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1500,16 +1518,26 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
if self.reconcile_on_advance_payment_date:
|
||||
posting_date = self.posting_date
|
||||
if invoice.reconcile_effect_on:
|
||||
posting_date = invoice.reconcile_effect_on
|
||||
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):
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
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
|
||||
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
@@ -1665,6 +1693,14 @@ class PaymentEntry(AccountsController):
|
||||
elif self.payment_type in ("Pay", "Internal Transfer"):
|
||||
return self.paid_from
|
||||
|
||||
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
conversion_rate = self.target_exchange_rate
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
conversion_rate = self.source_exchange_rate
|
||||
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
@@ -1882,7 +1918,7 @@ class PaymentEntry(AccountsController):
|
||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||
|
||||
for ref in self.references:
|
||||
reference_outstanding_amount = ref.outstanding_amount
|
||||
reference_outstanding_amount = flt(ref.outstanding_amount)
|
||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||
|
||||
if reference_outstanding_amount > 0:
|
||||
@@ -2327,10 +2363,17 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
outstanding_invoices = []
|
||||
negative_outstanding_invoices = []
|
||||
|
||||
party_account = args.get("party_account")
|
||||
|
||||
# get party account if advance account is set.
|
||||
if args.get("book_advance_payments_in_separate_party_account"):
|
||||
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
|
||||
else:
|
||||
party_account = args.get("party_account")
|
||||
accounts = get_party_account(
|
||||
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
|
||||
)
|
||||
advance_account = accounts[1] if len(accounts) >= 1 else None
|
||||
|
||||
if party_account == advance_account:
|
||||
party_account = accounts[0]
|
||||
|
||||
if args.get("get_outstanding_invoices"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
@@ -2668,9 +2711,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
)
|
||||
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
@@ -2679,7 +2720,6 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
"party_account": party_account,
|
||||
"party_name": party_name,
|
||||
"party_account_currency": account_currency,
|
||||
"party_balance": party_balance,
|
||||
"account_balance": account_balance,
|
||||
"party_bank_account": party_bank_account,
|
||||
"bank_account": bank_account,
|
||||
@@ -2910,6 +2950,7 @@ def get_payment_entry(
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||
|
||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||
pe.project = doc.get("project") or reduce(
|
||||
@@ -2918,7 +2959,7 @@ def get_payment_entry(
|
||||
|
||||
if pe.party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||
pe.set("bank_account", bank_account)
|
||||
pe.set("party_bank_account", bank_account)
|
||||
pe.set_bank_account_data()
|
||||
|
||||
# only Purchase Invoice can be blocked individually
|
||||
@@ -3396,13 +3437,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||
"""Add loss on income discount in base currency."""
|
||||
precision = doc.precision("total")
|
||||
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(base_loss_on_income, precision),
|
||||
"amount": flt(base_loss_on_income, precision) * positive_negative,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3414,6 +3456,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
tax_discount_loss = {}
|
||||
base_total_tax_loss = 0
|
||||
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
@@ -3436,7 +3479,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
"amount": flt(loss, precision) * positive_negative,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3504,19 +3547,6 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
return paid_amount[0][0] if paid_amount else 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_and_account_balance(
|
||||
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
|
||||
):
|
||||
return frappe._dict(
|
||||
{
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_payment_order(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
@@ -291,6 +291,48 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
|
||||
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
create_payment_terms_template_with_discount()
|
||||
pi.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
|
||||
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Service Tax",
|
||||
"rate": 18,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
|
||||
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
|
||||
self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
|
||||
self.assertEqual(pe_with_tax_loss.difference_amount, 0)
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe.payment_type, "Pay")
|
||||
self.assertEqual(pe.references[0].allocated_amount, 295.0)
|
||||
self.assertEqual(pe.paid_amount, 265.5)
|
||||
self.assertEqual(pe.deductions[0].amount, -29.5)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount(self):
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
create_payment_terms_template_with_discount()
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"payment_term_outstanding",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"reconcile_effect_on",
|
||||
"column_break_4",
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
@@ -144,12 +145,18 @@
|
||||
"is_virtual": 1,
|
||||
"label": "Payment Request Outstanding",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reconcile_effect_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reconcile Effect On",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 18:11:50.019343",
|
||||
"modified": "2025-01-13 15:56:18.895082",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -30,6 +30,7 @@ class PaymentEntryReference(Document):
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
reconcile_effect_on: DF.Date | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
|
||||
@@ -335,6 +335,7 @@ class PaymentReconciliation(Document):
|
||||
for payment in non_reconciled_payments:
|
||||
row = self.append("payments", {})
|
||||
row.update(payment)
|
||||
row.is_advance = payment.book_advance_payments_in_separate_party_account
|
||||
|
||||
def get_invoice_entries(self):
|
||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||
@@ -424,6 +425,9 @@ class PaymentReconciliation(Document):
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
|
||||
exc_gain_loss_posting_date = frappe.db.get_single_value(
|
||||
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
|
||||
)
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
|
||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
@@ -450,6 +454,11 @@ class PaymentReconciliation(Document):
|
||||
res.difference_account = default_exchange_gain_loss_account
|
||||
res.exchange_rate = inv.get("exchange_rate")
|
||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||
if not pay.get("is_advance"):
|
||||
if exc_gain_loss_posting_date == "Invoice":
|
||||
res.update({"gain_loss_posting_date": inv.get("invoice_date")})
|
||||
elif exc_gain_loss_posting_date == "Reconciliation Date":
|
||||
res.update({"gain_loss_posting_date": nowdate()})
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
|
||||
from frappe.utils.data import getdate as convert_to_date
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -1680,7 +1681,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 1,
|
||||
"reconciliation_takes_effect_on": "Advance Payment Date",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1729,7 +1730,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": self.advance_receivable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
@@ -1838,7 +1839,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 0,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
amount = 200.0
|
||||
@@ -2057,6 +2058,102 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
def test_advance_reconciliation_effect_on_same_date(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": self.advance_receivable_account,
|
||||
"reconciliation_takes_effect_on": "Reconciliation Date",
|
||||
},
|
||||
)
|
||||
inv_date = convert_to_date(add_days(nowdate(), -1))
|
||||
adv_date = convert_to_date(add_days(nowdate(), -2))
|
||||
|
||||
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=200)
|
||||
pe = self.create_payment_entry(posting_date=adv_date, amount=80).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = add_days(nowdate(), -1)
|
||||
pr.to_invoice_date = nowdate()
|
||||
pr.from_payment_date = add_days(nowdate(), -2)
|
||||
pr.to_payment_date = nowdate()
|
||||
pr.default_advance_account = self.advance_receivable_account
|
||||
|
||||
# reconcile multiple payments against invoice
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
# check PR tool output post reconciliation
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "posting_date", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
|
||||
expected_gl = [
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"posting_date": adv_date,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": 80.0,
|
||||
},
|
||||
{
|
||||
"account": self.advance_receivable_account,
|
||||
"posting_date": convert_to_date(nowdate()),
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 80.0,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"posting_date": convert_to_date(nowdate()),
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": si.name,
|
||||
"debit": 0.0,
|
||||
"credit": 80.0,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"posting_date": adv_date,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": 80.0,
|
||||
"credit": 0.0,
|
||||
},
|
||||
]
|
||||
|
||||
self.assertEqual(expected_gl, gl_entries)
|
||||
|
||||
# cancel PE
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
pr.get_unreconciled_entries()
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -300,6 +300,7 @@ class PaymentRequest(Document):
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
"payment_gateway": self.payment_gateway,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -780,7 +781,10 @@ def get_existing_paid_amount(doctype, name):
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
(PL.against_voucher_type == PER.reference_doctype)
|
||||
& (PL.against_voucher_no == PER.reference_name)
|
||||
& (PL.voucher_type == PER.parenttype)
|
||||
& (PL.voucher_no == PER.parent)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
|
||||
@@ -709,6 +709,45 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_more_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300)
|
||||
pi_1.submit()
|
||||
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
|
||||
pr.grand_total = 200
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
pr_1 = make_payment_request(
|
||||
dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1
|
||||
)
|
||||
pr_1.grand_total = 200
|
||||
pr_1.submit()
|
||||
pr_1.create_payment_entry()
|
||||
|
||||
pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].reference_doctype = pi.doctype
|
||||
pe.references[0].reference_name = pi.name
|
||||
pe.references[0].grand_total = pi.grand_total
|
||||
pe.references[0].outstanding_amount = pi.outstanding_amount
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": pi_1.doctype,
|
||||
"reference_name": pi_1.name,
|
||||
"grand_total": pi_1.grand_total,
|
||||
"outstanding_amount": pi_1.outstanding_amount,
|
||||
"allocated_amount": 100,
|
||||
},
|
||||
)
|
||||
|
||||
pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
pi.load_from_db()
|
||||
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
|
||||
@@ -39,10 +39,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -68,6 +70,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -86,10 +89,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
# make return entry of pos_inv2
|
||||
@@ -111,10 +116,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -165,6 +172,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
# if in between a mandatory accounting dimension is added to the POS Profile then
|
||||
@@ -226,6 +234,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv.payments[0].amount = pos_inv.grand_total
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code,
|
||||
@@ -236,11 +245,9 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv2.payments[0].amount = pos_inv2.grand_total
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
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)
|
||||
|
||||
@@ -270,9 +277,6 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
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)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"title",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
@@ -192,16 +191,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "naming_series",
|
||||
@@ -1584,7 +1573,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified": "2025-01-06 15:03:19.957277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1635,7 +1624,6 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -161,7 +165,6 @@ class POSInvoice(SalesInvoice):
|
||||
terms: DF.TextEditor | None
|
||||
territory: DF.Link | None
|
||||
timesheets: DF.Table[SalesInvoiceTimesheet]
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
@@ -211,6 +214,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -485,6 +489,20 @@ class POSInvoice(SalesInvoice):
|
||||
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
def validate_full_payment(self):
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get("amended_from"):
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -317,7 +317,7 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
|
||||
)
|
||||
|
||||
pos.insert()
|
||||
@@ -328,6 +328,11 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.set("payments", [])
|
||||
pos_return1.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return1.paid_amount = -1000
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
@@ -342,6 +347,11 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.set("payments", [])
|
||||
pos_return2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return2.paid_amount = -1000
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
@@ -377,6 +387,15 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
inv.payments = []
|
||||
self.assertRaises(frappe.ValidationError, inv.insert)
|
||||
|
||||
def test_partial_payment(self):
|
||||
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
|
||||
)
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
|
||||
def test_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -589,7 +608,13 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
)
|
||||
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
lpe = frappe.get_doc(
|
||||
"Loyalty Point Entry",
|
||||
@@ -615,7 +640,13 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# add 10 loyalty points
|
||||
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
pos_inv.paid_amount = 10000
|
||||
pos_inv.submit()
|
||||
|
||||
before_lp_details = get_loyalty_program_details_with_points(
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
@@ -649,10 +680,12 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -684,6 +717,7 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
||||
@@ -700,6 +734,7 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -752,6 +787,7 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -782,7 +818,10 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
|
||||
# POS Invoice 1, for the batch without bundle
|
||||
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
||||
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
|
||||
)
|
||||
pos_inv1.items[0].batch_no = batch_no
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
@@ -798,8 +837,14 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
|
||||
# POS Invoice 2, for the batch with bundle
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1
|
||||
)
|
||||
pos_inv2.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
pos_inv2.reload()
|
||||
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
|
||||
|
||||
@@ -834,6 +879,10 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
pos_inv1 = create_pos_invoice(
|
||||
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
|
||||
)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
|
||||
)
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
@@ -843,7 +892,8 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
{
|
||||
"item_code": item.name,
|
||||
"warehouse": pos_inv2.items[0].warehouse,
|
||||
"voucher_type": "Delivery Note",
|
||||
"voucher_type": "POS Invoice",
|
||||
"voucher_no": pos_inv2.name,
|
||||
"qty": 2,
|
||||
"avg_rate": 300,
|
||||
"batches": frappe._dict({"TestBatch 01": 2}),
|
||||
|
||||
@@ -12,7 +12,9 @@ from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
|
||||
|
||||
|
||||
@@ -293,22 +295,23 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
accounting_dimensions = required_accounting_dimensions()
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
|
||||
dimension_values = frappe.db.get_value(
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
|
||||
)
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_value = dimension_values.get(dimension)
|
||||
dimension_value = dimension_values.get(dimension.fieldname)
|
||||
|
||||
if not dimension_value:
|
||||
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
|
||||
frappe.throw(
|
||||
_("Please set Accounting Dimension {} in {}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(dimension.label),
|
||||
frappe.get_desk_link("POS Profile", invoice.pos_profile),
|
||||
)
|
||||
)
|
||||
|
||||
invoice.set(dimension, dimension_value)
|
||||
invoice.set(dimension.fieldname, dimension_value)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
|
||||
@@ -40,14 +40,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -73,14 +76,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -135,6 +141,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
)
|
||||
inv.insert()
|
||||
inv.payments[0].amount = inv.grand_total
|
||||
inv.save()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
@@ -152,6 +159,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
)
|
||||
inv2.insert()
|
||||
inv2.payments[0].amount = inv.grand_total
|
||||
inv2.save()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -291,7 +299,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
inv2.submit()
|
||||
|
||||
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800})
|
||||
inv3.insert()
|
||||
inv3.submit()
|
||||
|
||||
@@ -299,8 +307,8 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertNotEqual(consolidated_invoice.status, "Paid")
|
||||
self.assertNotEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertEqual(consolidated_invoice.status, "Paid")
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
@@ -435,6 +443,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -449,6 +458,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
@@ -25,11 +25,13 @@
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"disable_grand_total_to_default_mop",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -374,24 +376,36 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_campaign",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Campaign",
|
||||
"options": "UTM Campaign"
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_source",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Source",
|
||||
"options": "UTM Source"
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Medium",
|
||||
"options": "UTM Campaign"
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "print_receipt_on_order_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Receipt on Order Complete"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_grand_total_to_default_mop",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable auto setting Grand Total to default Payment Mode"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -419,7 +433,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2024-06-28 10:51:48.543766",
|
||||
"modified": "2025-01-29 13:12:30.796630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
@@ -448,4 +462,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ from frappe import _, msgprint, scrub, unscrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form, now
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
|
||||
|
||||
class POSProfile(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -35,6 +39,7 @@ class POSProfile(Document):
|
||||
currency: DF.Link
|
||||
customer: DF.Link | None
|
||||
customer_groups: DF.Table[POSCustomerGroup]
|
||||
disable_grand_total_to_default_mop: DF.Check
|
||||
disable_rounded_total: DF.Check
|
||||
disabled: DF.Check
|
||||
expense_account: DF.Link | None
|
||||
@@ -46,6 +51,7 @@ class POSProfile(Document):
|
||||
letter_head: DF.Link | None
|
||||
payments: DF.Table[POSPaymentMethod]
|
||||
print_format: DF.Link | None
|
||||
print_receipt_on_order_complete: DF.Check
|
||||
select_print_heading: DF.Link | None
|
||||
selling_price_list: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
@@ -70,15 +76,19 @@ class POSProfile(Document):
|
||||
self.validate_accounting_dimensions()
|
||||
|
||||
def validate_accounting_dimensions(self):
|
||||
acc_dim_names = required_accounting_dimensions()
|
||||
for acc_dim in acc_dim_names:
|
||||
if not self.get(acc_dim):
|
||||
acc_dims = get_checks_for_pl_and_bs_accounts()
|
||||
for acc_dim in acc_dims:
|
||||
if (
|
||||
self.company == acc_dim.company
|
||||
and not self.get(acc_dim.fieldname)
|
||||
and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} is a mandatory Accounting Dimension. <br>"
|
||||
"Please set a value for {0} in Accounting Dimensions section."
|
||||
).format(
|
||||
unscrub(frappe.bold(acc_dim)),
|
||||
frappe.bold(acc_dim.label),
|
||||
),
|
||||
title=_("Mandatory Accounting Dimension"),
|
||||
)
|
||||
@@ -216,23 +226,6 @@ def get_child_nodes(group_type, root):
|
||||
)
|
||||
|
||||
|
||||
def required_accounting_dimensions():
|
||||
p = frappe.qb.DocType("Accounting Dimension")
|
||||
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
|
||||
acc_dim_doc = (
|
||||
frappe.qb.from_(p)
|
||||
.inner_join(c)
|
||||
.on(p.name == c.parent)
|
||||
.select(c.parent)
|
||||
.where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1))
|
||||
.where(p.disabled == 0)
|
||||
).run(as_dict=1)
|
||||
|
||||
acc_dim_names = [scrub(d.parent) for d in acc_dim_doc]
|
||||
return acc_dim_names
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import Counter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -22,4 +25,14 @@ class POSSettings(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
self.validate_invoice_fields()
|
||||
|
||||
def validate_invoice_fields(self):
|
||||
invoice_fields = [field.fieldname for field in self.invoice_fields]
|
||||
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
|
||||
|
||||
if len(duplicate_invoice_fields):
|
||||
for field in duplicate_invoice_fields:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"column_break_42",
|
||||
"free_item_uom",
|
||||
"round_free_qty",
|
||||
"dont_enforce_free_item_qty",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
@@ -643,12 +644,19 @@
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
|
||||
"fieldname": "dont_enforce_free_item_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Enforce Free Item Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -60,6 +60,7 @@ class PricingRule(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
dont_enforce_free_item_qty: DF.Check
|
||||
for_price_list: DF.Link | None
|
||||
free_item: DF.Link | None
|
||||
free_item_rate: DF.Currency
|
||||
@@ -415,8 +416,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get("child_docname"),
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -647,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
|
||||
if pricing_rule.margin_type in ["Percentage", "Amount"]:
|
||||
item_details.margin_rate_or_amount = 0.0
|
||||
item_details.margin_type = None
|
||||
elif pricing_rule.get("free_item"):
|
||||
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
|
||||
item_details.remove_free_item = (
|
||||
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
|
||||
)
|
||||
|
||||
@@ -438,6 +438,54 @@ class TestPricingRule(IntegrationTestCase):
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
def test_dont_enforce_free_item_qty(self):
|
||||
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"min_qty": 0,
|
||||
"max_qty": 7,
|
||||
"discount_percentage": 17.5,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 0,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
pricing_rule = frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With enforcement
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 2)
|
||||
|
||||
# Without enforcement
|
||||
pricing_rule.dont_enforce_free_item_qty = 1
|
||||
pricing_rule.save()
|
||||
|
||||
# Test 2 : Deleted free item will not be fetched again on save without enforcement
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_cumulative_pricing_rule(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
|
||||
test_record = {
|
||||
@@ -1461,6 +1509,7 @@ def make_pricing_rule(**args):
|
||||
"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,
|
||||
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
|
||||
args.pop((item.item_code, item.pricing_rules))
|
||||
|
||||
for free_item in args.values():
|
||||
doc.append("items", free_item)
|
||||
if doc.is_new() or not frappe.get_value(
|
||||
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
|
||||
):
|
||||
doc.append("items", free_item)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:ACC-PPR-{#####}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-30 21:28:39.793927",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -158,7 +157,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-27 14:48:56.715320",
|
||||
"modified": "2025-01-08 08:22:14.798085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -192,4 +191,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
}
|
||||
@@ -210,9 +210,9 @@ def trigger_reconciliation_for_queued_docs():
|
||||
|
||||
docs_to_trigger = []
|
||||
unique_filters = set()
|
||||
queue_size = 5
|
||||
queue_size = frappe.db.get_single_value("Accounts Settings", "reconciliation_queue_size") or 5
|
||||
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account"]
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:PPR-LOG-{##}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-13 15:00:09.149681",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -110,7 +109,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:18.769659",
|
||||
"modified": "2025-01-08 08:22:19.104975",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
@@ -153,11 +154,16 @@
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled"
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:18.933928",
|
||||
"modified": "2025-01-23 16:09:01.058574",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log Allocations",
|
||||
|
||||
@@ -20,6 +20,7 @@ class ProcessPaymentReconciliationLogAllocations(Document):
|
||||
difference_account: DF.Link | None
|
||||
difference_amount: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
gain_loss_posting_date: DF.Date | None
|
||||
invoice_number: DF.DynamicLink
|
||||
invoice_type: DF.Link
|
||||
is_advance: DF.Data | None
|
||||
|
||||
@@ -236,17 +236,21 @@ def get_ar_filters(doc, entry):
|
||||
|
||||
def get_html(doc, filters, entry, col, res, ageing):
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
if doc.report == "General Ledger"
|
||||
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
)
|
||||
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
if doc.report == "General Ledger":
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
)
|
||||
|
||||
process_soa_html = frappe.get_hooks("process_soa_html")
|
||||
# fetching custom print format for Process Statement of Accounts
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
@@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
@@ -321,9 +324,12 @@ def get_recipients_and_cc(customer, doc):
|
||||
recipients = []
|
||||
for clist in doc.customers:
|
||||
if clist.customer == customer:
|
||||
recipients.append(clist.billing_email)
|
||||
if clist.billing_email:
|
||||
for email in clist.billing_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
if doc.primary_mandatory and clist.primary_email:
|
||||
recipients.append(clist.primary_email)
|
||||
for email in clist.primary_email.split(","):
|
||||
recipients.append(email.strip())
|
||||
cc = []
|
||||
if doc.cc_to != "":
|
||||
try:
|
||||
|
||||
@@ -336,6 +336,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
||||
|
||||
let payment_terms_template = this.frm.doc.payment_terms_template;
|
||||
|
||||
erpnext.utils.get_party_details(
|
||||
this.frm,
|
||||
"erpnext.accounts.party.get_party_details",
|
||||
@@ -356,6 +358,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||
|
||||
// while duplicating, don't change payment terms
|
||||
if (me.frm.doc.__run_link_triggers === false) {
|
||||
me.frm.set_value("payment_terms_template", payment_terms_template);
|
||||
me.frm.refresh_field("payment_terms_template");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -372,6 +380,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
tax_withholding_category(frm) {
|
||||
var me = this;
|
||||
let filtered_taxes = (me.frm.doc.taxes || []).filter((row) => !row.is_tax_withholding_account);
|
||||
me.frm.clear_table("taxes");
|
||||
|
||||
filtered_taxes.forEach((row) => {
|
||||
me.frm.add_child("taxes", row);
|
||||
});
|
||||
|
||||
me.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
credit_to() {
|
||||
var me = this;
|
||||
if (this.frm.doc.credit_to) {
|
||||
|
||||
@@ -1625,7 +1625,7 @@
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||
"description": "Debit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
|
||||
"fieldname": "update_outstanding_for_self",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Outstanding for Self"
|
||||
@@ -1641,7 +1641,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-25 18:13:01.944477",
|
||||
"modified": "2025-01-14 11:39:04.564610",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
@@ -33,7 +32,7 @@ from erpnext.accounts.general_ledger import (
|
||||
merge_similar_entries,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
@@ -840,12 +839,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def update_supplier_outstanding(self, update_outstanding):
|
||||
if update_outstanding == "No":
|
||||
update_outstanding_amt(
|
||||
self.credit_to,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
account=self.credit_to,
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
@@ -1128,6 +1127,7 @@ class PurchaseInvoice(BuyingController):
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
and item.net_rate == net_rate_map[item.pr_detail]
|
||||
and item.item_code in stock_items
|
||||
):
|
||||
discrepancy_caused_by_exchange_rate_difference = (
|
||||
item.qty * item.net_rate
|
||||
@@ -1802,13 +1802,13 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
for row in voucher_wise_amount:
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
{
|
||||
"voucher_name": voucher_no,
|
||||
"voucher_type": voucher_details.get("voucher_type"),
|
||||
"taxable_amount": voucher_details.get("amount"),
|
||||
"voucher_name": row.voucher_name,
|
||||
"voucher_type": row.voucher_type,
|
||||
"taxable_amount": row.taxable_amount,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
},
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
if (frappe.model.can_create("Purchase Receipt")) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -383,6 +383,53 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
# Creating Purchase Invoice with USD currency
|
||||
pr = frappe.new_doc("Purchase Receipt")
|
||||
pr.currency = "USD"
|
||||
pr.company = "_Test Company with perpetual inventory"
|
||||
pr.conversion_rate = (70,)
|
||||
pr.supplier = "_Test Supplier USD"
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Non Stock Item",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
pr.append(
|
||||
"items",
|
||||
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
|
||||
)
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
|
||||
# Createing purchase invoice against Purchase Receipt
|
||||
pi = create_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 80
|
||||
pi.credit_to = "_Test Payable USD - TCP1"
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Get exchnage gain and loss account
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
|
||||
)
|
||||
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||
pi.items[1].base_net_amount - pr.items[1].base_net_amount
|
||||
)
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(self.globalTestRecords["Purchase Invoice"][1])
|
||||
pi.insert()
|
||||
|
||||
@@ -16,6 +16,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
setup(doc) {
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
Dunning: this.make_dunning.bind(this),
|
||||
"Invoice Discounting": this.make_invoice_discounting.bind(this),
|
||||
};
|
||||
}
|
||||
company() {
|
||||
super.company();
|
||||
@@ -61,7 +65,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
refresh(doc, dt, dn) {
|
||||
const me = this;
|
||||
super.refresh();
|
||||
if (this.frm.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
|
||||
if (this.frm?.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
|
||||
// hide new msgbox
|
||||
this.frm.msgbox.hide();
|
||||
}
|
||||
@@ -125,12 +129,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
function () {
|
||||
this.frm.events.create_invoice_discounting(this.frm);
|
||||
},
|
||||
this.make_invoice_discounting.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -139,22 +140,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(
|
||||
__("Dunning"),
|
||||
() => {
|
||||
this.frm.events.create_dunning(this.frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1) {
|
||||
this.frm.add_custom_button(
|
||||
__("Maintenance Schedule"),
|
||||
function () {
|
||||
this.frm.cscript.make_maintenance_schedule();
|
||||
},
|
||||
this.make_maintenance_schedule.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
@@ -189,6 +182,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
make_invoice_discounting() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_dunning() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
@@ -789,7 +796,11 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
onload: function (frm) {
|
||||
=======
|
||||
onload: function(frm) {
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
frm.redemption_conversion_factor = null;
|
||||
},
|
||||
|
||||
@@ -888,26 +899,115 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
|
||||
project: function (frm) {
|
||||
if (frm.doc.project) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project,
|
||||
frappe.call({
|
||||
method: "is_auto_fetch_timesheet_enabled",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
if (cint(r.message)) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
project: function(frm) {
|
||||
if (frm.doc.project) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
async add_timesheet_data(frm, kwargs) {
|
||||
if (kwargs === "Sales Invoice") {
|
||||
// called via frm.trigger()
|
||||
kwargs = Object();
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (!Object.prototype.hasOwnProperty.call(kwargs, "project") && frm.doc.project) {
|
||||
=======
|
||||
if (!kwargs.hasOwnProperty("project") && frm.doc.project) {
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
kwargs.project = frm.doc.project;
|
||||
}
|
||||
|
||||
const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
|
||||
return frm.events.set_timesheet_data(frm, timesheets);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
add_timesheet_row: function(frm, row, exchange_rate) {
|
||||
frm.add_child('timesheets', {
|
||||
'activity_type': row.activity_type,
|
||||
'description': row.description,
|
||||
'time_sheet': row.parent,
|
||||
'billing_hours': row.billing_hours,
|
||||
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
||||
'timesheet_detail': row.name
|
||||
=======
|
||||
},
|
||||
|
||||
async get_timesheet_data(frm, kwargs) {
|
||||
return frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
|
||||
args: kwargs
|
||||
}).then(r => {
|
||||
if (!r.exc && r.message.length > 0) {
|
||||
return r.message
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
});
|
||||
},
|
||||
|
||||
set_timesheet_data: function(frm, timesheets) {
|
||||
frm.clear_table("timesheets")
|
||||
timesheets.forEach(timesheet => {
|
||||
if (frm.doc.currency != timesheet.currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
from_currency: timesheet.currency,
|
||||
to_currency: frm.doc.currency
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
exchange_rate = r.message;
|
||||
frm.events.append_time_log(frm, timesheet, exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
append_time_log: function(frm, time_log, exchange_rate) {
|
||||
const row = frm.add_child("timesheets");
|
||||
row.activity_type = time_log.activity_type;
|
||||
row.description = time_log.description;
|
||||
row.time_sheet = time_log.time_sheet;
|
||||
row.from_time = time_log.from_time;
|
||||
row.to_time = time_log.to_time;
|
||||
row.billing_hours = time_log.billing_hours;
|
||||
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
||||
row.timesheet_detail = time_log.name;
|
||||
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
async get_timesheet_data(frm, kwargs) {
|
||||
return frappe
|
||||
.call({
|
||||
@@ -993,67 +1093,109 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(__("Fetch Timesheet"), function () {
|
||||
frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
=======
|
||||
calculate_timesheet_totals: function(frm) {
|
||||
frm.set_value("total_billing_amount",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_amount"] || 0.0), 0.0));
|
||||
frm.set_value("total_billing_hours",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0));
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(__("Fetch Timesheet"), function() {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
"label" : __("From"),
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
"label" : __("To"),
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
"label" : __("Project"),
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"default": frm.doc.project
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
project: data.project
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
primary_action_label: __("Get Timesheets")
|
||||
});
|
||||
d.show();
|
||||
});
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
},
|
||||
|
||||
create_invoice_discounting: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
|
||||
create_dunning: function (frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
@@ -1077,7 +1219,34 @@ var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
|
||||
});
|
||||
};
|
||||
|
||||
<<<<<<< HEAD
|
||||
var select_loyalty_program = function (frm, loyalty_programs) {
|
||||
=======
|
||||
create_invoice_discounting: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||
frm: frm
|
||||
});
|
||||
},
|
||||
|
||||
create_dunning: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
timesheets_remove(frm, cdt, cdn) {
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var select_loyalty_program = function(frm, loyalty_programs) {
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Loyalty Program"),
|
||||
fields: [
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"title",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
@@ -50,6 +49,18 @@
|
||||
"set_target_warehouse",
|
||||
"section_break_42",
|
||||
"items",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"packing_list",
|
||||
"packed_items",
|
||||
"product_bundle_help",
|
||||
"time_sheet_list",
|
||||
"timesheets",
|
||||
"total_billing_amount",
|
||||
"total_billing_hours",
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
"section_break_30",
|
||||
"total_qty",
|
||||
"total_net_weight",
|
||||
@@ -229,18 +240,6 @@
|
||||
"hide_seconds": 1,
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "naming_series",
|
||||
@@ -304,7 +303,8 @@
|
||||
"oldfieldname": "project_name",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1989,6 +1989,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
"fieldtype": "Check",
|
||||
@@ -1997,11 +1998,14 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
=======
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
"fieldname": "total_billing_hours",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Billing Hours",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
@@ -2159,7 +2163,7 @@
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||
"description": "Credit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
|
||||
"fieldname": "update_outstanding_for_self",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Outstanding for Self",
|
||||
@@ -2211,6 +2215,8 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
=======
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2223,7 +2229,15 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
"modified": "2025-02-06 15:59:54.636202",
|
||||
=======
|
||||
"modified": "2021-08-02 18:36:51.978581",
|
||||
>>>>>>> b57521a337 (feat: add `total_billing_hours` to Sales Invoice)
|
||||
=======
|
||||
"modified": "2021-08-15 18:40:20.445127",
|
||||
>>>>>>> 0f2f11cb33 (fix: typo)
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2275,7 +2289,7 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
@@ -206,7 +210,6 @@ class SalesInvoice(SellingController):
|
||||
terms: DF.TextEditor | None
|
||||
territory: DF.Link | None
|
||||
timesheets: DF.Table[SalesInvoiceTimesheet]
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
@@ -323,9 +326,7 @@ class SalesInvoice(SellingController):
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if not self.is_return:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
if self.is_return:
|
||||
self.timesheets = []
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
@@ -364,7 +365,7 @@ class SalesInvoice(SellingController):
|
||||
if self.update_stock:
|
||||
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
|
||||
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
|
||||
asset.status == "Sold" and not self.is_return
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -449,6 +450,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -558,6 +561,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_reservation_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
@@ -1092,16 +1096,21 @@ class SalesInvoice(SellingController):
|
||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||
|
||||
def update_timesheet_billing_for_project(self):
|
||||
if not self.timesheets and self.project:
|
||||
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
|
||||
self.add_timesheet_data()
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_auto_fetch_timesheet_enabled(self):
|
||||
return frappe.db.get_single_value("Projects Settings", "fetch_timesheet_in_sales_invoice")
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_timesheet_data(self):
|
||||
self.set("timesheets", [])
|
||||
if self.project:
|
||||
for data in get_projectwise_timesheet_data(self.project):
|
||||
<<<<<<< HEAD
|
||||
self.append(
|
||||
"timesheets",
|
||||
{
|
||||
@@ -1113,6 +1122,16 @@ class SalesInvoice(SellingController):
|
||||
"description": data.description,
|
||||
},
|
||||
)
|
||||
=======
|
||||
self.append('timesheets', {
|
||||
'time_sheet': data.time_sheet,
|
||||
'billing_hours': data.billing_hours,
|
||||
'billing_amount': data.billing_amount,
|
||||
'timesheet_detail': data.name,
|
||||
'activity_type': data.activity_type,
|
||||
'description': data.description
|
||||
})
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
@@ -1192,14 +1211,14 @@ class SalesInvoice(SellingController):
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
|
||||
update_outstanding_amt(
|
||||
self.debit_to,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.doctype,
|
||||
self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
account=self.debit_to,
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
)
|
||||
|
||||
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
|
||||
@@ -1703,14 +1722,6 @@ class SalesInvoice(SellingController):
|
||||
self.set("write_off_amount", reference_doc.get("write_off_amount"))
|
||||
self.due_date = None
|
||||
|
||||
def validate_serial_numbers(self):
|
||||
"""
|
||||
validate serial number agains Delivery Note and Sales Invoice
|
||||
"""
|
||||
for item in self.items:
|
||||
item.set_serial_no_against_delivery_note()
|
||||
item.validate_serial_against_delivery_note()
|
||||
|
||||
def update_project(self):
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
|
||||
@@ -32,12 +32,16 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function (listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
listview.page.add_action_item(__("Delivery Note"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
}
|
||||
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
listview.page.add_action_item(__("Payment"), () => {
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4294,6 +4294,7 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.company = "_Test Company"
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
@@ -4304,6 +4305,30 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
def test_pos_returns_with_party_account_currency(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
currency="USD",
|
||||
conversion_rate=86.595000000,
|
||||
qty=2,
|
||||
do_not_save=True,
|
||||
)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.debit_to = "_Test Receivable USD - _TC"
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils.data import cint
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import get_disposal_account_and_cost_center
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class SalesInvoiceItem(Document):
|
||||
@@ -128,39 +128,3 @@ class SalesInvoiceItem(Document):
|
||||
self.income_account = disposal_account
|
||||
if not self.cost_center:
|
||||
self.cost_center = depreciation_cost_center
|
||||
|
||||
def set_serial_no_against_delivery_note(self):
|
||||
"""Set serial no based on delivery note."""
|
||||
if self.serial_no and self.delivery_note and self.qty != len(get_serial_nos(self.serial_no)):
|
||||
self.serial_no = get_delivery_note_serial_no(self.item_code, self.qty, self.delivery_note)
|
||||
|
||||
def validate_serial_against_delivery_note(self):
|
||||
"""Ensure the serial numbers in this Sales Invoice Item are same as in the linked Delivery Note."""
|
||||
if not self.delivery_note or not self.dn_detail:
|
||||
return
|
||||
|
||||
serial_nos = frappe.db.get_value("Delivery Note Item", self.dn_detail, "serial_no") or ""
|
||||
dn_serial_nos = set(get_serial_nos(serial_nos))
|
||||
|
||||
serial_nos = self.serial_no or ""
|
||||
si_serial_nos = set(get_serial_nos(serial_nos))
|
||||
serial_no_diff = si_serial_nos - dn_serial_nos
|
||||
|
||||
if serial_no_diff:
|
||||
dn_link = frappe.utils.get_link_to_form("Delivery Note", self.delivery_note)
|
||||
msg = (
|
||||
_("Row #{0}: The following serial numbers are not present in Delivery Note {1}:").format(
|
||||
self.idx, dn_link
|
||||
)
|
||||
+ " "
|
||||
+ ", ".join(frappe.bold(d) for d in serial_no_diff)
|
||||
)
|
||||
|
||||
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
|
||||
|
||||
if self.serial_no and cint(self.qty) != len(si_serial_nos):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: {1} serial numbers are required for Item {2}. You have provided {3} serial numbers."
|
||||
).format(self.idx, self.qty, self.item_code, len(si_serial_nos))
|
||||
)
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
"fieldname": "project_name",
|
||||
@@ -113,11 +114,17 @@
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
=======
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2024-03-27 13:10:36.562795",
|
||||
=======
|
||||
"modified": "2021-08-02 23:03:08.084930",
|
||||
>>>>>>> 1110f88e5a (feat: refactor and enhance sales invoice timesheet)
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
|
||||
@@ -114,10 +114,10 @@ class Subscription(Document):
|
||||
|
||||
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
elif date:
|
||||
_current_invoice_start = date
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
else:
|
||||
_current_invoice_start = nowdate()
|
||||
|
||||
@@ -414,8 +414,8 @@ class Subscription(Document):
|
||||
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
# Add party currency to invoice
|
||||
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
|
||||
# Add currency to invoice
|
||||
invoice.currency = frappe.db.get_value("Subscription Plan", {"name": self.plans[0].plan}, "currency")
|
||||
|
||||
# Add dimensions in invoice for subscription:
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
@@ -634,9 +634,7 @@ class Subscription(Document):
|
||||
"""
|
||||
invoice = frappe.get_all(
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
},
|
||||
{"subscription": self.name, "docstatus": ("<", 2)},
|
||||
limit=1,
|
||||
order_by="to_date desc",
|
||||
pluck="name",
|
||||
@@ -675,6 +673,7 @@ class Subscription(Document):
|
||||
self.invoice_document_type,
|
||||
{
|
||||
"subscription": self.name,
|
||||
"docstatus": 1,
|
||||
"status": ["!=", "Paid"],
|
||||
},
|
||||
)
|
||||
@@ -697,7 +696,7 @@ class Subscription(Document):
|
||||
self.status = "Cancelled"
|
||||
self.cancelation_date = nowdate()
|
||||
|
||||
if to_generate_invoice:
|
||||
if to_generate_invoice and self.cancelation_date >= self.current_invoice_start:
|
||||
self.generate_invoice(self.current_invoice_start, self.cancelation_date)
|
||||
|
||||
self.save()
|
||||
|
||||
@@ -479,6 +479,28 @@ class TestSubscription(IntegrationTestCase):
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1},
|
||||
)
|
||||
def test_multi_currency_subscription_with_default_company_currency(self):
|
||||
party = "Test Subscription Customer Multi Currency"
|
||||
frappe.db.set_value("Customer", party, "default_currency", "USD")
|
||||
subscription = create_subscription(
|
||||
start_date="2018-01-01",
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
def test_subscription_recovery(self):
|
||||
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
|
||||
subscription = create_subscription(
|
||||
@@ -590,6 +612,12 @@ def create_parties():
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "Test Subscription Customer Multi Currency"
|
||||
customer.default_currency = "USD"
|
||||
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"
|
||||
|
||||
@@ -87,6 +87,7 @@ def get_party_details(inv):
|
||||
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
if inv.doctype == "Payment Entry":
|
||||
inv.tax_withholding_net_total = inv.net_total
|
||||
inv.base_tax_withholding_net_total = inv.net_total
|
||||
|
||||
pan_no = ""
|
||||
parties = []
|
||||
@@ -156,6 +157,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
}
|
||||
)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
@@ -266,7 +270,10 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
parties,
|
||||
tax_details,
|
||||
inv.company,
|
||||
party_type=party_type,
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
@@ -302,6 +309,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
# if tds account is changed.
|
||||
if not tax_deducted:
|
||||
tax_deducted = is_tax_deducted_on_the_basis_of_inv(vouchers)
|
||||
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
@@ -319,7 +330,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# once tds is deducted, not need to add vouchers in the invoice
|
||||
voucher_wise_amount = {}
|
||||
else:
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount)
|
||||
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
@@ -336,14 +347,41 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = (
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total"
|
||||
def is_tax_deducted_on_the_basis_of_inv(vouchers):
|
||||
return frappe.db.exists(
|
||||
"Purchase Taxes and Charges",
|
||||
{
|
||||
"parent": ["in", vouchers],
|
||||
"is_tax_withholding_account": 1,
|
||||
"parenttype": "Purchase Invoice",
|
||||
"base_tax_amount_after_discount_amount": [">", 0],
|
||||
},
|
||||
)
|
||||
voucher_wise_amount = {}
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
voucher_wise_amount = []
|
||||
vouchers = []
|
||||
|
||||
ldcs = frappe.db.get_all(
|
||||
"Lower Deduction Certificate",
|
||||
filters={
|
||||
"valid_from": [">=", tax_details.from_date],
|
||||
"valid_upto": ["<=", tax_details.to_date],
|
||||
"company": company,
|
||||
"supplier": ["in", parties],
|
||||
},
|
||||
fields=["supplier", "valid_from", "valid_upto", "rate"],
|
||||
)
|
||||
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = [
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"name",
|
||||
"grand_total",
|
||||
"posting_date",
|
||||
]
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
frappe.scrub(party_type): ["in", parties],
|
||||
@@ -357,15 +395,29 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
||||
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
|
||||
d = frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
"posting_date": d.posting_date,
|
||||
}
|
||||
)
|
||||
|
||||
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
|
||||
if ldc[0].supplier in parties and ldc[0].rate == 0:
|
||||
d.update({"taxable_amount": 0})
|
||||
|
||||
vouchers.append(d.voucher_name)
|
||||
voucher_wise_amount.append(d)
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name, ja.credit - ja.debit AS amount
|
||||
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
WHERE
|
||||
j.name = ja.parent
|
||||
@@ -384,13 +436,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if journal_entries_details:
|
||||
for d in journal_entries_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
|
||||
for d in journal_entries_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": "Journal Entry",
|
||||
"taxable_amount": d.amount,
|
||||
"reference_type": d.reference_type,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
@@ -489,12 +548,24 @@ def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||
return advance_tax_from_across_fiscal_year
|
||||
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
|
||||
tds_amount = 0
|
||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||
|
||||
pi_grand_total = 0
|
||||
pi_base_net_total = 0
|
||||
jv_credit_amt = 0
|
||||
pe_credit_amt = 0
|
||||
|
||||
for row in voucher_wise_amount:
|
||||
if row.voucher_type == "Purchase Invoice":
|
||||
pi_grand_total += row.get("grand_total", 0)
|
||||
pi_base_net_total += row.get("taxable_amount", 0)
|
||||
|
||||
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
|
||||
jv_credit_amt += row.get("taxable_amount", 0)
|
||||
|
||||
## for TDS to be deducted on advances
|
||||
payment_entry_filters = {
|
||||
pe_filters = {
|
||||
"party_type": "Supplier",
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
@@ -505,70 +576,49 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
|
||||
|
||||
if cint(tax_details.consider_party_ledger_amount):
|
||||
invoice_filters.pop("apply_tds", None)
|
||||
field = "sum(grand_total)"
|
||||
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"parent": ("in", vouchers),
|
||||
"docstatus": 1,
|
||||
"party": ("in", parties),
|
||||
"reference_type": ("!=", "Purchase Invoice"),
|
||||
},
|
||||
"sum(credit_in_account_currency - debit_in_account_currency)",
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
if consider_party_ledger_amt:
|
||||
pe_filters.pop("apply_tax_withholding_amount", None)
|
||||
pe_filters.pop("tax_withholding_category", None)
|
||||
|
||||
# Get Amount via payment entry
|
||||
payment_entry_amounts = frappe.db.get_all(
|
||||
payment_entries = frappe.db.get_all(
|
||||
"Payment Entry",
|
||||
filters=payment_entry_filters,
|
||||
fields=["sum(unallocated_amount) as amount", "payment_type"],
|
||||
group_by="payment_type",
|
||||
filters=pe_filters,
|
||||
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
supp_credit_amt += type.amount
|
||||
else:
|
||||
supp_credit_amt -= type.amount
|
||||
for row in payment_entries:
|
||||
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
|
||||
pe_credit_amt += value
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": row.name,
|
||||
"voucher_type": "Payment Entry",
|
||||
"taxable_amount": value,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0)
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
|
||||
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
cumulative_threshold_breached = (
|
||||
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
|
||||
)
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
|
||||
supp_credit_amt += pi_base_net_total
|
||||
|
||||
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
|
||||
supp_credit_amt = pi_base_net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.utils import add_days, add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -71,6 +71,49 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_with_account_changed(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Multi Account TDS Category"
|
||||
)
|
||||
invoices = []
|
||||
|
||||
# create invoices for lower than single threshold tax rate
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# create another invoice whose total when added to previously created invoice,
|
||||
# surpasses cumulative threshhold
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier")
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 3000)
|
||||
self.assertEqual(pi.grand_total, 7000)
|
||||
invoices.append(pi)
|
||||
|
||||
# account changed
|
||||
|
||||
frappe.db.set_value(
|
||||
"Tax Withholding Account",
|
||||
{"parent": "Multi Account TDS Category"},
|
||||
"account",
|
||||
"_Test Account VAT - _TC",
|
||||
)
|
||||
|
||||
# TDS should be on invoice only even though account is changed
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 500)
|
||||
invoices.append(pi)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds(self):
|
||||
invoices = []
|
||||
frappe.db.set_value(
|
||||
@@ -536,6 +579,15 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
pe = create_payment_entry(
|
||||
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier6", paid_amount=1000
|
||||
)
|
||||
pe.apply_tax_withholding_amount = 1
|
||||
pe.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pe.save()
|
||||
pe.submit()
|
||||
invoices.append(pe)
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
|
||||
pi2.apply_tds = 1
|
||||
pi2.tax_withholding_category = "Test Multi Invoice Category"
|
||||
@@ -551,6 +603,8 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[2].voucher_name == pe.name)
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[2].taxable_amount == pe.paid_amount)
|
||||
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
@@ -622,6 +676,49 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def test_ldc_at_0_rate(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
valid_from = fiscal_year[1]
|
||||
valid_upto = add_months(valid_from, 1)
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=0,
|
||||
limit=50000,
|
||||
valid_from=valid_from,
|
||||
valid_upto=valid_upto,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True
|
||||
)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
pi2 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier",
|
||||
rate=35000,
|
||||
posting_date=add_days(valid_upto, 1),
|
||||
set_posting_time=True,
|
||||
)
|
||||
pi2.submit()
|
||||
self.assertEqual(len(pi2.taxes), 1)
|
||||
# pi1 net total shouldn't be included as it lies within LDC at rate of '0'
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
@@ -779,7 +876,8 @@ def create_purchase_invoice(**args):
|
||||
pi = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Invoice",
|
||||
"posting_date": today(),
|
||||
"set_posting_time": args.set_posting_time or False,
|
||||
"posting_date": args.posting_date or today(),
|
||||
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||
"supplier": args.supplier,
|
||||
"company": "_Test Company",
|
||||
@@ -1071,6 +1169,16 @@ def create_tax_withholding_category_records():
|
||||
consider_party_ledger_amount=1,
|
||||
)
|
||||
|
||||
create_tax_withholding_category(
|
||||
category_name="Multi Account TDS Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=0,
|
||||
cumulative_threshold=30000,
|
||||
)
|
||||
|
||||
|
||||
def create_tax_withholding_category(
|
||||
category_name,
|
||||
@@ -1107,7 +1215,9 @@ def create_tax_withholding_category(
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit):
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
@@ -1118,8 +1228,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"valid_from": valid_from or fiscal_year[1],
|
||||
"valid_upto": valid_upto or fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ def make_gl_entries(
|
||||
make_acc_dimensions_offsetting_entry(gl_map)
|
||||
validate_accounting_period(gl_map)
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
create_payment_ledger_entry(
|
||||
@@ -164,12 +164,12 @@ def validate_accounting_period(gl_map):
|
||||
)
|
||||
|
||||
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
|
||||
if not gl_map:
|
||||
return []
|
||||
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost)
|
||||
|
||||
if merge_entries:
|
||||
gl_map = merge_similar_entries(gl_map, precision)
|
||||
@@ -179,13 +179,17 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
return gl_map
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
# Validate budget against main cost center
|
||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||
if not from_repost:
|
||||
validate_expense_against_budget(
|
||||
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
||||
)
|
||||
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
@@ -677,11 +681,15 @@ def make_reverse_gl_entries(
|
||||
|
||||
debit_in_account_currency = new_gle.get("debit_in_account_currency", 0)
|
||||
credit_in_account_currency = new_gle.get("credit_in_account_currency", 0)
|
||||
debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0)
|
||||
credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0)
|
||||
|
||||
new_gle["debit"] = credit
|
||||
new_gle["credit"] = debit
|
||||
new_gle["debit_in_account_currency"] = credit_in_account_currency
|
||||
new_gle["credit_in_account_currency"] = debit_in_account_currency
|
||||
new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency
|
||||
new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency
|
||||
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
@@ -621,34 +621,41 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
return due_date
|
||||
|
||||
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
doctype_date = "Date"
|
||||
if doctype == "Purchase Invoice":
|
||||
doctype_date = "Supplier Invoice Date"
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
doctype_date = "Posting Date"
|
||||
|
||||
frappe.throw(_("Due Date cannot be before {0}").format(doctype_date))
|
||||
else:
|
||||
if not template_name:
|
||||
return
|
||||
validate_due_date_with_template(posting_date, due_date, bill_date, template_name)
|
||||
|
||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
||||
"%Y-%m-%d"
|
||||
|
||||
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name):
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
)
|
||||
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
is_credit_controller = (
|
||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
if is_credit_controller:
|
||||
msgprint(
|
||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
||||
date_diff(due_date, default_due_date)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -681,7 +688,7 @@ def set_taxes(
|
||||
):
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
|
||||
|
||||
args = {party_type.lower(): party, "company": company}
|
||||
args = {frappe.scrub(party_type): party, "company": company}
|
||||
|
||||
if tax_category:
|
||||
args["tax_category"] = tax_category
|
||||
@@ -701,10 +708,10 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
if party_type in ("Customer", "Lead", "Prospect", "CRM Deal"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
if party_type in ["Lead", "Prospect", "CRM Deal"]:
|
||||
args["customer"] = None
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
@@ -770,7 +777,7 @@ def validate_account_party_type(self):
|
||||
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="right" colspan="3" ><strong>Total (debit) </strong></td>
|
||||
<td class="left" >{{ gl | sum(attribute='debit') }}</td>
|
||||
<td class="left" >{{ gl | sum(attribute='debit') | round(2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="top-bottom" colspan="5"><strong>Credit</strong></td>
|
||||
@@ -61,7 +61,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="right" colspan="3"><strong>Total (credit) </strong></td>
|
||||
<td class="left" >{{ gl | sum(attribute='credit') }}</td>
|
||||
<td class="left" >{{ gl | sum(attribute='credit') | round(2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td>
|
||||
|
||||
@@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ function get_filters() {
|
||||
fieldname: "budget_against_filter",
|
||||
label: __("Dimension Filter"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "budget_against",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ frappe.query_reports["Customer Ledger Summary"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -106,6 +107,7 @@ frappe.query_reports["Customer Ledger Summary"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -510,12 +510,16 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
|
||||
if ignore_opening_entries:
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
|
||||
@@ -71,9 +71,6 @@
|
||||
</div>
|
||||
<div style="text-align:center; font-size:13px;">
|
||||
<b>
|
||||
{% if(filters.party_type) { %}
|
||||
[ {%= filters.party_type %} ]<br>
|
||||
{% } %}
|
||||
{%= frappe.datetime.str_to_user(filters.from_date) %}
|
||||
{%= __("to") %}
|
||||
{%= frappe.datetime.str_to_user(filters.to_date) %}<br><br>
|
||||
|
||||
@@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
@@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -208,6 +208,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
if filters.account:
|
||||
@@ -270,9 +274,15 @@ def get_conditions(filters):
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
):
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date <=%(to_date)s")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disable_prepared_report": 0,
|
||||
@@ -9,7 +9,7 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2022-02-11 10:18:36.956558",
|
||||
"modified": "2025-01-27 18:40:24.493829",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
|
||||
@@ -178,7 +178,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
for src in gross_profit_data.si_list:
|
||||
if src.indent == 1:
|
||||
total_base_amount += src.base_amount or 0.0
|
||||
total_buying_amount += src.buying_amount or 0.0
|
||||
|
||||
row = frappe._dict()
|
||||
row.indent = src.indent
|
||||
row.parent_invoice = src.parent_invoice
|
||||
@@ -189,17 +196,57 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"sales_invoice": "Total",
|
||||
"qty": None,
|
||||
"avg._selling_rate": None,
|
||||
"valuation_rate": None,
|
||||
"selling_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_%": flt(
|
||||
(total_gross_profit / total_base_amount) * 100.0,
|
||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||
)
|
||||
if total_base_amount
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for src in gross_profit_data.grouped_data:
|
||||
row = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
row.append(filters.currency)
|
||||
group_columns = group_wise_columns.get(scrub(filters.group_by))
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
total_base_amount += src.base_amount or 0.00
|
||||
total_buying_amount += src.buying_amount or 0.00
|
||||
|
||||
row = [src.get(col) for col in group_columns] + [filters.currency]
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
"base_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
|
||||
}
|
||||
|
||||
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
|
||||
data.append(total_row)
|
||||
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -612,3 +612,33 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
||||
self.assertEqual(len(item_from_sinv2), 1)
|
||||
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
||||
|
||||
def test_gross_profit_groupby_invoices(self):
|
||||
create_sales_invoice(
|
||||
qty=1,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
@@ -319,7 +319,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Rate"),
|
||||
"label": _("Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"options": "currency",
|
||||
|
||||
@@ -50,6 +50,7 @@ function get_filters() {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ frappe.query_reports["Supplier Ledger Summary"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -88,6 +89,7 @@ frappe.query_reports["Supplier Ledger Summary"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance",
|
||||
"report_type": "Script Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ def get_data(filters):
|
||||
)
|
||||
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return None
|
||||
|
||||
@@ -96,7 +100,7 @@ def get_data(filters):
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(filters)
|
||||
opening_balances = get_opening_balances(filters, ignore_is_opening)
|
||||
|
||||
# add filter inside list so that the query in financial_statements.py doesn't break
|
||||
if filters.project:
|
||||
@@ -114,7 +118,13 @@ def get_data(filters):
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values"))
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
@@ -125,15 +135,15 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet")
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss")
|
||||
def get_opening_balances(filters, ignore_is_opening):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
|
||||
|
||||
balance_sheet_opening.update(pl_opening)
|
||||
return balance_sheet_opening
|
||||
|
||||
|
||||
def get_rootwise_opening_balances(filters, report_type):
|
||||
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = ""
|
||||
@@ -159,16 +169,24 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
gle += get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
|
||||
"GL Entry",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
start_date=start_date,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
else:
|
||||
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
|
||||
gle = get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
|
||||
)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
@@ -187,7 +205,13 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
|
||||
|
||||
def get_opening_balance(
|
||||
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
|
||||
doctype,
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=None,
|
||||
start_date=None,
|
||||
ignore_is_opening=0,
|
||||
):
|
||||
closing_balance = frappe.qb.DocType(doctype)
|
||||
account = frappe.qb.DocType("Account")
|
||||
@@ -223,11 +247,16 @@ def get_opening_balance(
|
||||
(closing_balance.posting_date >= start_date)
|
||||
& (closing_balance.posting_date < filters.from_date)
|
||||
)
|
||||
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
|
||||
|
||||
if not ignore_is_opening:
|
||||
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
|
||||
)
|
||||
if not ignore_is_opening:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
|
||||
@@ -298,7 +327,7 @@ def get_opening_balance(
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -316,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
d["opening_credit"] = opening_balances.get(d.name, {}).get("opening_credit", 0)
|
||||
|
||||
for entry in gl_entries_by_account.get(d.name, []):
|
||||
if cstr(entry.is_opening) != "Yes":
|
||||
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
|
||||
|
||||
@@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = {
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
doctype: "Account",
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
|
||||
|
||||
@@ -35,9 +37,14 @@ def get_data(filters, show_party_name):
|
||||
filters=party_filters,
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
account_filter = []
|
||||
if filters.get("account"):
|
||||
account_filter = get_accounts_with_children(filters.get("account"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
opening_balances = get_opening_balances(filters)
|
||||
balances_within_period = get_balances_within_period(filters)
|
||||
opening_balances = get_opening_balances(filters, account_filter)
|
||||
balances_within_period = get_balances_within_period(filters, account_filter)
|
||||
|
||||
data = []
|
||||
# total_debit, total_credit = 0, 0
|
||||
@@ -89,30 +96,34 @@ def get_data(filters, show_party_name):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_opening_balances(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("opening_debit"),
|
||||
Sum(GL_Entry.credit).as_("opening_credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (
|
||||
(GL_Entry.posting_date < filters.from_date)
|
||||
| ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date))
|
||||
)
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit)
|
||||
@@ -121,31 +132,33 @@ def get_opening_balances(filters):
|
||||
return opening
|
||||
|
||||
|
||||
def get_balances_within_period(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_balances_within_period(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled = 0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
|
||||
and ifnull(is_opening, 'No') = 'No'
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("debit"),
|
||||
Sum(GL_Entry.credit).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (GL_Entry.posting_date >= filters.from_date)
|
||||
& (GL_Entry.posting_date <= filters.to_date)
|
||||
& (GL_Entry.is_opening == "No")
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
balances_within_period = frappe._dict()
|
||||
for d in gle:
|
||||
balances_within_period.setdefault(d.party, [d.debit, d.credit])
|
||||
|
||||
@@ -730,6 +730,23 @@ def update_reference_in_payment_entry(
|
||||
}
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
@@ -778,6 +795,8 @@ def update_reference_in_payment_entry(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
)
|
||||
|
||||
# Ledgers will be reposted by Reconciliation tool
|
||||
payment_entry.flags.ignore_reposting_on_reconciliation = True
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
return row, update_advance_paid
|
||||
@@ -1661,7 +1680,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company)
|
||||
|
||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
||||
@@ -2267,3 +2286,38 @@ def run_ledger_health_checks():
|
||||
doc.general_and_payment_ledger_mismatch = True
|
||||
doc.checked_on = run_date
|
||||
doc.save()
|
||||
|
||||
|
||||
def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
|
||||
auto_reconciliation_job_trigger = auto_reconciliation_job_trigger or frappe.db.get_single_value(
|
||||
"Accounts Settings", "auto_reconciliation_job_trigger"
|
||||
)
|
||||
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
|
||||
|
||||
sch_event = frappe.get_doc(
|
||||
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
|
||||
)
|
||||
if frappe.db.get_value("Scheduled Job Type", {"method": method}):
|
||||
frappe.get_doc(
|
||||
"Scheduled Job Type",
|
||||
{
|
||||
"method": method,
|
||||
},
|
||||
).update(
|
||||
{
|
||||
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
|
||||
"scheduler_event": sch_event.name,
|
||||
}
|
||||
).save()
|
||||
else:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Scheduled Job Type",
|
||||
"method": method,
|
||||
"scheduler_event": sch_event.name,
|
||||
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
|
||||
"create_log": True,
|
||||
"stopped": False,
|
||||
"frequency": "Cron",
|
||||
}
|
||||
).save()
|
||||
|
||||
@@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_receipt) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt");
|
||||
} else {
|
||||
frm.set_value("purchase_receipt", "");
|
||||
frappe.msgprint({
|
||||
@@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_invoice) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice");
|
||||
} else {
|
||||
frm.set_value("purchase_invoice", "");
|
||||
frappe.msgprint({
|
||||
@@ -639,45 +635,38 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
},
|
||||
|
||||
set_values_from_purchase_doc: function (frm, doctype, purchase_doc) {
|
||||
frm.set_value("company", purchase_doc.company);
|
||||
if (purchase_doc.bill_date) {
|
||||
frm.set_value("purchase_date", purchase_doc.bill_date);
|
||||
} else {
|
||||
frm.set_value("purchase_date", purchase_doc.posting_date);
|
||||
}
|
||||
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
|
||||
frm.set_value("available_for_use_date", frm.doc.purchase_date);
|
||||
}
|
||||
const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
let doctype_field = frappe.scrub(doctype);
|
||||
frm.set_value(doctype_field, "");
|
||||
frappe.msgprint({
|
||||
title: __("Invalid {0}", [__(doctype)]),
|
||||
message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => {
|
||||
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||
var purchase_amount = flt(
|
||||
item.valuation_rate * asset_quantity,
|
||||
precision("gross_purchase_amount")
|
||||
);
|
||||
set_values_from_purchase_doc: (frm, doctype) => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc",
|
||||
args: {
|
||||
purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice,
|
||||
item_code: frm.doc.item_code,
|
||||
doctype: doctype,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
let data = r.message;
|
||||
frm.set_value("company", data.company);
|
||||
frm.set_value("purchase_date", data.purchase_date);
|
||||
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("asset_quantity", data.asset_quantity);
|
||||
frm.set_value("cost_center", data.cost_center);
|
||||
if (data.asset_location) {
|
||||
frm.set_value("location", data.asset_location);
|
||||
}
|
||||
|
||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||
frm.set_value("purchase_amount", purchase_amount);
|
||||
frm.set_value("asset_quantity", asset_quantity);
|
||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
|
||||
} else {
|
||||
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
|
||||
}
|
||||
|
||||
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
|
||||
frm.set_df_property("asset_quantity", "read_only", is_editable);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -228,8 +228,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset"
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
@@ -353,7 +352,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -436,8 +435,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "asset_quantity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Asset Quantity",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
"label": "Asset Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
@@ -507,17 +505,15 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
"label": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
"label": "Purchase Invoice Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "insurance_details_tab",
|
||||
@@ -592,7 +588,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-29 14:25:56.436124",
|
||||
"modified": "2025-02-11 16:01:56.140904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -95,9 +95,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_invoice_item: DF.Data | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
purchase_receipt_item: DF.Data | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -111,7 +111,7 @@ class Asset(AccountsController):
|
||||
"Issue",
|
||||
"Receipt",
|
||||
"Capitalized",
|
||||
"Decapitalized",
|
||||
"Work In Progress",
|
||||
]
|
||||
supplier: DF.Link | None
|
||||
total_asset_cost: DF.Currency
|
||||
@@ -121,6 +121,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.validate_precision()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
self.validate_item()
|
||||
@@ -200,6 +201,38 @@ class Asset(AccountsController):
|
||||
def after_delete(self):
|
||||
add_asset_activity(self.name, _("Asset deleted"))
|
||||
|
||||
def set_purchase_doc_row_item(self):
|
||||
if self.is_existing_asset or self.is_composite_asset:
|
||||
return
|
||||
|
||||
self.purchase_amount = self.gross_purchase_amount
|
||||
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
purchase_doc = self.purchase_receipt or self.purchase_invoice
|
||||
|
||||
if not purchase_doc:
|
||||
return
|
||||
|
||||
linked_item = self.get_linked_item(purchase_doc_type, purchase_doc)
|
||||
|
||||
if linked_item:
|
||||
if purchase_doc_type == "Purchase Receipt":
|
||||
self.purchase_receipt_item = linked_item
|
||||
else:
|
||||
self.purchase_invoice_item = linked_item
|
||||
|
||||
def get_linked_item(self, purchase_doc_type, purchase_doc):
|
||||
purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc)
|
||||
|
||||
for item in purchase_doc.items:
|
||||
if self.asset_quantity > 1:
|
||||
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
elif item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
if self.purchase_invoice or self.purchase_receipt:
|
||||
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
|
||||
@@ -1126,6 +1159,30 @@ def has_active_capitalization(asset):
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
||||
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
||||
|
||||
if not matching_items:
|
||||
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
|
||||
|
||||
first_item = matching_items[0]
|
||||
is_multiple_items = len(matching_items) > 1
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
"is_multiple_items": is_multiple_items,
|
||||
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
frappe.listview_settings["Asset"] = {
|
||||
add_fields: ["status"],
|
||||
add_fields: ["status", "docstatus"],
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Fully Depreciated") {
|
||||
return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"];
|
||||
@@ -7,8 +8,10 @@ frappe.listview_settings["Asset"] = {
|
||||
return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"];
|
||||
} else if (doc.status === "Sold") {
|
||||
return [__("Sold"), "green", "status,=,Sold"];
|
||||
} else if (["Capitalized", "Decapitalized"].includes(doc.status)) {
|
||||
return [__(doc.status), "grey", "status,=," + doc.status];
|
||||
} else if (doc.status === "Work In Progress") {
|
||||
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
|
||||
} else if (doc.status === "Capitalized") {
|
||||
return [__("Capitalized"), "grey", "status,=,Capitalized"];
|
||||
} else if (doc.status === "Scrapped") {
|
||||
return [__("Scrapped"), "grey", "status,=,Scrapped"];
|
||||
} else if (doc.status === "In Maintenance") {
|
||||
@@ -21,7 +24,7 @@ frappe.listview_settings["Asset"] = {
|
||||
return [__("Receipt"), "green", "status,=,Receipt"];
|
||||
} else if (doc.status === "Submitted") {
|
||||
return [__("Submitted"), "blue", "status,=,Submitted"];
|
||||
} else if (doc.status === "Draft") {
|
||||
} else if (doc.status === "Draft" || doc.docstatus === 0) {
|
||||
return [__("Draft"), "red", "status,=,Draft"];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -436,7 +436,7 @@ def scrap_asset(asset_name, scrap_date=None):
|
||||
|
||||
if asset.docstatus != 1:
|
||||
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
|
||||
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
|
||||
|
||||
today_date = getdate(today())
|
||||
@@ -448,9 +448,9 @@ def scrap_asset(asset_name, scrap_date=None):
|
||||
notes = _("This schedule was created when Asset {0} was scrapped.").format(
|
||||
get_link_to_form(asset.doctype, asset.name)
|
||||
)
|
||||
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
if asset.status != "Fully Depreciated":
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
|
||||
|
||||
|
||||
@@ -1752,6 +1752,10 @@ def create_asset(**args):
|
||||
},
|
||||
)
|
||||
|
||||
if asset.is_composite_asset:
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.purchase_amount = 0
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
asset.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -36,11 +36,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
me.setup_warehouse_query();
|
||||
|
||||
me.frm.set_query("target_item_code", function () {
|
||||
if (me.frm.doc.entry_type == "Capitalization") {
|
||||
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
|
||||
} else {
|
||||
return erpnext.queries.item({ is_stock_item: 1, is_fixed_asset: 0 });
|
||||
}
|
||||
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
|
||||
});
|
||||
|
||||
me.frm.set_query("target_asset", function () {
|
||||
@@ -51,7 +47,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
me.frm.set_query("asset", "asset_items", function () {
|
||||
var filters = {
|
||||
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
|
||||
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized"]],
|
||||
docstatus: 1,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,30 +8,26 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"capitalization_method",
|
||||
"target_item_code",
|
||||
"target_asset_location",
|
||||
"target_item_name",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"column_break_5",
|
||||
"finance_book",
|
||||
"target_asset_location",
|
||||
"column_break_9",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"finance_book",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"amended_from",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"section_break_16",
|
||||
"stock_items",
|
||||
"stock_items_total",
|
||||
@@ -58,12 +54,12 @@
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
@@ -84,22 +80,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset",
|
||||
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
|
||||
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -162,7 +154,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Stock Items"
|
||||
@@ -173,14 +165,6 @@
|
||||
"label": "Stock Items",
|
||||
"options": "Asset Capitalization Stock Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "target_has_batch_no",
|
||||
"fieldname": "target_batch_no",
|
||||
@@ -190,20 +174,9 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
"fieldname": "target_stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
"label": "Target Qty"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -241,16 +214,6 @@
|
||||
"label": "Assets",
|
||||
"options": "Asset Capitalization Asset Item"
|
||||
},
|
||||
{
|
||||
"default": "Capitalization",
|
||||
"fieldname": "entry_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Entry Type",
|
||||
"options": "Capitalization\nDecapitalization",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items_total",
|
||||
"fieldtype": "Currency",
|
||||
@@ -272,7 +235,7 @@
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Expenses"
|
||||
@@ -337,26 +300,24 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Asset Location",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Location"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "capitalization_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Capitalization Method",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:33.080441",
|
||||
"modified": "2025-01-08 13:14:33.008458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user