mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-17 03:59:22 +00:00
Compare commits
637 Commits
update-cod
...
v14.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3967773fbe | ||
|
|
074c75252a | ||
|
|
dd4dbd4b00 | ||
|
|
ce5dbf891a | ||
|
|
fd36ee0fda | ||
|
|
d68fa0eff2 | ||
|
|
3f72156ea9 | ||
|
|
a260426dd4 | ||
|
|
148dc37d92 | ||
|
|
74a6479f70 | ||
|
|
f2f1c160f8 | ||
|
|
9b66020fc7 | ||
|
|
6af43ba40d | ||
|
|
bd531eb898 | ||
|
|
15f4812108 | ||
|
|
4c5b688e0d | ||
|
|
d1b2786f24 | ||
|
|
2eb01aae27 | ||
|
|
1152ac3ff1 | ||
|
|
89a1c83431 | ||
|
|
2a1a61238a | ||
|
|
34bd7837e2 | ||
|
|
f66a6693f4 | ||
|
|
4035873295 | ||
|
|
4a86efedfc | ||
|
|
b7cbc804b2 | ||
|
|
fc6389280c | ||
|
|
6dbb4a3b0d | ||
|
|
6114241ff2 | ||
|
|
a03ec0afb3 | ||
|
|
db5d74b7dc | ||
|
|
ad6e0f3af7 | ||
|
|
abcf30aaa3 | ||
|
|
cc9c876007 | ||
|
|
199e77faef | ||
|
|
98428f0bce | ||
|
|
a4b99a34cb | ||
|
|
c4587d8caa | ||
|
|
faf25c0b95 | ||
|
|
d742e6d56b | ||
|
|
3f0b03c0a4 | ||
|
|
195500cb32 | ||
|
|
6eafff8694 | ||
|
|
36f2b65401 | ||
|
|
f082c9e797 | ||
|
|
0ab69c0e32 | ||
|
|
de3996e411 | ||
|
|
797512ca13 | ||
|
|
d39ec5ef5f | ||
|
|
2dae93c0a1 | ||
|
|
953f78d6a9 | ||
|
|
29197dcd7f | ||
|
|
123d720616 | ||
|
|
1c95c67f58 | ||
|
|
753b2edebf | ||
|
|
f50b485225 | ||
|
|
fb1c30718b | ||
|
|
2705de084f | ||
|
|
a0ace27f56 | ||
|
|
2348c42c79 | ||
|
|
1b3a0be0f1 | ||
|
|
b9bf5666bc | ||
|
|
25e87641b6 | ||
|
|
8550bbde27 | ||
|
|
7398cbdf12 | ||
|
|
6ce32fd5a9 | ||
|
|
18904456f3 | ||
|
|
d7a65b1d41 | ||
|
|
eb6db59adf | ||
|
|
fb2b2edf7e | ||
|
|
c2f8f64d1e | ||
|
|
9a5e238702 | ||
|
|
77f4c666ee | ||
|
|
5cb9f7b3a4 | ||
|
|
e1fc67c730 | ||
|
|
99d410ab75 | ||
|
|
54de569251 | ||
|
|
cab3f39f95 | ||
|
|
fff9e76718 | ||
|
|
dbbebd2a73 | ||
|
|
9bf101a234 | ||
|
|
887690449d | ||
|
|
3300856fb4 | ||
|
|
e539579fb4 | ||
|
|
f8934faa73 | ||
|
|
2c9845f8cb | ||
|
|
92b9d3dc6d | ||
|
|
da538a37ad | ||
|
|
d6bf85f8bb | ||
|
|
c3024af296 | ||
|
|
ccc58f48e3 | ||
|
|
ed2a093e49 | ||
|
|
8a88105aed | ||
|
|
9703771b75 | ||
|
|
bd62a1d2d3 | ||
|
|
ffd82f3302 | ||
|
|
8c658638b4 | ||
|
|
064e364b4f | ||
|
|
831f60f439 | ||
|
|
469813d3e1 | ||
|
|
656008dd66 | ||
|
|
362bf5fc19 | ||
|
|
8af90b2421 | ||
|
|
6903fb68d3 | ||
|
|
907a41a8da | ||
|
|
e62fdf4af0 | ||
|
|
720370537f | ||
|
|
5a85c2d661 | ||
|
|
64e9a25347 | ||
|
|
2191f8a488 | ||
|
|
1fbbaa8fcf | ||
|
|
fbb641936e | ||
|
|
891a4afb5f | ||
|
|
01c5fbdbfb | ||
|
|
cc938fb028 | ||
|
|
9bcec15ca4 | ||
|
|
a88ac4016a | ||
|
|
0654f303bb | ||
|
|
98e7b2dd25 | ||
|
|
1ee0d990e5 | ||
|
|
faedd85b66 | ||
|
|
2db1da0c8a | ||
|
|
d9519f24aa | ||
|
|
a260c594c1 | ||
|
|
dac5989d72 | ||
|
|
9e94dd9203 | ||
|
|
794167aa36 | ||
|
|
eaea8469fc | ||
|
|
af4dafdc64 | ||
|
|
a012c55c81 | ||
|
|
6a89cb57b0 | ||
|
|
3c457e2638 | ||
|
|
62cabdf792 | ||
|
|
ed85683fea | ||
|
|
829a0ff827 | ||
|
|
0674d42c96 | ||
|
|
cb0c4b5664 | ||
|
|
36b9926527 | ||
|
|
d38071eb20 | ||
|
|
c4b39c7c1c | ||
|
|
c83c645ab0 | ||
|
|
eab24b8366 | ||
|
|
31782d6f71 | ||
|
|
2494ccc0a2 | ||
|
|
108720b9c3 | ||
|
|
4f3e209009 | ||
|
|
23b18a5e38 | ||
|
|
b386b8c634 | ||
|
|
49221d47c2 | ||
|
|
09e9675653 | ||
|
|
8eda2dbdf8 | ||
|
|
b653f43b70 | ||
|
|
0b26131b65 | ||
|
|
423c0190e2 | ||
|
|
948b231e92 | ||
|
|
23aca6663f | ||
|
|
6c85bdd735 | ||
|
|
154904e960 | ||
|
|
03db8b2f5f | ||
|
|
236a5f37c7 | ||
|
|
db4fbc9e58 | ||
|
|
89314d4171 | ||
|
|
b092791ac8 | ||
|
|
22ad3ad6f3 | ||
|
|
79151be8ce | ||
|
|
5a8329add1 | ||
|
|
b0a9f79e18 | ||
|
|
31cd96bc89 | ||
|
|
db563483cb | ||
|
|
6fa1b892f7 | ||
|
|
4b65dd7f2a | ||
|
|
62296c85cd | ||
|
|
a71be4e99a | ||
|
|
c9002526b9 | ||
|
|
b9b08e4d5e | ||
|
|
3c0bca3d74 | ||
|
|
27509e2141 | ||
|
|
52dd967b0d | ||
|
|
9601e47ae4 | ||
|
|
0a24859c9c | ||
|
|
998367cedc | ||
|
|
8ba503bf0e | ||
|
|
943e6192cc | ||
|
|
91b0e7ddbd | ||
|
|
e25db8b1b5 | ||
|
|
50d790c919 | ||
|
|
8a4c07a7e8 | ||
|
|
d0f3818060 | ||
|
|
bda25c4d5d | ||
|
|
f70103dac6 | ||
|
|
a519d68826 | ||
|
|
49cedca8de | ||
|
|
6060072d22 | ||
|
|
a226fcf898 | ||
|
|
bd8e61b2dc | ||
|
|
d5f693806b | ||
|
|
5f6bbd4929 | ||
|
|
2f1b8af13c | ||
|
|
3d6a37ea04 | ||
|
|
3caf905019 | ||
|
|
4941b6e280 | ||
|
|
a02ddc92ef | ||
|
|
6017f6790e | ||
|
|
3daff104c3 | ||
|
|
17dd1abb34 | ||
|
|
73742ff565 | ||
|
|
70e990c4f3 | ||
|
|
217e407371 | ||
|
|
90607becb8 | ||
|
|
d6888bc8c8 | ||
|
|
063f206b9b | ||
|
|
7a8038500d | ||
|
|
8851826a57 | ||
|
|
2a82f16680 | ||
|
|
32107c4a79 | ||
|
|
bbc7559380 | ||
|
|
9c56a5ff7c | ||
|
|
c8c0f345d7 | ||
|
|
d3c151f32b | ||
|
|
f09222a9e5 | ||
|
|
ba80ba07f1 | ||
|
|
34845b7630 | ||
|
|
ecfa64f22e | ||
|
|
3e7582f55b | ||
|
|
6f571218bf | ||
|
|
8ab2f2f925 | ||
|
|
2d34761057 | ||
|
|
c0a141bfcd | ||
|
|
fd232d1db7 | ||
|
|
df9147f2e5 | ||
|
|
8e68219c5b | ||
|
|
060ce45572 | ||
|
|
432da7d1d1 | ||
|
|
9318d9a7fc | ||
|
|
faca06d6c3 | ||
|
|
1918b5c4b0 | ||
|
|
444f3b5397 | ||
|
|
3736f4ecb8 | ||
|
|
5fe2079dcc | ||
|
|
11f92797b9 | ||
|
|
17996efbc7 | ||
|
|
c2bed24c31 | ||
|
|
e5d1f59d96 | ||
|
|
793fa18225 | ||
|
|
f7ec00ef49 | ||
|
|
4d51d73797 | ||
|
|
7a096231fb | ||
|
|
db60299cf3 | ||
|
|
9ec0d104a5 | ||
|
|
7243089027 | ||
|
|
5aa9c4fd07 | ||
|
|
fd86876a0e | ||
|
|
3f0a3b702d | ||
|
|
f167abbbfd | ||
|
|
5015f38a8d | ||
|
|
381231257b | ||
|
|
f4648ed610 | ||
|
|
51fcb3c666 | ||
|
|
7b11da2373 | ||
|
|
5c0446d9fc | ||
|
|
097398914e | ||
|
|
d91ac5e549 | ||
|
|
9c915ceeb2 | ||
|
|
4b8dd1fab6 | ||
|
|
fc581464b8 | ||
|
|
b3b7650ac8 | ||
|
|
86052450a5 | ||
|
|
bad3b330f4 | ||
|
|
671033755f | ||
|
|
f5f13c4611 | ||
|
|
6b55a01834 | ||
|
|
29c4a32c33 | ||
|
|
c9c203a5eb | ||
|
|
261aac8e26 | ||
|
|
b2f280dee6 | ||
|
|
3abe4a16af | ||
|
|
32ad36ad0f | ||
|
|
e98c0ab924 | ||
|
|
4a29c7fafb | ||
|
|
4b6e1f4892 | ||
|
|
a28ae4d653 | ||
|
|
11be30d68e | ||
|
|
305eb02bfc | ||
|
|
1184fa3313 | ||
|
|
2fd0291afb | ||
|
|
959fdcd841 | ||
|
|
a83e36b801 | ||
|
|
37d0721f36 | ||
|
|
408f2bda82 | ||
|
|
ddf4d02c7d | ||
|
|
2f5276b794 | ||
|
|
e708a33684 | ||
|
|
931aa62072 | ||
|
|
8c6ddb6721 | ||
|
|
f1d774034c | ||
|
|
4a1de87822 | ||
|
|
f5a3f8b486 | ||
|
|
e464de3c5d | ||
|
|
2eeff8a00b | ||
|
|
a403e729d6 | ||
|
|
1c32bf23d7 | ||
|
|
e599b61ac5 | ||
|
|
24234d584d | ||
|
|
a69881e8e5 | ||
|
|
b3897406b0 | ||
|
|
0136d58d59 | ||
|
|
3a74ac9194 | ||
|
|
21a4556f99 | ||
|
|
bcfa9b8593 | ||
|
|
f2dd547451 | ||
|
|
4e31448e3d | ||
|
|
d5d31060c4 | ||
|
|
f5c517d08d | ||
|
|
8d5131ef8f | ||
|
|
0e7c0144cb | ||
|
|
d54bf6d91c | ||
|
|
c50d6c5e74 | ||
|
|
08daa237f2 | ||
|
|
85cda4a995 | ||
|
|
ed9a896f72 | ||
|
|
181dccd8d8 | ||
|
|
7554ac9569 | ||
|
|
fcd46aa243 | ||
|
|
d62b983057 | ||
|
|
b0131a8a80 | ||
|
|
c0003195b8 | ||
|
|
957fc440fd | ||
|
|
4939153f8c | ||
|
|
302f2d10e2 | ||
|
|
eba46dae6c | ||
|
|
27fdd41a6e | ||
|
|
f759c29d55 | ||
|
|
e8076629fa | ||
|
|
77678ecfeb | ||
|
|
273ed40cfb | ||
|
|
a56b5ed8e5 | ||
|
|
d7d75be0d7 | ||
|
|
5ba5b7bf51 | ||
|
|
dcfc11df7a | ||
|
|
f14b577766 | ||
|
|
887663129e | ||
|
|
327256a516 | ||
|
|
ff210c73eb | ||
|
|
a85dd86832 | ||
|
|
c98413c981 | ||
|
|
198d19b920 | ||
|
|
5a2e286de6 | ||
|
|
24ccebc633 | ||
|
|
908944b68b | ||
|
|
cbaffb4858 | ||
|
|
7dcb8bb674 | ||
|
|
a15f0d427c | ||
|
|
436b7e3b70 | ||
|
|
075d456417 | ||
|
|
ff78906361 | ||
|
|
5f3caf6975 | ||
|
|
5c09b28d62 | ||
|
|
b700a0be1f | ||
|
|
cceadcec33 | ||
|
|
22372e69eb | ||
|
|
88c446b0bf | ||
|
|
5cab0aa1d7 | ||
|
|
569b4c256a | ||
|
|
17ea6cc5a5 | ||
|
|
442f54a988 | ||
|
|
c07e61953a | ||
|
|
2102b83bd9 | ||
|
|
f370c7b50b | ||
|
|
f752822bb3 | ||
|
|
af5fb78fe2 | ||
|
|
85cf30cfa8 | ||
|
|
14e5f239ac | ||
|
|
64aad88684 | ||
|
|
fed23536dc | ||
|
|
f126e88e5e | ||
|
|
3f8c701083 | ||
|
|
153ef5f164 | ||
|
|
cbdc46b89e | ||
|
|
b9a249918a | ||
|
|
fb99d3f1b9 | ||
|
|
3b79e24c7e | ||
|
|
97b462538a | ||
|
|
2fe72af359 | ||
|
|
38488c13e6 | ||
|
|
740c17c231 | ||
|
|
5f4b214217 | ||
|
|
5b04bebc0a | ||
|
|
38c31077c9 | ||
|
|
cd613c6033 | ||
|
|
2e1fb5255e | ||
|
|
9fd8fffb28 | ||
|
|
53f235b630 | ||
|
|
2c4867cc98 | ||
|
|
dda057d257 | ||
|
|
ded6e7f544 | ||
|
|
7bb5f6eb23 | ||
|
|
28b1d4e2a3 | ||
|
|
027e66f637 | ||
|
|
944b8a0ed7 | ||
|
|
95da66a201 | ||
|
|
9fbf507877 | ||
|
|
3c3ab897d5 | ||
|
|
60b914b2a6 | ||
|
|
581c5cbc38 | ||
|
|
4e09203ddc | ||
|
|
af883be065 | ||
|
|
08f5f81fa8 | ||
|
|
702c16eac4 | ||
|
|
82440e3124 | ||
|
|
93e510023a | ||
|
|
0bc61b3b16 | ||
|
|
174c7503c9 | ||
|
|
3083b4c218 | ||
|
|
c06e241fbc | ||
|
|
a9e9dad66a | ||
|
|
86e5f362e3 | ||
|
|
7cc98181ec | ||
|
|
1ea778a77b | ||
|
|
a2285d5e5e | ||
|
|
ccd0449006 | ||
|
|
793ee83f39 | ||
|
|
9203217678 | ||
|
|
3c1c50f171 | ||
|
|
a408722983 | ||
|
|
16502b30a0 | ||
|
|
61a1b2b42b | ||
|
|
467ee97938 | ||
|
|
d3cc9d4fa6 | ||
|
|
844f120a56 | ||
|
|
8be12f0858 | ||
|
|
5af006278e | ||
|
|
7ce6729100 | ||
|
|
c4c43bc550 | ||
|
|
62741bf744 | ||
|
|
4568648d8c | ||
|
|
7840fed7a5 | ||
|
|
f2ab220ce3 | ||
|
|
9b6dd7e71f | ||
|
|
64831952d5 | ||
|
|
d53fd8c592 | ||
|
|
2d2eef6b29 | ||
|
|
c313c37006 | ||
|
|
a00bc2967e | ||
|
|
4bac0daf9b | ||
|
|
404668fcc9 | ||
|
|
09d8fa4b5d | ||
|
|
354aaae587 | ||
|
|
7235c3f88f | ||
|
|
15733c14e8 | ||
|
|
22b2386aa1 | ||
|
|
6ab0637b0b | ||
|
|
76ae4d87ca | ||
|
|
3c688dfa6d | ||
|
|
8b8d054ded | ||
|
|
121ec83562 | ||
|
|
26536da74b | ||
|
|
f605564094 | ||
|
|
d14a9a1e89 | ||
|
|
b7e8fbe43f | ||
|
|
00a73c7a57 | ||
|
|
edb100274b | ||
|
|
6976316ada | ||
|
|
357f74a580 | ||
|
|
aedd0397b8 | ||
|
|
4cb685a326 | ||
|
|
afcc0cbccd | ||
|
|
29bca45b1e | ||
|
|
784fb47197 | ||
|
|
5520c6b2f3 | ||
|
|
f873547447 | ||
|
|
d65d2f617d | ||
|
|
0438433a4f | ||
|
|
055556b7f1 | ||
|
|
60fa421409 | ||
|
|
dd2fd12d5f | ||
|
|
319ee41403 | ||
|
|
b96526eefd | ||
|
|
78a992c086 | ||
|
|
f871dd4ef6 | ||
|
|
79ecf7751f | ||
|
|
4698dba402 | ||
|
|
f55881aef8 | ||
|
|
f1b2ba5a84 | ||
|
|
4409f11282 | ||
|
|
84865a8421 | ||
|
|
d9632e8138 | ||
|
|
4bbd0ec985 | ||
|
|
92f8f0ec74 | ||
|
|
aea484be1f | ||
|
|
52fc10d00c | ||
|
|
1b9082e07b | ||
|
|
0e9a1fb40e | ||
|
|
32abf67c80 | ||
|
|
52b42e9492 | ||
|
|
77ac7f06d4 | ||
|
|
8717235a34 | ||
|
|
670426f428 | ||
|
|
1b5a1cbaad | ||
|
|
7ca1beb15d | ||
|
|
c660db145b | ||
|
|
0aaf9c4f05 | ||
|
|
b3b0272ec9 | ||
|
|
f7898b4954 | ||
|
|
b067eae38c | ||
|
|
cee867f941 | ||
|
|
6c4fcd80c6 | ||
|
|
11657effa5 | ||
|
|
d3b7942f32 | ||
|
|
f407c972d1 | ||
|
|
0b72295fa2 | ||
|
|
204b6c0272 | ||
|
|
1118e25b6d | ||
|
|
9b9df70632 | ||
|
|
39ff0cc6d8 | ||
|
|
c407d1e51a | ||
|
|
4d8ced6c87 | ||
|
|
4f832678cf | ||
|
|
f0f7afa669 | ||
|
|
cb6cbf7818 | ||
|
|
3aec1175df | ||
|
|
c645995ae3 | ||
|
|
f772c17f3f | ||
|
|
193502ce03 | ||
|
|
abd637a238 | ||
|
|
4cf9fb08e1 | ||
|
|
e4ca20654f | ||
|
|
bc873939eb | ||
|
|
c6d3e9f432 | ||
|
|
54f4504df6 | ||
|
|
309da96442 | ||
|
|
94ebfa765c | ||
|
|
2577747c5c | ||
|
|
fb387426d6 | ||
|
|
15915d7053 | ||
|
|
362976fa42 | ||
|
|
a7d23abc2f | ||
|
|
4b609322ba | ||
|
|
5778f227ee | ||
|
|
7ac75aab1a | ||
|
|
e831b6e054 | ||
|
|
856a64b77c | ||
|
|
3afb625ff8 | ||
|
|
b6d2de2cc1 | ||
|
|
0bfb774bdf | ||
|
|
d7a8db04a1 | ||
|
|
2d5ae811d2 | ||
|
|
be9607e27b | ||
|
|
fd45a7afbe | ||
|
|
0c73af6ee2 | ||
|
|
981add9b6f | ||
|
|
8f5736c500 | ||
|
|
052f7c3345 | ||
|
|
fa4a40812c | ||
|
|
a76732613e | ||
|
|
dd602989a8 | ||
|
|
78b39d6ca4 | ||
|
|
77fa64e100 | ||
|
|
ac04fc60ef | ||
|
|
6be77d5729 | ||
|
|
37e5b93e2d | ||
|
|
19d29d1861 | ||
|
|
0db912998a | ||
|
|
200a971743 | ||
|
|
c0f0986539 | ||
|
|
376293326b | ||
|
|
9a29e3c9f2 | ||
|
|
e099e10c8e | ||
|
|
3b222339b8 | ||
|
|
9e60dd32e8 | ||
|
|
7ff5414571 | ||
|
|
d48487ada2 | ||
|
|
da69cc5477 | ||
|
|
b2a720d847 | ||
|
|
6656d23e45 | ||
|
|
ae5c05081d | ||
|
|
028e939cca | ||
|
|
216cb9b07b | ||
|
|
50ad612453 | ||
|
|
6b71af9008 | ||
|
|
3c8412efdb | ||
|
|
623f56a95c | ||
|
|
b637d4d5f1 | ||
|
|
601bc64618 | ||
|
|
04d3571dd9 | ||
|
|
aa5aaa113e | ||
|
|
354a9d6169 | ||
|
|
c7e2217c92 | ||
|
|
ce5fc5b457 | ||
|
|
abe18945a6 | ||
|
|
010a0ca0a9 | ||
|
|
74664a34c0 | ||
|
|
82f1dd268d | ||
|
|
1aa96defda | ||
|
|
8fdbbf374d | ||
|
|
66e5202642 | ||
|
|
35e9bfca38 | ||
|
|
735a60807a | ||
|
|
c3fd802351 | ||
|
|
6046f8bc5e | ||
|
|
d7db8ed12e | ||
|
|
adcd21724b | ||
|
|
04bdff736b | ||
|
|
65bb1d8cc2 | ||
|
|
ebf766cf62 | ||
|
|
80bf47170f | ||
|
|
0faa7b0432 | ||
|
|
1d1f12f949 | ||
|
|
4c82533239 | ||
|
|
5a28ba8537 | ||
|
|
8737c10ce4 | ||
|
|
2defb89962 | ||
|
|
53b9d61c46 | ||
|
|
3092131913 | ||
|
|
6dce122825 | ||
|
|
248cc48842 | ||
|
|
ebd8f2f45b | ||
|
|
bb00d38dd7 | ||
|
|
06aead0470 | ||
|
|
cdb1800087 | ||
|
|
13f8edd43f | ||
|
|
85d1a237ce | ||
|
|
dc3c27fd1b | ||
|
|
2d9da22721 | ||
|
|
86a6293e62 | ||
|
|
c311b8ea4f | ||
|
|
e832944dfe | ||
|
|
003cfe2717 | ||
|
|
132b517584 | ||
|
|
d173e06e69 | ||
|
|
9ae0380a96 | ||
|
|
8873ef7b67 | ||
|
|
dc24a657fd | ||
|
|
8c54be7e99 | ||
|
|
7a5d75b68d | ||
|
|
3b9bc8e4ef | ||
|
|
702b5c32c1 | ||
|
|
6c748966e7 |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-14
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
@@ -13,10 +13,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
@@ -28,4 +30,4 @@ jobs:
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
run: npx semantic-release
|
||||
|
||||
29
.github/workflows/server-tests-mariadb.yml
vendored
29
.github/workflows/server-tests-mariadb.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
container: [1, 2, 3, 4]
|
||||
container: [1, 2, 3]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
@@ -120,32 +120,7 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 3 --build-number ${{ matrix.container }}'
|
||||
env:
|
||||
TYPE: server
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
|
||||
coverage:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": ["version-14"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
@@ -10,7 +10,7 @@
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -21,4 +21,4 @@
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
20
CODEOWNERS
20
CODEOWNERS
@@ -11,22 +11,18 @@ erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/portal/ @marination
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/shopping_cart/ @marination
|
||||
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination
|
||||
erpnext/public/ @nextchamp-saqib @marination
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
|
||||
|
||||
.github/ @ankush
|
||||
pyproject.toml @gavindsouza @ankush
|
||||
pyproject.toml @ankush
|
||||
|
||||
@@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt))
|
||||
|
||||
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
|
||||
|
||||
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
|
||||
|
||||
@@ -2,7 +2,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.0.0-dev"
|
||||
__version__ = "14.5.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -37,7 +37,7 @@ class Account(NestedSet):
|
||||
def autoname(self):
|
||||
from erpnext.accounts.utils import get_autoname_with_number
|
||||
|
||||
self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company)
|
||||
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
|
||||
|
||||
def validate(self):
|
||||
from erpnext.accounts.utils import validate_field_number
|
||||
|
||||
@@ -4,6 +4,23 @@
|
||||
frappe.ui.form.on("Bank Clearance", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("account", "account_currency", "account_currency");
|
||||
|
||||
frm.set_query("account", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"account_type": ["in",["Bank","Cash"]],
|
||||
"is_group": 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
'is_company_account': 1
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
@@ -12,14 +29,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "";
|
||||
frm.set_value("account", default_bank_account);
|
||||
|
||||
frm.set_query("account", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"account_type": ["in",["Bank","Cash"]],
|
||||
"is_group": 0
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
frm.set_value("from_date", frappe.datetime.month_start());
|
||||
frm.set_value("to_date", frappe.datetime.month_end());
|
||||
|
||||
@@ -99,7 +99,7 @@ class BankClearance(Document):
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, frappe.qb.desc)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
@@ -126,7 +126,9 @@ class BankClearance(Document):
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc)
|
||||
query = query.orderby(loan_repayment.posting_date).orderby(
|
||||
loan_repayment.name, order=frappe.qb.desc
|
||||
)
|
||||
|
||||
loan_repayments = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
},
|
||||
|
||||
show_import_status(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || "[]");
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
let successful_records = import_log.filter((log) => log.success);
|
||||
let failed_records = import_log.filter((log) => !log.success);
|
||||
if (successful_records.length === 0) return;
|
||||
@@ -309,7 +309,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||
|
||||
show_import_preview(frm, preview_data) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || "[]");
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
|
||||
if (
|
||||
frm.import_preview &&
|
||||
@@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || "[]");
|
||||
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
|
||||
let logs = import_log;
|
||||
frm.toggle_display("import_log", false);
|
||||
frm.toggle_display("import_log_section", logs.length > 0);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"import_log",
|
||||
"statement_import_log",
|
||||
"show_failed_logs",
|
||||
"import_log_preview",
|
||||
"reference_doctype",
|
||||
@@ -90,12 +90,6 @@
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log",
|
||||
"fieldtype": "Code",
|
||||
"label": "Import Log",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -198,11 +192,17 @@
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "statement_import_log",
|
||||
"fieldtype": "Code",
|
||||
"label": "Statement Import Log",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-12 14:17:37.777246",
|
||||
"modified": "2022-09-07 11:11:40.293317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2016-05-16 11:42:29.632528",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -9,6 +10,7 @@
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
@@ -190,15 +192,26 @@
|
||||
"label": "Budget Accounts",
|
||||
"options": "Budget Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-06 15:13:54.055854",
|
||||
"modified": "2022-10-10 22:14:36.361509",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -220,5 +233,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -23,11 +22,6 @@ class DuplicateBudgetError(frappe.ValidationError):
|
||||
|
||||
|
||||
class Budget(Document):
|
||||
def autoname(self):
|
||||
self.name = make_autoname(
|
||||
self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###"
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if not self.get(frappe.scrub(self.budget_against)):
|
||||
frappe.throw(_("{0} is mandatory").format(self.budget_against))
|
||||
@@ -109,8 +103,11 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
def validate_expense_against_budget(args):
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
@@ -178,13 +175,13 @@ def validate_expense_against_budget(args):
|
||||
) # nosec
|
||||
|
||||
if budget_records:
|
||||
validate_budget_records(args, budget_records)
|
||||
validate_budget_records(args, budget_records, expense_amount)
|
||||
|
||||
|
||||
def validate_budget_records(args, budget_records):
|
||||
def validate_budget_records(args, budget_records, expense_amount):
|
||||
for budget in budget_records:
|
||||
if flt(budget.budget_amount):
|
||||
amount = get_amount(args, budget)
|
||||
amount = expense_amount or get_amount(args, budget)
|
||||
yearly_action, monthly_action = get_actions(args, budget)
|
||||
|
||||
if monthly_action in ["Stop", "Warn"]:
|
||||
|
||||
@@ -334,6 +334,39 @@ class TestBudget(unittest.TestCase):
|
||||
budget.cancel()
|
||||
jv.cancel()
|
||||
|
||||
def test_monthly_budget_against_main_cost_center(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
|
||||
create_cost_center_allocation,
|
||||
)
|
||||
|
||||
cost_centers = [
|
||||
"Main Budget Cost Center 1",
|
||||
"Sub Budget Cost Center 1",
|
||||
"Sub Budget Cost Center 2",
|
||||
]
|
||||
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Budget Cost Center 1 - _TC",
|
||||
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
400000,
|
||||
"Main Budget Cost Center 1 - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
|
||||
@@ -16,7 +16,7 @@ class CostCenter(NestedSet):
|
||||
from erpnext.accounts.utils import get_autoname_with_number
|
||||
|
||||
self.name = get_autoname_with_number(
|
||||
self.cost_center_number, self.cost_center_name, None, self.company
|
||||
self.cost_center_number, self.cost_center_name, self.company
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -366,7 +366,7 @@ def update_outstanding_amt(
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
# Didn't use db_set for optimization purpose
|
||||
ref_doc.outstanding_amount = bal
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
|
||||
|
||||
|
||||
@@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", {
|
||||
var update_jv_details = function(doc, r) {
|
||||
$.each(r, function(i, d) {
|
||||
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
|
||||
row.account = d.account;
|
||||
row.balance = d.balance;
|
||||
frappe.model.set_value(row.doctype, row.name, "account", d.account)
|
||||
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
|
||||
});
|
||||
refresh_field("accounts");
|
||||
}
|
||||
@@ -312,8 +312,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
}
|
||||
}
|
||||
|
||||
get_outstanding(doctype, docname, company, child, due_date) {
|
||||
var me = this;
|
||||
get_outstanding(doctype, docname, company, child) {
|
||||
var args = {
|
||||
"doctype": doctype,
|
||||
"docname": docname,
|
||||
|
||||
@@ -184,7 +184,9 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
@@ -1208,6 +1210,7 @@ def get_outstanding(args):
|
||||
args = json.loads(args)
|
||||
|
||||
company_currency = erpnext.get_company_currency(args.get("company"))
|
||||
due_date = None
|
||||
|
||||
if args.get("doctype") == "Journal Entry":
|
||||
condition = " and party=%(party)s" if args.get("party") else ""
|
||||
@@ -1232,10 +1235,12 @@ def get_outstanding(args):
|
||||
invoice = frappe.db.get_value(
|
||||
args["doctype"],
|
||||
args["docname"],
|
||||
["outstanding_amount", "conversion_rate", scrub(party_type)],
|
||||
["outstanding_amount", "conversion_rate", scrub(party_type), "due_date"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
due_date = invoice.get("due_date")
|
||||
|
||||
exchange_rate = (
|
||||
invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
|
||||
)
|
||||
@@ -1258,6 +1263,7 @@ def get_outstanding(args):
|
||||
"exchange_rate": exchange_rate,
|
||||
"party_type": party_type,
|
||||
"party": invoice.get(scrub(party_type)),
|
||||
"reference_due_date": due_date,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
|
||||
},
|
||||
{
|
||||
@@ -209,13 +210,15 @@
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
"fieldname": "reference_due_date",
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Due Date"
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -274,19 +277,22 @@
|
||||
"fieldname": "reference_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No"
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 21:27:32.200299",
|
||||
"modified": "2022-10-26 20:03:10.906259",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
frappe.model.set_default_values(frm.doc);
|
||||
|
||||
frm.set_query("account" ,"accounts", function(){
|
||||
|
||||
@@ -22,13 +22,13 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
}
|
||||
if (data.user != frappe.session.user) return;
|
||||
if (data.count == data.total) {
|
||||
setTimeout((title) => {
|
||||
setTimeout(() => {
|
||||
frm.doc.import_in_progress = false;
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.page.clear_indicator();
|
||||
frm.dashboard.hide_progress(title);
|
||||
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
|
||||
frm.dashboard.hide_progress();
|
||||
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
|
||||
}, 1500, data.title);
|
||||
return;
|
||||
}
|
||||
@@ -51,13 +51,6 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
method: "make_invoices",
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
callback: function(r) {
|
||||
if (r.message.length == 1) {
|
||||
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
|
||||
} else if (r.message.length < 50) {
|
||||
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -257,8 +257,6 @@ def start_import(invoices):
|
||||
|
||||
|
||||
def publish(index, total, doctype):
|
||||
if total < 50:
|
||||
return
|
||||
frappe.publish_realtime(
|
||||
"opening_invoice_creation_progress",
|
||||
dict(
|
||||
|
||||
@@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
|
||||
|
||||
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
|
||||
frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
|
||||
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
|
||||
frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.doc.total_taxes_and_charges = 0.0;
|
||||
frm.doc.base_total_taxes_and_charges = 0.0;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
@@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
}
|
||||
|
||||
tax.tax_amount = current_tax_amount;
|
||||
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
|
||||
// tax accounts are only in company currency
|
||||
tax.base_tax_amount = current_tax_amount;
|
||||
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
|
||||
if(i==0) {
|
||||
@@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
|
||||
}
|
||||
|
||||
tax.base_total = tax.total * frm.doc.source_exchange_rate;
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
|
||||
// tac accounts are only in company currency
|
||||
tax.base_total = tax.total
|
||||
|
||||
// calculate total taxes and base total taxes
|
||||
if(frm.doc.payment_type == "Pay") {
|
||||
// tax accounts only have company currency
|
||||
if(tax.currency != frm.doc.paid_to_account_currency) {
|
||||
//total_taxes_and_charges has the target currency. so using target conversion rate
|
||||
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
|
||||
|
||||
} else {
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
}
|
||||
} else if(frm.doc.payment_type == "Receive") {
|
||||
if(tax.currency != frm.doc.paid_from_account_currency) {
|
||||
//total_taxes_and_charges has the target currency. so using source conversion rate
|
||||
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
|
||||
} else {
|
||||
frm.doc.total_taxes_and_charges += current_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
|
||||
|
||||
frm.refresh_field('taxes');
|
||||
frm.refresh_field('total_taxes_and_charges');
|
||||
|
||||
@@ -181,7 +181,11 @@ class PaymentEntry(AccountsController):
|
||||
frappe.throw(_("Party is mandatory"))
|
||||
|
||||
_party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name"
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
|
||||
|
||||
if frappe.db.has_column(self.party_type, _party_name):
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
|
||||
else:
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
||||
|
||||
if self.party:
|
||||
if not self.party_balance:
|
||||
@@ -295,6 +299,9 @@ class PaymentEntry(AccountsController):
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
|
||||
if not valid_reference_doctypes:
|
||||
return
|
||||
|
||||
for d in self.get("references"):
|
||||
if not d.allocated_amount:
|
||||
continue
|
||||
@@ -362,7 +369,7 @@ class PaymentEntry(AccountsController):
|
||||
if not d.allocated_amount:
|
||||
continue
|
||||
|
||||
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"):
|
||||
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
outstanding_amount, is_return = frappe.get_cached_value(
|
||||
d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]
|
||||
)
|
||||
@@ -933,6 +940,13 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if not d.included_in_paid_amount:
|
||||
if get_account_currency(payment_account) != self.company_currency:
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
elif self.payment_type in ["Pay", "Internal Transfer"]:
|
||||
exchange_rate = self.source_exchange_rate
|
||||
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -981,7 +995,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
@@ -1026,7 +1042,7 @@ class PaymentEntry(AccountsController):
|
||||
for fieldname in tax_fields:
|
||||
tax.set(fieldname, 0.0)
|
||||
|
||||
self.paid_amount_after_tax = self.paid_amount
|
||||
self.paid_amount_after_tax = self.base_paid_amount
|
||||
|
||||
def determine_exclusive_rate(self):
|
||||
if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")):
|
||||
@@ -1045,7 +1061,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item
|
||||
|
||||
self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
|
||||
self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.total_taxes_and_charges = 0.0
|
||||
@@ -1068,7 +1084,7 @@ class PaymentEntry(AccountsController):
|
||||
current_tax_amount += actual_tax_dict[tax.idx]
|
||||
|
||||
tax.tax_amount = current_tax_amount
|
||||
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
|
||||
tax.base_tax_amount = current_tax_amount
|
||||
|
||||
if tax.add_deduct_tax == "Deduct":
|
||||
current_tax_amount *= -1.0
|
||||
@@ -1082,14 +1098,20 @@ class PaymentEntry(AccountsController):
|
||||
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
|
||||
)
|
||||
|
||||
tax.base_total = tax.total * self.source_exchange_rate
|
||||
tax.base_total = tax.total
|
||||
|
||||
if self.payment_type == "Pay":
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
if tax.currency != self.paid_to_account_currency:
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
elif self.payment_type == "Receive":
|
||||
if tax.currency != self.paid_from_account_currency:
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
@@ -1184,6 +1206,7 @@ def get_outstanding_reference_documents(args):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
common_filter = []
|
||||
posting_and_due_date = []
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
if args.get("party_type") == "Supplier":
|
||||
@@ -1200,7 +1223,7 @@ def get_outstanding_reference_documents(args):
|
||||
party_account_currency = get_account_currency(args.get("party_account"))
|
||||
company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency")
|
||||
|
||||
# Get positive outstanding sales /purchase invoices/ Fees
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = " and voucher_type={0} and voucher_no={1}".format(
|
||||
@@ -1224,7 +1247,7 @@ def get_outstanding_reference_documents(args):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
@@ -1235,6 +1258,7 @@ def get_outstanding_reference_documents(args):
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
)
|
||||
@@ -1595,10 +1619,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
elif reference_doctype != "Journal Entry":
|
||||
if not total_amount:
|
||||
if party_account_currency == company_currency:
|
||||
total_amount = ref_doc.base_grand_total
|
||||
# for handling cases that don't have multi-currency (base field)
|
||||
total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total")
|
||||
exchange_rate = 1
|
||||
else:
|
||||
total_amount = ref_doc.grand_total
|
||||
total_amount = ref_doc.get("grand_total")
|
||||
if not exchange_rate:
|
||||
# Get the exchange rate from the original ref doc
|
||||
# or get it based on the posting date of the ref doc.
|
||||
@@ -1609,7 +1634,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
else:
|
||||
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
|
||||
outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid"))
|
||||
|
||||
else:
|
||||
# Get the exchange rate based on the posting date of the ref doc.
|
||||
@@ -1627,16 +1652,23 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
|
||||
def get_payment_entry(
|
||||
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
|
||||
party_type = set_party_type(dt)
|
||||
if not party_type:
|
||||
party_type = set_party_type(dt)
|
||||
|
||||
party_account = set_party_account(dt, dn, doc, party_type)
|
||||
party_account_currency = set_party_account_currency(dt, party_account, doc)
|
||||
payment_type = set_payment_type(dt, doc)
|
||||
|
||||
if not payment_type:
|
||||
payment_type = set_payment_type(dt, doc)
|
||||
|
||||
grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(
|
||||
party_amount, dt, party_account_currency, doc
|
||||
)
|
||||
@@ -1786,8 +1818,6 @@ def set_party_account(dt, dn, doc, party_type):
|
||||
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
|
||||
elif dt == "Purchase Invoice":
|
||||
party_account = doc.credit_to
|
||||
elif dt == "Fees":
|
||||
party_account = doc.receivable_account
|
||||
else:
|
||||
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
|
||||
return party_account
|
||||
@@ -1803,8 +1833,7 @@ def set_party_account_currency(dt, party_account, doc):
|
||||
|
||||
def set_payment_type(dt, doc):
|
||||
if (
|
||||
dt == "Sales Order"
|
||||
or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)
|
||||
dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)
|
||||
) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
|
||||
payment_type = "Receive"
|
||||
else:
|
||||
@@ -1822,18 +1851,15 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
|
||||
else:
|
||||
grand_total = doc.rounded_total or doc.grand_total
|
||||
outstanding_amount = doc.outstanding_amount
|
||||
elif dt == "Fees":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.outstanding_amount
|
||||
elif dt == "Dunning":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.grand_total
|
||||
else:
|
||||
if party_account_currency == doc.company_currency:
|
||||
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||
grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total"))
|
||||
else:
|
||||
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
|
||||
outstanding_amount = grand_total - flt(doc.advance_paid)
|
||||
grand_total = flt(doc.get("rounded_total") or doc.get("grand_total"))
|
||||
outstanding_amount = doc.get("outstanding_amount") or (grand_total - flt(doc.advance_paid))
|
||||
return grand_total, outstanding_amount
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
@@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
|
||||
)
|
||||
|
||||
def test_gl_of_multi_currency_payment_with_taxes(self):
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
)
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 100,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Test",
|
||||
},
|
||||
)
|
||||
payment_entry.target_exchange_rate = 80
|
||||
payment_entry.received_amount = 12.5
|
||||
payment_entry = payment_entry.submit()
|
||||
gle = qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
|
||||
expected_gl_entries = (
|
||||
("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
|
||||
("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
|
||||
("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
|
||||
)
|
||||
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"amount",
|
||||
"account_currency",
|
||||
"amount_in_account_currency",
|
||||
"delinked"
|
||||
"delinked",
|
||||
"remarks"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -136,12 +137,17 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"label": "Remarks"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-11 09:13:54.379168",
|
||||
"modified": "2022-08-22 15:32:56.629430",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
@@ -127,6 +128,25 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def create_sales_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
transaction_date=posting_date,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
currency="INR",
|
||||
qty=qty,
|
||||
rate=100,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return so
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
@@ -406,3 +426,89 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
|
||||
self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
||||
)
|
||||
def test_multi_payment_unlink_on_invoice_cancellation(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
for amt in [40, 40, 20]:
|
||||
# payment 1
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = amt
|
||||
pe.get("references")[0].allocated_amount = amt
|
||||
pe = pe.save().submit()
|
||||
|
||||
si.reload()
|
||||
si.cancel()
|
||||
|
||||
entries = frappe.db.get_list(
|
||||
"Payment Ledger Entry",
|
||||
filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(entries, [])
|
||||
|
||||
# with references removed, deletion should be possible
|
||||
si.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
||||
)
|
||||
def test_multi_je_unlink_on_invoice_cancellation(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# multiple JE's against invoice
|
||||
for amt in [40, 40, 20]:
|
||||
je1 = self.create_journal_entry(
|
||||
self.income_account, self.debit_to, amt, posting_date=transaction_date
|
||||
)
|
||||
je1.get("accounts")[1].party_type = "Customer"
|
||||
je1.get("accounts")[1].party = self.customer
|
||||
je1.get("accounts")[1].reference_type = si.doctype
|
||||
je1.get("accounts")[1].reference_name = si.name
|
||||
je1 = je1.save().submit()
|
||||
|
||||
si.reload()
|
||||
si.cancel()
|
||||
|
||||
entries = frappe.db.get_list(
|
||||
"Payment Ledger Entry",
|
||||
filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(entries, [])
|
||||
|
||||
# with references removed, deletion should be possible
|
||||
si.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
|
||||
)
|
||||
def test_advance_payment_unlink_on_order_cancellation(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit()
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name).save().submit()
|
||||
|
||||
so.reload()
|
||||
so.cancel()
|
||||
|
||||
entries = frappe.db.get_list(
|
||||
"Payment Ledger Entry",
|
||||
filters={"against_voucher_type": so.doctype, "against_voucher_no": so.name, "delinked": 0},
|
||||
)
|
||||
self.assertEqual(entries, [])
|
||||
|
||||
# with references removed, deletion should be possible
|
||||
so.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
update_reference_in_payment_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
@@ -22,6 +23,7 @@ class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
@@ -150,6 +152,7 @@ class PaymentReconciliation(Document):
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
@@ -187,6 +190,7 @@ class PaymentReconciliation(Document):
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
)
|
||||
@@ -209,6 +213,23 @@ class PaymentReconciliation(Document):
|
||||
inv.currency = entry.get("currency")
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, allocated_entry):
|
||||
if allocated_entry.get("reference_type") != "Payment Entry":
|
||||
return
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
row = self.get_payment_details(allocated_entry, dr_or_cr)
|
||||
|
||||
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
|
||||
update_reference_in_payment_entry(row, doc, do_not_save=True)
|
||||
|
||||
return doc.difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
@@ -224,12 +245,16 @@ class PaymentReconciliation(Document):
|
||||
res = self.get_allocated_entry(pay, inv, pay["amount"])
|
||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||
pay["amount"] = 0
|
||||
|
||||
res.difference_amount = self.get_difference_amount(res)
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
break
|
||||
elif inv.get("outstanding_amount") == 0:
|
||||
entries.append(res)
|
||||
continue
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -350,6 +375,7 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
self.ple_posting_date_filter.clear()
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
self.common_filter_conditions.append(ple.company == self.company)
|
||||
@@ -359,15 +385,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
if get_invoices:
|
||||
if self.from_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
if self.to_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
|
||||
elif get_return_invoices:
|
||||
if self.from_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_payment_date))
|
||||
if self.to_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
@@ -283,6 +283,41 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
def test_filter_posting_date_case2(self):
|
||||
"""
|
||||
Posting date should not affect outstanding amount calculation
|
||||
"""
|
||||
|
||||
from_date = add_days(nowdate(), -30)
|
||||
to_date = nowdate()
|
||||
self.create_payment_entry(amount=25, posting_date=from_date).submit()
|
||||
self.create_sales_invoice(rate=25, qty=1, posting_date=to_date)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.from_payment_date = from_date
|
||||
pr.to_invoice_date = pr.to_payment_date = to_date
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
pr.from_invoice_date = pr.from_payment_date = to_date
|
||||
pr.to_invoice_date = pr.to_payment_date = to_date
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
|
||||
def test_filter_invoice_limit(self):
|
||||
# check filter condition - invoice limit
|
||||
transaction_date = nowdate()
|
||||
|
||||
@@ -186,8 +186,10 @@
|
||||
{
|
||||
"fetch_from": "bank_account.bank",
|
||||
"fieldname": "bank",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Bank"
|
||||
"fieldtype": "Link",
|
||||
"label": "Bank",
|
||||
"options": "Bank",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.bank_account_no",
|
||||
@@ -366,10 +368,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 12:24:14.178853",
|
||||
"modified": "2022-09-30 16:19:43.680025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -401,5 +404,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "payment_term.description",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
@@ -159,7 +160,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-28 05:41:35.084233",
|
||||
"modified": "2022-09-16 13:57:06.382859",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -168,5 +169,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -36,6 +36,15 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
});
|
||||
|
||||
set_html_data(frm);
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frm.set_value("posting_date", frappe.datetime.nowdate());
|
||||
}
|
||||
if (!frm.doc.posting_time) {
|
||||
frm.set_value("posting_time", frappe.datetime.now_time());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"period_end_date",
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"pos_opening_entry",
|
||||
"status",
|
||||
"section_break_5",
|
||||
@@ -51,7 +52,6 @@
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Period End Date",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -219,6 +219,13 @@
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Error",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
@@ -228,10 +235,11 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-20 16:19:25.340565",
|
||||
"modified": "2022-08-01 11:37:14.991228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -278,5 +286,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -15,6 +15,9 @@ from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
class POSClosingEntry(StatusUpdater):
|
||||
def validate(self):
|
||||
self.posting_date = self.posting_date or frappe.utils.nowdate()
|
||||
self.posting_time = self.posting_time or frappe.utils.nowtime()
|
||||
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
|
||||
@@ -343,7 +343,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "POS Invoice",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1553,7 +1554,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-22 13:00:24.166684",
|
||||
"modified": "2022-09-30 03:49:50.455199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
)
|
||||
if flt(available_stock) <= 0:
|
||||
if is_stock_item and flt(available_stock) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
elif is_stock_item and flt(available_stock) < flt(d.qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
@@ -632,11 +632,12 @@ def get_stock_availability(item_code, warehouse):
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", item_code):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
# Is a service item
|
||||
is_stock_item = False
|
||||
# Is a service item or non_stock item
|
||||
return 0, is_stock_item
|
||||
|
||||
|
||||
@@ -650,7 +651,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles:
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
bundle_bin_qty = max_available_bundles
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
|
||||
|
||||
@@ -495,6 +495,67 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos.submit)
|
||||
|
||||
def test_value_error_on_serial_no_validation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
se = make_serialized_item(
|
||||
company="_Test Company",
|
||||
target_warehouse="Stores - _TC",
|
||||
cost_center="Main - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
)
|
||||
serial_nos = se.get("items")[0].serial_no
|
||||
|
||||
# make a pos invoice
|
||||
pos = create_pos_invoice(
|
||||
company="_Test Company",
|
||||
debit_to="Debtors - _TC",
|
||||
account_for_change_amount="Cash - _TC",
|
||||
warehouse="Stores - _TC",
|
||||
income_account="Sales - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
item=se.get("items")[0].item_code,
|
||||
rate=1000,
|
||||
qty=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos.get("items")[0].has_serial_no = 1
|
||||
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
|
||||
pos.set("payments", [])
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
)
|
||||
pos = pos.save().submit()
|
||||
|
||||
# make a return
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.paid_amount = pos_return.grand_total
|
||||
pos_return.save()
|
||||
pos_return.submit()
|
||||
|
||||
# set docstatus to 2 for pos to trigger this issue
|
||||
frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2)
|
||||
|
||||
pos2 = create_pos_invoice(
|
||||
company="_Test Company",
|
||||
debit_to="Debtors - _TC",
|
||||
account_for_change_amount="Cash - _TC",
|
||||
warehouse="Stores - _TC",
|
||||
income_account="Sales - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
item=se.get("items")[0].item_code,
|
||||
rate=1000,
|
||||
qty=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pos2.get("items")[0].has_serial_no = 1
|
||||
pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
|
||||
# Value error should not be triggered on validation
|
||||
pos2.save()
|
||||
|
||||
def test_loyalty_points(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
"column_break_3",
|
||||
"pos_closing_entry",
|
||||
@@ -105,12 +106,19 @@
|
||||
"label": "Customer Group",
|
||||
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||
"options": "Customer Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-14 11:17:19.001142",
|
||||
"modified": "2022-08-01 11:36:42.456429",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -173,5 +181,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.utils import cint, flt, getdate, nowdate
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -99,6 +99,7 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
@@ -115,6 +116,7 @@ class POSInvoiceMergeLog(Document):
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
@@ -402,6 +404,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_date = (
|
||||
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
|
||||
)
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"currency",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"write_off_limit",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
@@ -360,6 +361,14 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Auto write off precision loss while consolidation",
|
||||
"fieldname": "write_off_limit",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Write Off Limit",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the consolidated invoices will have rounded total disabled",
|
||||
@@ -393,7 +402,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-07-21 11:16:46.911173",
|
||||
"modified": "2022-08-10 12:57:06.241439",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -52,7 +52,10 @@
|
||||
"free_item_rate",
|
||||
"column_break_42",
|
||||
"free_item_uom",
|
||||
"round_free_qty",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"section_break_23",
|
||||
"valid_from",
|
||||
"valid_upto",
|
||||
@@ -176,7 +179,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions",
|
||||
"fieldname": "section_break_18",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount on Other Item"
|
||||
@@ -297,12 +300,12 @@
|
||||
{
|
||||
"fieldname": "min_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Min Qty"
|
||||
"label": "Min Qty (As Per Stock UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "max_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Max Qty"
|
||||
"label": "Max Qty (As Per Stock UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
@@ -481,7 +484,7 @@
|
||||
"description": "System will notify to increase or decrease quantity or amount ",
|
||||
"fieldname": "threshold_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Threshold for Suggestion"
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"description": "Higher the number, higher the priority",
|
||||
@@ -578,15 +581,38 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "PRLE-.####"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_recursive",
|
||||
"description": "Give free item for every N quantity",
|
||||
"fieldname": "recurse_for",
|
||||
"fieldtype": "Float",
|
||||
"label": "Recurse Every (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_recursive",
|
||||
"description": "Qty for which recursion isn't applicable.",
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-06 15:10:04.219321",
|
||||
"modified": "2022-10-13 19:05:35.056304",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -642,5 +668,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class PricingRule(Document):
|
||||
self.validate_applicable_for_selling_or_buying()
|
||||
self.validate_min_max_amt()
|
||||
self.validate_min_max_qty()
|
||||
self.validate_recursion()
|
||||
self.cleanup_fields_value()
|
||||
self.validate_rate_or_discount()
|
||||
self.validate_max_discount()
|
||||
@@ -109,6 +110,18 @@ class PricingRule(Document):
|
||||
if self.min_amt and self.max_amt and flt(self.min_amt) > flt(self.max_amt):
|
||||
throw(_("Min Amt can not be greater than Max Amt"))
|
||||
|
||||
def validate_recursion(self):
|
||||
if self.price_or_product_discount != "Product":
|
||||
return
|
||||
if self.free_item or self.same_item:
|
||||
if flt(self.recurse_for) <= 0:
|
||||
self.recurse_for = 1
|
||||
if self.is_recursive:
|
||||
if flt(self.apply_recursion_over) > flt(self.min_qty):
|
||||
throw(_("Min Qty should be greater than Recurse Over Qty"))
|
||||
if flt(self.apply_recursion_over) < 0:
|
||||
throw(_("Recurse Over Qty cannot be less than 0"))
|
||||
|
||||
def cleanup_fields_value(self):
|
||||
for logic_field in ["apply_on", "applicable_for", "rate_or_discount"]:
|
||||
fieldname = frappe.scrub(self.get(logic_field) or "")
|
||||
@@ -268,6 +281,18 @@ def get_serial_no_for_item(args):
|
||||
return item_details
|
||||
|
||||
|
||||
def update_pricing_rule_uom(pricing_rule, args):
|
||||
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
|
||||
pricing_rule.apply_on
|
||||
)
|
||||
|
||||
apply_on_field = frappe.scrub(pricing_rule.apply_on)
|
||||
|
||||
for row in pricing_rule.get(child_doc):
|
||||
if row.get(apply_on_field) == args.get(apply_on_field):
|
||||
pricing_rule.uom = row.uom
|
||||
|
||||
|
||||
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
@@ -324,7 +349,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
|
||||
if isinstance(pricing_rule, str):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -337,7 +363,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
|
||||
item_details.update(
|
||||
{
|
||||
"apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items),
|
||||
"price_or_product_discount": pricing_rule.price_or_product_discount,
|
||||
"apply_rule_on": (
|
||||
frappe.scrub(pricing_rule.apply_rule_on_other)
|
||||
@@ -347,6 +372,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
}
|
||||
)
|
||||
|
||||
if pricing_rule.apply_rule_on_other_items:
|
||||
item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code == None:
|
||||
return item_details
|
||||
|
||||
@@ -438,12 +466,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
if pricing_rule.currency == args.currency:
|
||||
pricing_rule_rate = pricing_rule.rate
|
||||
|
||||
# TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way.
|
||||
if pricing_rule_rate:
|
||||
is_blank_uom = pricing_rule.get("uom") != args.get("uom")
|
||||
# Override already set price list rate (from item price)
|
||||
# if pricing_rule_rate > 0
|
||||
item_details.update(
|
||||
{
|
||||
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
|
||||
"price_list_rate": pricing_rule_rate
|
||||
* (args.get("conversion_factor", 1) if is_blank_uom else 1),
|
||||
}
|
||||
)
|
||||
item_details.update({"discount_percentage": 0.0})
|
||||
@@ -492,7 +523,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
|
||||
)
|
||||
|
||||
if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"):
|
||||
items = get_pricing_rule_items(pricing_rule)
|
||||
items = get_pricing_rule_items(pricing_rule, other_items=True)
|
||||
item_details.apply_on = (
|
||||
frappe.scrub(pricing_rule.apply_rule_on_other)
|
||||
if pricing_rule.apply_rule_on_other
|
||||
|
||||
@@ -595,6 +595,121 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||
item.delete()
|
||||
|
||||
def test_item_price_with_blank_uom_pricing_rule(self):
|
||||
properties = {
|
||||
"item_code": "Item Blank UOM",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item Blank UOM", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item Blank UOM Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "Item Blank UOM",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
|
||||
self.assertEqual(si.items[0].price_list_rate, 1010)
|
||||
self.assertEqual(si.items[0].rate, 1010)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete()
|
||||
|
||||
item.delete()
|
||||
|
||||
def test_item_price_with_selling_uom_pricing_rule(self):
|
||||
properties = {
|
||||
"item_code": "Item UOM other than Stock",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item UOM other than Stock", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item UOM other than Stock Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "Item UOM other than Stock",
|
||||
"uom": "Box",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so apply pricing_rule only on Box UOM.
|
||||
# Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so pricing_rule won't apply as selling_uom is Nos.
|
||||
# As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete()
|
||||
|
||||
item.delete()
|
||||
|
||||
def test_pricing_rule_for_different_currency(self):
|
||||
make_item("Test Sanitizer Item")
|
||||
|
||||
@@ -766,6 +881,107 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
def test_pricing_rule_for_other_items_cond_with_amount(self):
|
||||
item = make_item("Water Flask New")
|
||||
other_item = make_item("Other Water Flask New")
|
||||
make_item_price(item.name, "_Test Price List", 100)
|
||||
make_item_price(other_item.name, "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Water Flask Rule",
|
||||
"apply_on": "Item Code",
|
||||
"apply_rule_on_other": "Item Code",
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"other_item_code": other_item.name,
|
||||
"items": [
|
||||
{
|
||||
"item_code": item.name,
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"min_amt": 200,
|
||||
"discount_percentage": 10,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code=item.name)
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": other_item.name,
|
||||
"item_name": other_item.item_name,
|
||||
"description": other_item.description,
|
||||
"stock_uom": other_item.stock_uom,
|
||||
"uom": other_item.stock_uom,
|
||||
"cost_center": si.items[0].cost_center,
|
||||
"expense_account": si.items[0].expense_account,
|
||||
"warehouse": si.items[0].warehouse,
|
||||
"conversion_factor": 1,
|
||||
"qty": 1,
|
||||
},
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].discount_percentage, 0)
|
||||
self.assertEqual(si.items[1].discount_percentage, 0)
|
||||
|
||||
si.items[0].qty = 2
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].discount_percentage, 0)
|
||||
self.assertEqual(si.items[0].stock_qty, 2)
|
||||
self.assertEqual(si.items[0].amount, 200)
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[1].discount_percentage, 10)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
|
||||
def test_pricing_rule_for_product_free_item_rounded_qty_and_recursion(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate": 0,
|
||||
"min_qty": 3,
|
||||
"max_qty": 7,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 1,
|
||||
"free_qty": 1,
|
||||
"round_free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 2,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With pricing rule
|
||||
so = make_sales_order(item_code="_Test Item", qty=5)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 2)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=7)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 4)
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values):
|
||||
)
|
||||
|
||||
if apply_on_field == "item_code":
|
||||
if args.get("uom", None):
|
||||
item_conditions += (
|
||||
" and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
|
||||
child_doc=child_doc, item_uom=args.get("uom")
|
||||
)
|
||||
)
|
||||
if "variant_of" not in args:
|
||||
args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of")
|
||||
|
||||
@@ -252,12 +258,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
stock_qty = flt(args.get("stock_qty"))
|
||||
amount = flt(args.get("price_list_rate")) * flt(args.get("qty"))
|
||||
|
||||
if pricing_rules[0].apply_rule_on_other:
|
||||
field = frappe.scrub(pricing_rules[0].apply_rule_on_other)
|
||||
|
||||
if field and pricing_rules[0].get("other_" + field) != args.get(field):
|
||||
return
|
||||
|
||||
pr_doc = frappe.get_cached_doc("Pricing Rule", pricing_rules[0].name)
|
||||
|
||||
if pricing_rules[0].mixed_conditions and doc:
|
||||
@@ -274,7 +274,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
amount += data[1]
|
||||
|
||||
if pricing_rules[0].apply_rule_on_other and not pricing_rules[0].mixed_conditions and doc:
|
||||
pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules) or []
|
||||
pricing_rules = get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, args) or []
|
||||
else:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, args)
|
||||
|
||||
@@ -352,16 +352,14 @@ def validate_quantity_and_amount_for_suggestion(args, qty, amount, item_code, tr
|
||||
if fieldname:
|
||||
msg = _(
|
||||
"If you {0} {1} quantities of the item {2}, the scheme {3} will be applied on the item."
|
||||
).format(
|
||||
type_of_transaction, args.get(fieldname), bold(item_code), bold(args.rule_description)
|
||||
)
|
||||
).format(type_of_transaction, args.get(fieldname), bold(item_code), bold(args.title))
|
||||
|
||||
if fieldname in ["min_amt", "max_amt"]:
|
||||
msg = _("If you {0} {1} worth item {2}, the scheme {3} will be applied on the item.").format(
|
||||
type_of_transaction,
|
||||
fmt_money(args.get(fieldname), currency=args.get("currency")),
|
||||
bold(item_code),
|
||||
bold(args.rule_description),
|
||||
bold(args.title),
|
||||
)
|
||||
|
||||
frappe.msgprint(msg)
|
||||
@@ -454,17 +452,29 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
|
||||
return sum_qty, sum_amt, items
|
||||
|
||||
|
||||
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
|
||||
items = get_pricing_rule_items(pr_doc)
|
||||
def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
other_items = get_pricing_rule_items(pr_doc, other_items=True)
|
||||
pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on"))
|
||||
apply_on = frappe.scrub(pr_doc.get("apply_on"))
|
||||
|
||||
items = []
|
||||
for d in pr_doc.get(pricing_rule_apply_on):
|
||||
if apply_on == "item_group":
|
||||
items.extend(get_child_item_groups(d.get(apply_on)))
|
||||
else:
|
||||
items.append(d.get(apply_on))
|
||||
|
||||
for row in doc.items:
|
||||
if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(
|
||||
row.get("stock_qty"), row.get("amount"), pricing_rules, row
|
||||
)
|
||||
if row.get(apply_on) in items:
|
||||
if not row.get("qty"):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
pricing_rules[0].apply_rule_on_other_items = items
|
||||
pricing_rules[0].apply_rule_on_other_items = other_items
|
||||
return pricing_rules
|
||||
|
||||
|
||||
@@ -617,9 +627,13 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
qty = pricing_rule.free_qty or 1
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = args.get("qty") if args else doc.total_qty
|
||||
transaction_qty = (
|
||||
args.get("qty") if args else doc.total_qty
|
||||
) - pricing_rule.apply_recursion_over
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = round(qty)
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
@@ -658,21 +672,21 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values
|
||||
doc.append("items", args)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc):
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
apply_on_data = []
|
||||
apply_on = frappe.scrub(pr_doc.get("apply_on"))
|
||||
|
||||
pricing_rule_apply_on = apply_on_table.get(pr_doc.get("apply_on"))
|
||||
|
||||
for d in pr_doc.get(pricing_rule_apply_on):
|
||||
if apply_on == "item_group":
|
||||
apply_on_data.extend(get_child_item_groups(d.get(apply_on)))
|
||||
else:
|
||||
apply_on_data.append(d.get(apply_on))
|
||||
|
||||
if pr_doc.apply_rule_on_other:
|
||||
if pr_doc.apply_rule_on_other and other_items:
|
||||
apply_on = frappe.scrub(pr_doc.apply_rule_on_other)
|
||||
apply_on_data.append(pr_doc.get("other_" + apply_on))
|
||||
else:
|
||||
for d in pr_doc.get(pricing_rule_apply_on):
|
||||
if apply_on == "item_group":
|
||||
apply_on_data.extend(get_child_item_groups(d.get(apply_on)))
|
||||
else:
|
||||
apply_on_data.append(d.get(apply_on))
|
||||
|
||||
return list(set(apply_on_data))
|
||||
|
||||
|
||||
@@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
|
||||
filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
|
||||
)
|
||||
|
||||
make_gl_entries(gl_entries=gl_entries, cancel=1)
|
||||
make_gl_entries(gl_map=gl_entries, cancel=1)
|
||||
|
||||
@@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-10")
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-01-01",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-01-31",
|
||||
type="Income",
|
||||
)
|
||||
)
|
||||
pda.submit()
|
||||
pda.cancel()
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered" style="font-size: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
|
||||
@@ -34,8 +34,8 @@ pricing_rule_fields = [
|
||||
other_fields = [
|
||||
"min_qty",
|
||||
"max_qty",
|
||||
"min_amt",
|
||||
"max_amt",
|
||||
"min_amount",
|
||||
"max_amount",
|
||||
"priority",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -246,7 +246,11 @@ def prepare_pricing_rule(
|
||||
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||
pr.update(args)
|
||||
for field in other_fields + discount_fields:
|
||||
pr.set(field, child_doc_fields.get(field))
|
||||
target_field = field
|
||||
if target_field in ["min_amount", "max_amount"]:
|
||||
target_field = "min_amt" if field == "min_amount" else "max_amt"
|
||||
|
||||
pr.set(target_field, child_doc_fields.get(field))
|
||||
|
||||
pr.promotional_scheme_id = child_doc_fields.name
|
||||
pr.promotional_scheme = doc.name
|
||||
|
||||
@@ -90,6 +90,23 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_min_max_amount_configuration(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.price_discount_slabs[0].min_amount = 10
|
||||
ps.price_discount_slabs[0].max_amount = 1000
|
||||
ps.save()
|
||||
|
||||
price_rules_data = frappe.db.get_value(
|
||||
"Pricing Rule", {"promotional_scheme": ps.name}, ["min_amt", "max_amt"], as_dict=1
|
||||
)
|
||||
|
||||
self.assertEqual(price_rules_data.min_amt, 10)
|
||||
self.assertEqual(price_rules_data.max_amt, 1000)
|
||||
|
||||
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
frm.clear_table("tax_withheld_vouchers");
|
||||
}
|
||||
},
|
||||
|
||||
is_subcontracted: function(frm) {
|
||||
|
||||
@@ -12,39 +12,23 @@
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"tax_id",
|
||||
"due_date",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"company",
|
||||
"column_break_6",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"due_date",
|
||||
"column_break1",
|
||||
"is_paid",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"supplier_invoice_details",
|
||||
"bill_no",
|
||||
"column_break_15",
|
||||
"bill_date",
|
||||
"returns",
|
||||
"return_against",
|
||||
"section_addresses",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"col_break_address",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -54,37 +38,33 @@
|
||||
"plc_conversion_rate",
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"set_warehouse",
|
||||
"rejected_warehouse",
|
||||
"col_break_warehouse",
|
||||
"set_from_warehouse",
|
||||
"supplier_warehouse",
|
||||
"is_subcontracted",
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"col_break_warehouse",
|
||||
"update_stock",
|
||||
"set_warehouse",
|
||||
"set_from_warehouse",
|
||||
"is_subcontracted",
|
||||
"rejected_warehouse",
|
||||
"supplier_warehouse",
|
||||
"items_section",
|
||||
"items",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"raw_materials_supplied",
|
||||
"supplied_items",
|
||||
"section_break_26",
|
||||
"total_qty",
|
||||
"total_net_weight",
|
||||
"column_break_50",
|
||||
"base_total",
|
||||
"base_net_total",
|
||||
"column_break_28",
|
||||
"total_net_weight",
|
||||
"total",
|
||||
"net_total",
|
||||
"taxes_section",
|
||||
"taxes_and_charges",
|
||||
"column_break_58",
|
||||
"tax_category",
|
||||
"column_break_49",
|
||||
"shipping_rule",
|
||||
"section_break_51",
|
||||
"taxes_and_charges",
|
||||
"taxes",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"totals",
|
||||
"base_taxes_and_charges_added",
|
||||
"base_taxes_and_charges_deducted",
|
||||
@@ -93,13 +73,6 @@
|
||||
"taxes_and_charges_added",
|
||||
"taxes_and_charges_deducted",
|
||||
"total_taxes_and_charges",
|
||||
"section_break_44",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_46",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"section_break_49",
|
||||
"base_grand_total",
|
||||
"base_rounding_adjustment",
|
||||
@@ -113,24 +86,58 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_break_44",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_46",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"tax_withheld_vouchers_section",
|
||||
"tax_withheld_vouchers",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"raw_materials_supplied",
|
||||
"supplied_items",
|
||||
"payments_tab",
|
||||
"payments_section",
|
||||
"mode_of_payment",
|
||||
"cash_bank_account",
|
||||
"base_paid_amount",
|
||||
"clearance_date",
|
||||
"col_br_payments",
|
||||
"cash_bank_account",
|
||||
"paid_amount",
|
||||
"base_paid_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
"write_off",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"column_break_61",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
"address_and_contact_tab",
|
||||
"section_addresses",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
"col_break_address",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"company_shipping_address_section",
|
||||
"shipping_address",
|
||||
"column_break_126",
|
||||
"shipping_address_display",
|
||||
"company_billing_address_section",
|
||||
"billing_address",
|
||||
"column_break_130",
|
||||
"billing_address_display",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -138,23 +145,15 @@
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"printing_settings",
|
||||
"letter_head",
|
||||
"select_print_heading",
|
||||
"column_break_112",
|
||||
"group_same_items",
|
||||
"language",
|
||||
"sb_14",
|
||||
"on_hold",
|
||||
"release_date",
|
||||
"cb_17",
|
||||
"hold_comment",
|
||||
"more_info",
|
||||
"more_info_tab",
|
||||
"status_section",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"column_break_147",
|
||||
"is_internal_supplier",
|
||||
"column_break_177",
|
||||
"per_received",
|
||||
"supplier_invoice_details",
|
||||
"bill_no",
|
||||
"column_break_15",
|
||||
"bill_date",
|
||||
"accounting_details_section",
|
||||
"credit_to",
|
||||
"party_account_currency",
|
||||
@@ -162,15 +161,32 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"remarks",
|
||||
"subscription_section",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"column_break_114",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"per_received",
|
||||
"is_old_subcontracting_flow"
|
||||
"column_break_114",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
"column_break_112",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"sb_14",
|
||||
"on_hold",
|
||||
"release_date",
|
||||
"cb_17",
|
||||
"hold_comment",
|
||||
"additional_info_section",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"column_break_147",
|
||||
"inter_company_invoice_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"remarks",
|
||||
"connections_tab",
|
||||
"column_break_38"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -353,7 +369,7 @@
|
||||
"collapsible_depends_on": "bill_no",
|
||||
"fieldname": "supplier_invoice_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier Invoice Details"
|
||||
"label": "Supplier Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "bill_no",
|
||||
@@ -376,12 +392,6 @@
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "return_against",
|
||||
"fieldname": "returns",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Returns"
|
||||
},
|
||||
{
|
||||
"depends_on": "return_against",
|
||||
"fieldname": "return_against",
|
||||
@@ -393,10 +403,9 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_addresses",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"label": "Supplier Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_address",
|
||||
@@ -512,17 +521,17 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sec_warehouse",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Items"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"description": "Sets 'Accepted Warehouse' in each row of the items table.",
|
||||
"fieldname": "set_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Set Accepted Warehouse",
|
||||
@@ -531,7 +540,6 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"description": "Warehouse where you are maintaining stock of rejected items",
|
||||
"fieldname": "rejected_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Warehouse",
|
||||
@@ -554,6 +562,7 @@
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
},
|
||||
@@ -581,6 +590,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "pricing_rule_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Pricing Rules"
|
||||
@@ -593,6 +603,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "supplied_items",
|
||||
"fieldname": "raw_materials_supplied",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -656,6 +667,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "total_net_weight",
|
||||
"fieldname": "total_net_weight",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Net Weight",
|
||||
@@ -665,6 +677,8 @@
|
||||
{
|
||||
"fieldname": "taxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Taxes and Charges",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-money"
|
||||
},
|
||||
@@ -688,7 +702,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_51",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes_and_charges",
|
||||
@@ -792,7 +807,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "discount_amount",
|
||||
"fieldname": "section_break_44",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Discount"
|
||||
@@ -832,7 +846,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
@@ -1003,8 +1018,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "write_off_amount",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "write_off",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Write Off"
|
||||
@@ -1081,7 +1094,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:(!doc.is_return)",
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1102,8 +1114,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "terms",
|
||||
"fieldname": "terms_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Terms and Conditions",
|
||||
@@ -1119,13 +1129,13 @@
|
||||
{
|
||||
"fieldname": "terms",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Terms and Conditions1"
|
||||
"label": "Terms and Conditions"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "printing_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Printing Settings"
|
||||
"label": "Print Settings"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -1166,15 +1176,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
@@ -1260,7 +1261,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Section",
|
||||
"label": "Subscription",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -1339,7 +1340,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_internal_supplier",
|
||||
"description": "Unrealized Profit / Loss account for intra-company transfers",
|
||||
"description": "Unrealized Profit/Loss account for intra-company transfers",
|
||||
"fieldname": "unrealized_profit_loss_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Unrealized Profit / Loss Account",
|
||||
@@ -1356,7 +1357,6 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
|
||||
"description": "Sets 'From Warehouse' in each row of the items table.",
|
||||
"fieldname": "set_from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Set From Warehouse",
|
||||
@@ -1367,7 +1367,7 @@
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Warehouse",
|
||||
@@ -1386,12 +1386,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Additional Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
@@ -1426,13 +1420,106 @@
|
||||
"hidden": 1,
|
||||
"label": "Is Old Subcontracting Flow",
|
||||
"read_only": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"collapsible_depends_on": "tax_withheld_vouchers",
|
||||
"fieldname": "tax_withheld_vouchers_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withheld Vouchers"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withheld_vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withheld Vouchers",
|
||||
"no_copy": 1,
|
||||
"options": "Tax Withheld Vouchers",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_58",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_126",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_billing_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Billing Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_130",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "status_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_177",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_info_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Info",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-15 15:40:58.527065",
|
||||
"modified": "2022-10-11 13:04:44.304389",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1492,6 +1579,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
|
||||
@@ -71,6 +71,9 @@ class PurchaseInvoice(BuyingController):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
|
||||
if self.is_new():
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
def before_save(self):
|
||||
if not self.on_hold:
|
||||
self.release_date = ""
|
||||
@@ -575,7 +578,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
if self.check_asset_cwip_enabled():
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
@@ -670,9 +672,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||
|
||||
enable_discount_accounting = cint(
|
||||
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
|
||||
)
|
||||
provisional_accounting_for_non_stock_items = cint(
|
||||
frappe.db.get_value(
|
||||
"Company", self.company, "enable_provisional_accounting_for_non_stock_items"
|
||||
@@ -709,6 +708,10 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
credit_amount = item.base_net_amount
|
||||
if self.is_internal_supplier and item.valuation_rate:
|
||||
credit_amount = flt(item.valuation_rate * item.stock_qty)
|
||||
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -718,7 +721,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
|
||||
},
|
||||
warehouse_account[item.from_warehouse]["account_currency"],
|
||||
item=item,
|
||||
@@ -807,7 +810,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if not item.is_fixed_asset:
|
||||
dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
|
||||
@@ -1160,12 +1163,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
# tax table gl entries
|
||||
valuation_tax = {}
|
||||
enable_discount_accounting = cint(
|
||||
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting)
|
||||
amount, base_amount = self.get_tax_amounts(tax, None)
|
||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
@@ -1250,15 +1250,6 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
self._enable_discount_accounting = cint(
|
||||
frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
return self._enable_discount_accounting
|
||||
|
||||
def make_internal_transfer_gl_entries(self, gl_entries):
|
||||
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
|
||||
account_currency = get_account_currency(self.unrealized_profit_loss_account)
|
||||
@@ -1427,6 +1418,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
@@ -1520,7 +1512,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if not self.tax_withholding_category:
|
||||
return
|
||||
|
||||
tax_withholding_details, advance_taxes = get_party_tax_withholding_details(
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
self, self.tax_withholding_category
|
||||
)
|
||||
|
||||
@@ -1549,6 +1541,19 @@ class PurchaseInvoice(BuyingController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
{
|
||||
"voucher_name": voucher_no,
|
||||
"voucher_type": voucher_details.get("voucher_type"),
|
||||
"taxable_amount": voucher_details.get("amount"),
|
||||
},
|
||||
)
|
||||
|
||||
# calculate totals again after applying TDS
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
@@ -1791,4 +1796,6 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.set_onload("ignore_price_list", True)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -338,59 +338,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
|
||||
def test_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
pi = make_purchase_invoice(discount_account=discount_account, rate=45)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 225.0, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 25.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
@change_settings("Buying Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
|
||||
pi.apply_discount_on = "Grand Total"
|
||||
pi.additional_discount_account = additional_discount_account
|
||||
pi.additional_discount_percentage = 10
|
||||
pi.disable_rounded_total = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
|
||||
["_Test Account VAT - _TC", 25.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 247.5, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 27.5, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(test_records[1])
|
||||
pi.insert()
|
||||
@@ -1596,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].conversion_factor, 1000)
|
||||
|
||||
def test_batch_expiry_for_purchase_invoice(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
item = self.make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
qty=1,
|
||||
item_code=item.name,
|
||||
update_stock=True,
|
||||
)
|
||||
|
||||
pi.load_from_db()
|
||||
batch_no = pi.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
|
||||
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.save().submit()
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
"manufacturer_part_no",
|
||||
"accounting",
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"col_break5",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
@@ -215,6 +214,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
@@ -712,6 +712,7 @@
|
||||
"label": "Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -820,6 +821,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
@@ -860,12 +862,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "product_bundle",
|
||||
"fieldtype": "Link",
|
||||
@@ -877,7 +873,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-17 05:31:10.520171",
|
||||
"modified": "2022-10-26 16:05:37.304788",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -479,9 +479,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
is_cash_or_non_trade_discount() {
|
||||
this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount);
|
||||
this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount);
|
||||
|
||||
if (!this.frm.doc.is_cash_or_non_trade_discount) {
|
||||
this.frm.set_value("additional_discount_account", "");
|
||||
}
|
||||
|
||||
this.calculate_taxes_and_totals();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,44 +12,29 @@
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"pos_profile",
|
||||
"is_pos",
|
||||
"is_consolidated",
|
||||
"is_return",
|
||||
"is_debit_note",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"column_break1",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"column_break1",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"due_date",
|
||||
"column_break_14",
|
||||
"is_pos",
|
||||
"pos_profile",
|
||||
"is_consolidated",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"is_debit_note",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"dispatch_address_name",
|
||||
"dispatch_address",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"column_break_27",
|
||||
"campaign",
|
||||
"source",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -58,60 +43,35 @@
|
||||
"price_list_currency",
|
||||
"plc_conversion_rate",
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"set_warehouse",
|
||||
"column_break_55",
|
||||
"set_target_warehouse",
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"update_stock",
|
||||
"column_break_39",
|
||||
"set_warehouse",
|
||||
"set_target_warehouse",
|
||||
"section_break_42",
|
||||
"items",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"packing_list",
|
||||
"packed_items",
|
||||
"product_bundle_help",
|
||||
"time_sheet_list",
|
||||
"timesheets",
|
||||
"total_billing_amount",
|
||||
"total_billing_hours",
|
||||
"section_break_30",
|
||||
"total_qty",
|
||||
"total_net_weight",
|
||||
"column_break_32",
|
||||
"base_total",
|
||||
"base_net_total",
|
||||
"column_break_32",
|
||||
"total_net_weight",
|
||||
"column_break_52",
|
||||
"total",
|
||||
"net_total",
|
||||
"taxes_section",
|
||||
"taxes_and_charges",
|
||||
"column_break_38",
|
||||
"shipping_rule",
|
||||
"column_break_55",
|
||||
"tax_category",
|
||||
"section_break_40",
|
||||
"taxes",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"section_break_43",
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"total_taxes_and_charges",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"section_break_49",
|
||||
"apply_discount_on",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"totals",
|
||||
"base_grand_total",
|
||||
"base_rounding_adjustment",
|
||||
@@ -125,21 +85,28 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"section_break_49",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
"packing_list",
|
||||
"packed_items",
|
||||
"product_bundle_help",
|
||||
"time_sheet_list",
|
||||
"timesheets",
|
||||
"section_break_104",
|
||||
"total_billing_hours",
|
||||
"column_break_106",
|
||||
"total_billing_amount",
|
||||
"payments_tab",
|
||||
"payments_section",
|
||||
"cash_bank_account",
|
||||
"payments",
|
||||
@@ -152,47 +119,95 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"write_off_section",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"loyalty_points_redemption",
|
||||
"redeem_loyalty_points",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"contact_and_address_tab",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"col_break4",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"shipping_address_section",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"shipping_addr_col_break",
|
||||
"dispatch_address_name",
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_addr_col_break",
|
||||
"company_address_display",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
"select_print_heading",
|
||||
"column_break_84",
|
||||
"language",
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"customer_group",
|
||||
"campaign",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"source",
|
||||
"more_info_tab",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"is_opening",
|
||||
"column_break8",
|
||||
"unrealized_profit_loss_account",
|
||||
"remarks",
|
||||
"against_income_account",
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break10",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break10",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break2",
|
||||
"sales_team",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
"column_break_84",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"subscription_section",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"column_break_140",
|
||||
"auto_repeat",
|
||||
"column_break_140",
|
||||
"to_date",
|
||||
"update_auto_repeat_reference",
|
||||
"against_income_account"
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"customer_group",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -453,12 +468,11 @@
|
||||
"label": "Customer's Purchase Order Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Address and Contact"
|
||||
"label": "Billing Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_address",
|
||||
@@ -560,7 +574,6 @@
|
||||
{
|
||||
"fieldname": "company_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Company Address",
|
||||
@@ -649,16 +662,8 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sec_warehouse",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "set_warehouse",
|
||||
@@ -672,6 +677,7 @@
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Items",
|
||||
@@ -703,7 +709,6 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Items",
|
||||
"oldfieldname": "entries",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Invoice Item",
|
||||
@@ -756,9 +761,10 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
||||
"depends_on": "eval: !doc.is_return",
|
||||
"depends_on": "eval:!doc.is_return",
|
||||
"fieldname": "time_sheet_list",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheet List"
|
||||
@@ -846,6 +852,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "total_net_weight",
|
||||
"fieldname": "total_net_weight",
|
||||
"fieldtype": "Float",
|
||||
"hide_days": 1,
|
||||
@@ -857,8 +864,10 @@
|
||||
{
|
||||
"fieldname": "taxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Taxes and Charges",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-money"
|
||||
},
|
||||
@@ -901,6 +910,7 @@
|
||||
{
|
||||
"fieldname": "section_break_40",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1
|
||||
},
|
||||
@@ -909,7 +919,6 @@
|
||||
"fieldtype": "Table",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Sales Taxes and Charges",
|
||||
"oldfieldname": "other_charges",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Taxes and Charges"
|
||||
@@ -1047,7 +1056,6 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "discount_amount",
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
@@ -1103,6 +1111,7 @@
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Totals",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-money",
|
||||
"print_hide": 1
|
||||
@@ -1284,8 +1293,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
@@ -1315,7 +1322,9 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_pos===1||(doc.advances && doc.advances.length>0)",
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:!doc.is_pos",
|
||||
"depends_on": "eval:doc.is_pos===1",
|
||||
"fieldname": "payments_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
@@ -1353,6 +1362,7 @@
|
||||
"hide_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points",
|
||||
"fieldname": "base_paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@@ -1384,10 +1394,13 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "is_pos",
|
||||
"fieldname": "section_break_88",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1
|
||||
"hide_seconds": 1,
|
||||
"label": "Changes"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_pos",
|
||||
@@ -1428,17 +1441,6 @@
|
||||
"options": "Account",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "write_off_amount",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "column_break4",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Write Off",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "write_off_amount",
|
||||
"fieldtype": "Currency",
|
||||
@@ -1496,8 +1498,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "terms",
|
||||
"fieldname": "terms_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
@@ -1531,7 +1531,7 @@
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Printing Settings"
|
||||
"label": "Print Settings"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -1592,7 +1592,7 @@
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "More Information"
|
||||
"label": "Additional Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "inter_company_invoice_reference",
|
||||
@@ -1815,7 +1815,7 @@
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Subscription Section"
|
||||
"label": "Subscription"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -1936,10 +1936,6 @@
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_55",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_internal_customer && doc.update_stock",
|
||||
"fieldname": "set_target_warehouse",
|
||||
@@ -2010,6 +2006,97 @@
|
||||
"fieldname": "is_cash_or_non_trade_discount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cash or Non Trade Discount"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact & Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_39",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_42",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_55",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_addr_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_addr_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_27",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_52",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
|
||||
"fieldname": "section_break_104",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_106",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "write_off_amount",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "write_off_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Write Off",
|
||||
"width": "50%"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2022,7 +2109,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified": "2022-10-11 13:07:36.488095",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -7,17 +7,7 @@ from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
@@ -32,10 +22,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
make_depreciation_entry,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@@ -186,7 +178,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") or (
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
|
||||
asset.status == "Sold" and not self.is_return
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -710,6 +702,7 @@ class SalesInvoice(SellingController):
|
||||
if (
|
||||
cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
|
||||
and not self.is_return
|
||||
and not self.is_internal_customer
|
||||
):
|
||||
self.validate_rate_with_reference_doc(
|
||||
[["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]]
|
||||
@@ -1033,22 +1026,6 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.additional_discount_account,
|
||||
"against": self.debit_to,
|
||||
"debit": self.base_discount_amount,
|
||||
"debit_in_account_currency": self.discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
enable_discount_accounting = cint(
|
||||
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
|
||||
@@ -1107,23 +1084,25 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.depreciate_asset(asset)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = self.customer
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
@@ -1177,101 +1156,6 @@ class SalesInvoice(SellingController):
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (
|
||||
len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book
|
||||
):
|
||||
frappe.throw(
|
||||
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
|
||||
)
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=self.posting_date)
|
||||
|
||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
def reverse_depreciation_entry_made_after_sale(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||
if not self.sale_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, posting_date_of_original_invoice
|
||||
) or self.sale_happens_in_the_future(posting_date_of_original_invoice):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
asset.finance_books[0].value_after_depreciation += depreciation_amount
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def sale_was_made_on_original_schedule_date(
|
||||
self, asset, schedule, row, posting_date_of_original_invoice
|
||||
):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_original_invoice:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
|
||||
if posting_date_of_original_invoice > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_depreciation_amount_in_je(self, journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
@@ -2103,13 +1987,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
|
||||
source_document_warehouse_field = "target_warehouse"
|
||||
target_document_warehouse_field = "from_warehouse"
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
else:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
received_items = {}
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
@@ -2133,6 +2017,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
@@ -2177,6 +2064,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
|
||||
if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item":
|
||||
target.purchase_order = source.parent
|
||||
target.purchase_order_item = source.name
|
||||
|
||||
if (
|
||||
source.get("purchase_order")
|
||||
and source.get("purchase_order_item")
|
||||
and target.doctype == "Purchase Invoice Item"
|
||||
):
|
||||
target.purchase_order = source.purchase_order
|
||||
target.po_detail = source.purchase_order_item
|
||||
|
||||
item_field_map = {
|
||||
"doctype": target_doctype + " Item",
|
||||
@@ -2203,6 +2101,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"serial_no": "serial_no",
|
||||
}
|
||||
)
|
||||
elif target_doctype == "Sales Order":
|
||||
item_field_map["field_map"].update(
|
||||
{
|
||||
source_document_warehouse_field: "warehouse",
|
||||
}
|
||||
)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
@@ -2247,6 +2151,7 @@ def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
|
||||
def set_purchase_references(doc):
|
||||
# add internal PO or PR links if any
|
||||
|
||||
if doc.is_internal_transfer():
|
||||
if doc.doctype == "Purchase Receipt":
|
||||
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
|
||||
@@ -2276,15 +2181,6 @@ def set_purchase_references(doc):
|
||||
warehouse_map,
|
||||
)
|
||||
|
||||
if list(so_item_map.values()):
|
||||
pd_item_map, parent_child_map, warehouse_map = get_pd_details(
|
||||
"Purchase Order Item", so_item_map, "sales_order_item"
|
||||
)
|
||||
|
||||
update_pi_items(
|
||||
doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map
|
||||
)
|
||||
|
||||
|
||||
def update_pi_items(
|
||||
doc,
|
||||
@@ -2300,13 +2196,19 @@ def update_pi_items(
|
||||
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
|
||||
if doc.update_stock:
|
||||
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
|
||||
if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"):
|
||||
item.warehouse = frappe.db.get_value(
|
||||
"Purchase Order Item", item.purchase_order_item, "warehouse"
|
||||
)
|
||||
|
||||
|
||||
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
|
||||
for item in doc.get("items"):
|
||||
item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item))
|
||||
item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
|
||||
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
|
||||
if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"):
|
||||
item.warehouse = frappe.db.get_value(
|
||||
"Purchase Order Item", item.purchase_order_item, "warehouse"
|
||||
)
|
||||
|
||||
|
||||
def get_delivery_note_details(internal_reference):
|
||||
@@ -2535,7 +2437,6 @@ def create_dunning(source_name, target_doc=None):
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
amounts = calculate_interest_and_amount(
|
||||
target.posting_date,
|
||||
target.outstanding_amount,
|
||||
target.rate_of_interest,
|
||||
target.dunning_fee,
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -32,10 +32,20 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
get_qty_after_transaction,
|
||||
make_stock_entry,
|
||||
)
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class TestSalesInvoice(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||
|
||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||
create_internal_parties()
|
||||
setup_accounts()
|
||||
|
||||
def make(self):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
w.is_pos = 0
|
||||
@@ -955,7 +965,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -1000)
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -500)
|
||||
self.assertEqual(pos_return.get("payments")[1].amount, -500)
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile(
|
||||
@@ -1705,7 +1716,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.save()
|
||||
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
|
||||
|
||||
def test_outstanding_amount_after_advance_jv_cancelation(self):
|
||||
def test_outstanding_amount_after_advance_jv_cancellation(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
)
|
||||
@@ -1749,7 +1760,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")),
|
||||
)
|
||||
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancelation(self):
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
@@ -2367,29 +2378,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
acc_settings.save()
|
||||
|
||||
def test_inter_company_transaction(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer",
|
||||
represents_company="_Test Company 1",
|
||||
allowed_to_interact_with="Wind Power LLC",
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
|
||||
supplier = frappe.get_doc(
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"supplier_name": "_Test Internal Supplier",
|
||||
"doctype": "Supplier",
|
||||
"is_internal_supplier": 1,
|
||||
"represents_company": "Wind Power LLC",
|
||||
}
|
||||
)
|
||||
|
||||
supplier.append("companies", {"company": "_Test Company 1"})
|
||||
|
||||
supplier.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
@@ -2440,38 +2428,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"Expenses Included In Valuation - _TC1",
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Internal Customer"):
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
"customer_group": "_Test Customer Group",
|
||||
"customer_name": "_Test Internal Customer",
|
||||
"customer_type": "Individual",
|
||||
"doctype": "Customer",
|
||||
"territory": "_Test Territory",
|
||||
"is_internal_customer": 1,
|
||||
"represents_company": "_Test Company 1",
|
||||
}
|
||||
)
|
||||
|
||||
customer.append("companies", {"company": "Wind Power LLC"})
|
||||
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Supplier", "_Test Internal Supplier"):
|
||||
supplier = frappe.get_doc(
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"supplier_name": "_Test Internal Supplier",
|
||||
"doctype": "Supplier",
|
||||
"is_internal_supplier": 1,
|
||||
"represents_company": "Wind Power LLC",
|
||||
}
|
||||
)
|
||||
|
||||
supplier.append("companies", {"company": "_Test Company 1"})
|
||||
|
||||
supplier.insert()
|
||||
|
||||
# begin test
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
@@ -2541,34 +2497,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
se.cancel()
|
||||
|
||||
def test_internal_transfer_gl_entry(self):
|
||||
## Create internal transfer account
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
account = create_account(
|
||||
account_name="Unrealized Profit",
|
||||
parent_account="Current Liabilities - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
|
||||
)
|
||||
|
||||
customer = create_internal_customer(
|
||||
"_Test Internal Customer 2",
|
||||
"_Test Company with perpetual inventory",
|
||||
"_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
"_Test Internal Supplier 2",
|
||||
"_Test Company with perpetual inventory",
|
||||
"_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
customer=customer,
|
||||
customer="_Test Internal Customer 2",
|
||||
debit_to="Debtors - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
@@ -2582,7 +2513,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.update_stock = 1
|
||||
si.items[0].target_warehouse = "Work In Progress - TCP1"
|
||||
|
||||
# Add stock to stores for succesful stock transfer
|
||||
# Add stock to stores for successful stock transfer
|
||||
make_stock_entry(
|
||||
target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100
|
||||
)
|
||||
@@ -2638,6 +2569,77 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
|
||||
# Remove all existing stock for this
|
||||
if get_stock_balance("_Test Internal Transfer Item", "Stores - TCP1", "2022-04-10"):
|
||||
create_stock_reconciliation(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
warehouse="Stores - TCP1",
|
||||
qty=0,
|
||||
rate=0,
|
||||
company="_Test Company with perpetual inventory",
|
||||
expense_account="Stock Adjustment - TCP1"
|
||||
if frappe.get_all("Stock Ledger Entry")
|
||||
else "Temporary Opening - TCP1",
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
target="Stores - TCP1",
|
||||
qty=9000000,
|
||||
basic_rate=52.0,
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Internal Transfer Item",
|
||||
target="Stores - TCP1",
|
||||
qty=60000000,
|
||||
basic_rate=52.349777,
|
||||
posting_date="2020-04-10",
|
||||
posting_time="14:00",
|
||||
)
|
||||
|
||||
# Make an internal transfer Sales Invoice Stock in non stock uom to check
|
||||
# for rounding errors while converting to stock uom
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code="_Test Internal Transfer Item",
|
||||
qty=5000000,
|
||||
uom="Box",
|
||||
debit_to="Debtors - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
currency="INR",
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
# Check GL Entries with precision
|
||||
si.update_stock = 1
|
||||
si.items[0].target_warehouse = "Work In Progress - TCP1"
|
||||
si.items[0].conversion_factor = 10
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Check if adjustment entry is created
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"remarks": "Rounding gain/loss Entry for Stock Transfer",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
@@ -3077,7 +3079,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
[deferred_account, 2022.47, 0.0, "2019-03-15"],
|
||||
]
|
||||
|
||||
gl_entries = gl_entries = frappe.db.sql(
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
|
||||
@@ -3196,6 +3198,53 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_sales_invoice_return(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(qty=1, item_code=item.name)
|
||||
|
||||
batch_no = pr.items[0].batch_no
|
||||
si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no)
|
||||
|
||||
si.load_from_db()
|
||||
batch_no = si.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
|
||||
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.save().submit()
|
||||
|
||||
self.assertTrue(return_si.docstatus == 1)
|
||||
|
||||
def test_sales_invoice_with_payable_tax_account(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Creditors - _TC",
|
||||
"description": "Test",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 10,
|
||||
"total": 10,
|
||||
"dont_recompute_tax": 0,
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
@@ -3275,6 +3324,7 @@ def create_sales_invoice(**args):
|
||||
"item_name": args.item_name or "_Test Item",
|
||||
"description": args.description or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"target_warehouse": args.target_warehouse,
|
||||
"qty": args.qty or 1,
|
||||
"uom": args.uom or "Nos",
|
||||
"stock_uom": args.uom or "Nos",
|
||||
@@ -3287,8 +3337,9 @@ def create_sales_invoice(**args):
|
||||
"asset": args.asset or None,
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no,
|
||||
"conversion_factor": 1,
|
||||
"conversion_factor": args.get("conversion_factor", 1),
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"batch_no": args.batch_no or None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3399,6 +3450,34 @@ def get_taxes_and_charges():
|
||||
]
|
||||
|
||||
|
||||
def create_internal_parties():
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer",
|
||||
represents_company="_Test Company 1",
|
||||
allowed_to_interact_with="Wind Power LLC",
|
||||
)
|
||||
|
||||
create_internal_customer(
|
||||
customer_name="_Test Internal Customer 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier",
|
||||
represents_company="Wind Power LLC",
|
||||
allowed_to_interact_with="_Test Company 1",
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
supplier_name="_Test Internal Supplier 2",
|
||||
represents_company="_Test Company with perpetual inventory",
|
||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
|
||||
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.get_doc(
|
||||
@@ -3421,6 +3500,19 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
|
||||
return supplier_name
|
||||
|
||||
|
||||
def setup_accounts():
|
||||
## Create internal transfer account
|
||||
account = create_account(
|
||||
account_name="Unrealized Profit",
|
||||
parent_account="Current Liabilities - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account
|
||||
)
|
||||
|
||||
|
||||
def add_taxes(doc):
|
||||
doc.append(
|
||||
"taxes",
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
"delivery_note",
|
||||
"dn_detail",
|
||||
"delivered_qty",
|
||||
"internal_transfer_section",
|
||||
"purchase_order",
|
||||
"column_break_92",
|
||||
"purchase_order_item",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -243,6 +247,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
|
||||
"fieldname": "discount_and_margin",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
@@ -282,7 +287,6 @@
|
||||
"label": "Discount (%) on Price List Rate with Margin",
|
||||
"oldfieldname": "adj_rate",
|
||||
"oldfieldtype": "Float",
|
||||
"precision": "2",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -817,6 +821,7 @@
|
||||
"label": "Incoming Rate (Costing)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -841,12 +846,38 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:parent.is_internal_customer == 1",
|
||||
"fieldname": "internal_transfer_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Internal Transfer"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Purchase Order Item",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-17 05:33:15.335912",
|
||||
"modified": "2022-10-26 11:38:36.119339",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from dateutil import relativedelta
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, flt, get_first_day, get_last_day, getdate
|
||||
@@ -49,7 +50,7 @@ def get_plan_rate(
|
||||
start_date = getdate(start_date)
|
||||
end_date = getdate(end_date)
|
||||
|
||||
no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1
|
||||
no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1
|
||||
cost = plan.cost * no_of_months
|
||||
|
||||
# Adjust cost if start or end date is not month start or end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-09-13 16:18:59.404842",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_name",
|
||||
"taxable_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Voucher Name",
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxable_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Taxable Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 23:40:41.479208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withheld Vouchers",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxWithheldVouchers(Document):
|
||||
pass
|
||||
@@ -109,7 +109,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
).format(tax_withholding_category, inv.company, party)
|
||||
)
|
||||
|
||||
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
|
||||
tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount(
|
||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
return tax_row
|
||||
|
||||
@@ -217,7 +217,9 @@ def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -236,6 +238,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
@@ -246,6 +249,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
|
||||
# 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, tax_deducted, vouchers)
|
||||
|
||||
@@ -261,12 +267,13 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
dr_or_cr = "credit" if party_type == "Supplier" else "debit"
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
voucher_wise_amount = {}
|
||||
vouchers = []
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
@@ -281,29 +288,40 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"])
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name
|
||||
SELECT j.name, ja.credit - ja.debit AS amount
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
WHERE
|
||||
j.docstatus = 1
|
||||
j.name = ja.parent
|
||||
AND j.docstatus = 1
|
||||
AND j.is_opening = 'No'
|
||||
AND j.posting_date between %s and %s
|
||||
AND ja.{dr_or_cr} > 0
|
||||
AND ja.party in %s
|
||||
""".format(
|
||||
dr_or_cr=dr_or_cr
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
),
|
||||
(tax_details.from_date, tax_details.to_date, tuple(parties)),
|
||||
as_list=1,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if journal_entries:
|
||||
journal_entries = journal_entries[0]
|
||||
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"}})
|
||||
|
||||
return invoices + journal_entries
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_advance_vouchers(
|
||||
@@ -318,9 +336,11 @@ def get_advance_vouchers(
|
||||
"is_cancelled": 0,
|
||||
"party_type": party_type,
|
||||
"party": ["in", parties],
|
||||
"against_voucher": ["is", "not set"],
|
||||
}
|
||||
|
||||
if party_type == "Customer":
|
||||
filters.update({"against_voucher": ["is", "not set"]})
|
||||
|
||||
if company:
|
||||
filters["company"] = company
|
||||
if from_date and to_date:
|
||||
@@ -330,23 +350,25 @@ def get_advance_vouchers(
|
||||
|
||||
|
||||
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
advances = [d.reference_name for d in inv.get("advances")]
|
||||
tax_info = []
|
||||
|
||||
if advances:
|
||||
pe = frappe.qb.DocType("Payment Entry").as_("pe")
|
||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
||||
if inv.get("advances"):
|
||||
advances = [d.reference_name for d in inv.get("advances")]
|
||||
|
||||
tax_info = (
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if advances:
|
||||
pe = frappe.qb.DocType("Payment Entry").as_("pe")
|
||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
||||
|
||||
tax_info = (
|
||||
frappe.qb.from_(at)
|
||||
.inner_join(pe)
|
||||
.on(pe.name == at.parent)
|
||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||
.where(at.parent.isin(advances))
|
||||
.where(at.account_head == tax_details.account_head)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return tax_info
|
||||
|
||||
@@ -395,11 +417,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt += inv.net_total
|
||||
|
||||
debit_note_amount = get_debit_note_amount(
|
||||
parties, tax_details.from_date, tax_details.to_date, inv.company
|
||||
)
|
||||
supp_credit_amt -= debit_note_amount
|
||||
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
@@ -411,7 +428,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
):
|
||||
# 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(net_total)") or 0.0
|
||||
net_total = 0
|
||||
if vouchers:
|
||||
net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)")
|
||||
|
||||
net_total += inv.net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
@@ -516,22 +536,6 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
|
||||
return tds_amount
|
||||
|
||||
|
||||
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
|
||||
|
||||
filters = {
|
||||
"supplier": ["in", suppliers],
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ["between", (from_date, to_date)],
|
||||
}
|
||||
fields = ["abs(sum(net_total)) as net_total"]
|
||||
|
||||
if company:
|
||||
filters["company"] = company
|
||||
|
||||
return frappe.get_all("Purchase Invoice", filters, fields)[0].get("net_total") or 0.0
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
return current_amount * rate / 100
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
invoices.append(pi)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds(self):
|
||||
@@ -88,7 +88,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 1000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_category_checks(self):
|
||||
@@ -114,7 +114,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
# TDS should be applied only on 1000
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in invoices:
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
@@ -148,8 +148,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
self.assertEqual(tcs_charged, 500)
|
||||
invoices.append(si)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
@@ -182,8 +182,8 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 4000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
@@ -207,8 +207,50 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 250)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_category_voucher_display(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category"
|
||||
)
|
||||
invoices = []
|
||||
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True)
|
||||
pi1.apply_tds = 1
|
||||
pi1.is_return = 1
|
||||
pi1.items[0].qty = -1
|
||||
pi1.tax_withholding_category = "Test Multi Invoice Category"
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
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"
|
||||
pi2.save()
|
||||
pi2.submit()
|
||||
invoices.append(pi2)
|
||||
|
||||
pi2.load_from_db()
|
||||
|
||||
self.assertTrue(pi2.taxes[0].tax_amount, 1100)
|
||||
|
||||
self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name)
|
||||
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)
|
||||
|
||||
# cancel invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
|
||||
@@ -308,6 +350,7 @@ def create_records():
|
||||
"Test TDS Supplier3",
|
||||
"Test TDS Supplier4",
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
@@ -498,3 +541,22 @@ def create_tax_with_holding_category():
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Multi Invoice Category",
|
||||
"category_name": "Test Multi Invoice Category",
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 5000,
|
||||
"cumulative_threshold": 10000,
|
||||
}
|
||||
],
|
||||
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -128,6 +128,12 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
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 cost_center and cost_center_allocation.get(cost_center):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
@@ -489,7 +495,6 @@ def make_reverse_gl_entries(
|
||||
).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
create_payment_ledger_entry(
|
||||
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
|
||||
)
|
||||
|
||||
@@ -207,7 +207,7 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if company_address:
|
||||
party_details.update({"company_address": company_address})
|
||||
party_details.company_address = company_address
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
@@ -219,12 +219,31 @@ def set_address_details(
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if party_details.company_address:
|
||||
party_details["shipping_address"] = shipping_address or party_details["company_address"]
|
||||
party_details.shipping_address_display = get_address_display(party_details["shipping_address"])
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address", party_details.shipping_address)
|
||||
shipping_address=shipping_address,
|
||||
shipping_address_display=get_address_display(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||
)
|
||||
|
||||
if party_details.company_address:
|
||||
# billing address
|
||||
party_details.update(
|
||||
billing_address=party_details.company_address,
|
||||
billing_address_display=(
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||
)
|
||||
|
||||
# shipping address - if not already set
|
||||
if not party_details.shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=party_details.billing_address,
|
||||
shipping_address_display=party_details.billing_address_display,
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
|
||||
)
|
||||
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
|
||||
@@ -51,6 +51,8 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
} else {
|
||||
frappe.query_report.set_filter_value('tax_id', "");
|
||||
}
|
||||
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -178,6 +178,11 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_name",
|
||||
"label": __("Customer Name"),
|
||||
|
||||
@@ -119,6 +119,7 @@ class ReceivablePayableReport(object):
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
@@ -165,7 +166,7 @@ class ReceivablePayableReport(object):
|
||||
"range4",
|
||||
"range5",
|
||||
"future_amount",
|
||||
"remaining_balance"
|
||||
"remaining_balance",
|
||||
]
|
||||
|
||||
def get_voucher_balance(self, ple):
|
||||
@@ -178,6 +179,11 @@ class ReceivablePayableReport(object):
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
return row
|
||||
|
||||
def update_voucher_balance(self, ple):
|
||||
@@ -187,7 +193,11 @@ class ReceivablePayableReport(object):
|
||||
if not row:
|
||||
return
|
||||
|
||||
amount = ple.amount
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
|
||||
# update voucher
|
||||
@@ -685,9 +695,10 @@ class ReceivablePayableReport(object):
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
ple.account_currency,
|
||||
ple.amount,
|
||||
ple.amount_in_account_currency,
|
||||
ple.remarks,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
@@ -722,6 +733,7 @@ class ReceivablePayableReport(object):
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
party_type_field = scrub(self.party_type)
|
||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
||||
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
@@ -736,7 +748,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self, conditions):
|
||||
def get_cost_center_conditions(self):
|
||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||
cost_center_list = [
|
||||
center.name
|
||||
@@ -772,7 +784,7 @@ class ReceivablePayableReport(object):
|
||||
def add_customer_filters(
|
||||
self,
|
||||
):
|
||||
self.customter = qb.DocType("Customer")
|
||||
self.customer = qb.DocType("Customer")
|
||||
|
||||
if self.filters.get("customer_group"):
|
||||
self.get_hierarchical_filters("Customer Group", "customer_group")
|
||||
@@ -826,7 +838,7 @@ class ReceivablePayableReport(object):
|
||||
customer = self.customer
|
||||
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
|
||||
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
|
||||
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
|
||||
self.qb_selection_filter.append(ple.party.isin(customers))
|
||||
|
||||
def add_accounting_dimensions_filters(self):
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
@@ -965,6 +977,9 @@ class ReceivablePayableReport(object):
|
||||
options="Supplier Group",
|
||||
)
|
||||
|
||||
if self.filters.show_remarks:
|
||||
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
|
||||
|
||||
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
|
||||
if not fieldname:
|
||||
fieldname = scrub(label)
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAccountsReceivable(unittest.TestCase):
|
||||
def test_accounts_receivable(self):
|
||||
class TestAccountsReceivable(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"based_on_payment_terms": 1,
|
||||
@@ -66,6 +74,50 @@ class TestAccountsReceivable(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_payment_againt_po_in_receivable_report(self):
|
||||
"""
|
||||
Payments made against Purchase Order will show up as outstanding amount
|
||||
"""
|
||||
|
||||
so = make_sales_order(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
warehouse="Finished Goods - _TC2",
|
||||
currency="EUR",
|
||||
debit_to="Debtors - _TC2",
|
||||
income_account="Sales - _TC2",
|
||||
expense_account="Cost of Goods Sold - _TC2",
|
||||
cost_center="Main - _TC2",
|
||||
)
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name)
|
||||
pe = pe.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"based_on_payment_terms": 0,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [0, 1000, 0, -1000]
|
||||
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_sales_invoice():
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -22,8 +22,7 @@ def get_columns():
|
||||
{
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldname": "payment_document_type",
|
||||
"fieldtype": "Link",
|
||||
"options": "Doctype",
|
||||
"fieldtype": "Data",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
@@ -33,15 +32,15 @@ def get_columns():
|
||||
"options": "payment_document_type",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
{"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120},
|
||||
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 120},
|
||||
{
|
||||
"label": _("Against Account"),
|
||||
"fieldname": "against",
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 170,
|
||||
"width": 200,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
@@ -535,7 +535,11 @@ def get_accounts(root_type, companies):
|
||||
):
|
||||
if account.account_name not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account.account_name)
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
added_accounts.append(account_key)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -52,22 +52,22 @@
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}</td>
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}</td>
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{%= frappe.format(data[i].account, {fieldtype: "Link"}) || " " %}</b></td>
|
||||
<td style="text-align: right">
|
||||
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
{% } %}
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].balance, filters.presentation_currency) %}
|
||||
{%= format_currency(data[i].balance, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
|
||||
@@ -237,9 +237,9 @@ 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")
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
conditions.append("(posting_date <=%(to_date)s)")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d[key] = totals[d["account"]]
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
frappe.query_reports["Gross Profit"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
@@ -12,46 +12,58 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.defaults.get_user_default("year_start_date"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_date",
|
||||
"fieldname": "to_date",
|
||||
"label": __("To Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.defaults.get_user_default("year_end_date"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_invoice",
|
||||
"fieldname": "sales_invoice",
|
||||
"label": __("Sales Invoice"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname":"group_by",
|
||||
"fieldname": "group_by",
|
||||
"label": __("Group By"),
|
||||
"fieldtype": "Select",
|
||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
|
||||
"default": "Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
"label": __("Item Group"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_person",
|
||||
"label": __("Sales Person"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Person"
|
||||
},
|
||||
],
|
||||
"tree": true,
|
||||
"name_field": "parent",
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
|
||||
if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _, scrub
|
||||
from frappe.utils import cint, flt, formatdate
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -616,7 +617,7 @@ class GrossProfitGenerator(object):
|
||||
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
|
||||
|
||||
if previous_stock_value:
|
||||
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
else:
|
||||
@@ -676,6 +677,17 @@ class GrossProfitGenerator(object):
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
||||
|
||||
if self.filters.sales_person:
|
||||
conditions += """
|
||||
and exists(select 1
|
||||
from `tabSales Team` st
|
||||
where st.parent = `tabSales Invoice`.name
|
||||
and st.sales_person = %(sales_person)s)
|
||||
"""
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
@@ -723,6 +735,7 @@ class GrossProfitGenerator(object):
|
||||
from
|
||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
|
||||
{sales_team_table}
|
||||
{payment_term_table}
|
||||
where
|
||||
|
||||
@@ -97,6 +97,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
row.update({"rate": d.base_net_rate, "amount": d.base_net_amount})
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
@@ -105,10 +106,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
if item_tax.get("is_other_charges"):
|
||||
total_other_charges += flt(item_tax.get("tax_amount"))
|
||||
else:
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
|
||||
row.update(
|
||||
{"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency}
|
||||
{
|
||||
"total_tax": total_tax,
|
||||
"total_other_charges": total_other_charges,
|
||||
"total": d.base_net_amount + total_tax,
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
if filters.get("group_by"):
|
||||
@@ -477,7 +486,7 @@ def get_tax_accounts(
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, parent, description, item_wise_tax_detail,
|
||||
name, parent, description, item_wise_tax_detail, account_head,
|
||||
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
@@ -493,11 +502,22 @@ def get_tax_accounts(
|
||||
tuple([doctype] + list(invoice_item_row)),
|
||||
)
|
||||
|
||||
account_doctype = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(account_doctype)
|
||||
.select(account_doctype.name)
|
||||
.where((account_doctype.account_type == "Tax"))
|
||||
)
|
||||
|
||||
tax_accounts = query.run()
|
||||
|
||||
for (
|
||||
name,
|
||||
parent,
|
||||
description,
|
||||
item_wise_tax_detail,
|
||||
account_head,
|
||||
charge_type,
|
||||
add_deduct_tax,
|
||||
tax_amount,
|
||||
@@ -540,7 +560,11 @@ def get_tax_accounts(
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
{"tax_rate": tax_rate, "tax_amount": tax_value}
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"tax_amount": tax_value,
|
||||
"is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1,
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
@@ -583,6 +607,13 @@ def get_tax_accounts(
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total Other Charges"),
|
||||
"fieldname": "total_other_charges",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total"),
|
||||
"fieldname": "total",
|
||||
|
||||
0
erpnext/accounts/report/payment_ledger/__init__.py
Normal file
0
erpnext/accounts/report/payment_ledger/__init__.py
Normal file
59
erpnext/accounts/report/payment_ledger/payment_ledger.js
Normal file
59
erpnext/accounts/report/payment_ledger/payment_ledger.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"voucher_no",
|
||||
"label": __("Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname":"against_voucher_no",
|
||||
"label": __("Against Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
|
||||
]
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Payment Ledger"] = {
|
||||
"filters": get_filters()
|
||||
};
|
||||
32
erpnext/accounts/report/payment_ledger/payment_ledger.json
Normal file
32
erpnext/accounts/report/payment_ledger/payment_ledger.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2022-06-06 08:50:43.933708",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2022-06-06 08:50:43.933708",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Ledger Entry",
|
||||
"report_name": "Payment Ledger",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
222
erpnext/accounts/report/payment_ledger/payment_ledger.py
Normal file
222
erpnext/accounts/report/payment_ledger/payment_ledger.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
|
||||
class PaymentLedger(object):
|
||||
def __init__(self, filters=None):
|
||||
self.filters = filters
|
||||
self.columns, self.data = [], []
|
||||
self.voucher_dict = OrderedDict()
|
||||
self.voucher_amount = []
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
def init_voucher_dict(self):
|
||||
|
||||
if self.voucher_amount:
|
||||
s = set()
|
||||
# build a set of unique vouchers
|
||||
for ple in self.voucher_amount:
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
s.add(key)
|
||||
|
||||
# for each unique vouchers, initialize +/- list
|
||||
for key in s:
|
||||
self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list())
|
||||
|
||||
# for each ple, using against voucher and amount, assign it to +/- list
|
||||
# group by against voucher
|
||||
for ple in self.voucher_amount:
|
||||
against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
target = None
|
||||
if self.voucher_dict.get(against_key):
|
||||
if ple.amount > 0:
|
||||
target = self.voucher_dict.get(against_key).increase
|
||||
else:
|
||||
target = self.voucher_dict.get(against_key).decrease
|
||||
|
||||
# this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple)
|
||||
# need to somehow include the stray entries as well.
|
||||
if target is not None:
|
||||
entry = frappe._dict(
|
||||
company=ple.company,
|
||||
account=ple.account,
|
||||
party_type=ple.party_type,
|
||||
party=ple.party,
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
against_voucher_type=ple.against_voucher_type,
|
||||
against_voucher_no=ple.against_voucher_no,
|
||||
amount=ple.amount,
|
||||
currency=ple.account_currency,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
entry["amount_in_account_currency"] = ple.amount_in_account_currency
|
||||
|
||||
target.append(entry)
|
||||
|
||||
def build_data(self):
|
||||
self.data.clear()
|
||||
|
||||
for value in self.voucher_dict.values():
|
||||
voucher_data = []
|
||||
if value.increase != []:
|
||||
voucher_data.extend(value.increase)
|
||||
if value.decrease != []:
|
||||
voucher_data.extend(value.decrease)
|
||||
|
||||
if voucher_data:
|
||||
# balance row
|
||||
total = 0
|
||||
total_in_account_currency = 0
|
||||
|
||||
for x in voucher_data:
|
||||
total += x.amount
|
||||
if self.filters.include_account_currency:
|
||||
total_in_account_currency += x.amount_in_account_currency
|
||||
|
||||
entry = frappe._dict(
|
||||
against_voucher_no="Outstanding:",
|
||||
amount=total,
|
||||
currency=voucher_data[0].currency,
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
entry["amount_in_account_currency"] = total_in_account_currency
|
||||
|
||||
voucher_data.append(entry)
|
||||
|
||||
# empty row
|
||||
voucher_data.append(frappe._dict())
|
||||
self.data.extend(voucher_data)
|
||||
|
||||
def build_conditions(self):
|
||||
self.conditions = []
|
||||
|
||||
if self.filters.company:
|
||||
self.conditions.append(self.ple.company == self.filters.company)
|
||||
|
||||
if self.filters.account:
|
||||
self.conditions.append(self.ple.account.isin(self.filters.account))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
self.conditions.append(self.ple.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
self.conditions.append(self.ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if self.filters.voucher_no:
|
||||
self.conditions.append(self.ple.voucher_no == self.filters.voucher_no)
|
||||
|
||||
if self.filters.against_voucher_no:
|
||||
self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no)
|
||||
|
||||
def get_data(self):
|
||||
ple = self.ple
|
||||
|
||||
self.build_conditions()
|
||||
|
||||
# fetch data from table
|
||||
self.voucher_amount = (
|
||||
qb.from_(ple)
|
||||
.select(ple.star)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100")
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(label=_("Account"), fieldname="account", fieldtype="data", options=options, width="100")
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Party Type"), fieldname="party_type", fieldtype="data", options=options, width="100"
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(label=_("Party"), fieldname="party", fieldtype="data", options=options, width="100")
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher Type"),
|
||||
fieldname="voucher_type",
|
||||
fieldtype="data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100"
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Against Voucher Type"),
|
||||
fieldname="against_voucher_type",
|
||||
fieldtype="data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Against Voucher No"),
|
||||
fieldname="against_voucher_no",
|
||||
fieldtype="data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Amount"),
|
||||
fieldname="amount",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.include_account_currency:
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Amount in Account Currency"),
|
||||
fieldname="amount_in_account_currency",
|
||||
fieldtype="Currency",
|
||||
options="currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
|
||||
# initialize dictionary and group using against voucher
|
||||
self.init_voucher_dict()
|
||||
|
||||
# convert dictionary to list and add balance rows
|
||||
self.build_data()
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return PaymentLedger(filters).run()
|
||||
@@ -0,0 +1,65 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.payment_ledger.payment_ledger import execute
|
||||
|
||||
|
||||
class TestPaymentLedger(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
doctypes.append(qb.DocType("Payment Entry"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def create_company(self):
|
||||
name = "Test Payment Ledger"
|
||||
company = None
|
||||
if frappe.db.exists("Company", name):
|
||||
company = frappe.get_doc("Company", name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses" + " - " + company.abbr
|
||||
self.income_account = company.default_income_account
|
||||
self.expense_account = company.default_expense_account
|
||||
self.debit_to = company.default_receivable_account
|
||||
|
||||
def test_unpaid_invoice_outstanding(self):
|
||||
sinv = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to=self.debit_to,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
income_account=self.income_account,
|
||||
warehouse=self.warehouse,
|
||||
)
|
||||
pe = get_payment_entry(sinv.doctype, sinv.name).save().submit()
|
||||
|
||||
filters = frappe._dict({"company": self.company})
|
||||
columns, data = execute(filters=filters)
|
||||
outstanding = [x for x in data if x.get("against_voucher_no") == "Outstanding:"]
|
||||
self.assertEqual(outstanding[0].get("amount"), 0)
|
||||
@@ -370,7 +370,7 @@ def get_conditions(filters):
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||
conditions += get_sales_invoice_item_field_condition("brand")
|
||||
|
||||
@@ -14,9 +14,9 @@ def execute(filters=None):
|
||||
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
columns = get_columns(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
|
||||
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
|
||||
final_result = group_by_supplier_and_category(res)
|
||||
|
||||
return columns, final_result
|
||||
|
||||
@@ -26,7 +26,6 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
supplier_map = get_supplier_pan_map()
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
print(journal_entry_party_map)
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
|
||||
@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
query_filters = {
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"report_type": report_type,
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
where
|
||||
company=%(company)s
|
||||
{additional_conditions}
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
and account in (select name from `tabAccount` where report_type=%(report_type)s)
|
||||
and is_cancelled = 0
|
||||
group by account""".format(
|
||||
|
||||
@@ -104,12 +104,17 @@ def get_opening_balances(filters):
|
||||
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 < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""".format(
|
||||
account_filter=account_filter
|
||||
),
|
||||
{"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type},
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_fiscal_years(
|
||||
)
|
||||
)
|
||||
|
||||
query = query.orderby(FY.year_start_date, Order.desc)
|
||||
query = query.orderby(FY.year_start_date, order=Order.desc)
|
||||
fiscal_years = query.run(as_dict=True)
|
||||
|
||||
frappe.cache().hset("fiscal_years", company, fiscal_years)
|
||||
@@ -460,10 +460,6 @@ def reconcile_against_document(args): # nosemgrep
|
||||
|
||||
frappe.flags.ignore_party_validation = False
|
||||
|
||||
if entry.voucher_type in ("Payment Entry", "Journal Entry"):
|
||||
if hasattr(doc, "update_expense_claim"):
|
||||
doc.update_expense_claim()
|
||||
|
||||
|
||||
def check_if_advance_entry_modified(args):
|
||||
"""
|
||||
@@ -648,6 +644,16 @@ def unlink_ref_doc_from_payment_entries(ref_doc):
|
||||
(now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
|
||||
)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
|
||||
ple.against_voucher_no, ple.voucher_no
|
||||
).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
|
||||
(ple.against_voucher_type == ref_doc.doctype)
|
||||
& (ple.against_voucher_no == ref_doc.name)
|
||||
& (ple.delinked == 0)
|
||||
).run()
|
||||
|
||||
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
ref_doc.set("advances", [])
|
||||
|
||||
@@ -823,7 +829,13 @@ def get_held_invoices(party_type, party):
|
||||
|
||||
|
||||
def get_outstanding_invoices(
|
||||
party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
|
||||
party_type,
|
||||
party,
|
||||
account,
|
||||
common_filter=None,
|
||||
posting_date=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -850,6 +862,7 @@ def get_outstanding_invoices(
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_date,
|
||||
min_outstanding=min_outstanding,
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
@@ -1030,7 +1043,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m
|
||||
|
||||
frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip())
|
||||
|
||||
new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company)
|
||||
new_name = get_autoname_with_number(cost_center_number, cost_center_name, company)
|
||||
if docname != new_name:
|
||||
frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge)
|
||||
return new_name
|
||||
@@ -1053,16 +1066,14 @@ def validate_field_number(doctype_name, docname, number_value, company, field_na
|
||||
)
|
||||
|
||||
|
||||
def get_autoname_with_number(number_value, doc_title, name, company):
|
||||
def get_autoname_with_number(number_value, doc_title, company):
|
||||
"""append title with prefix as number and suffix as company's abbreviation separated by '-'"""
|
||||
if name:
|
||||
name_split = name.split("-")
|
||||
parts = [doc_title.strip(), name_split[len(name_split) - 1].strip()]
|
||||
else:
|
||||
abbr = frappe.get_cached_value("Company", company, ["abbr"], as_dict=True)
|
||||
parts = [doc_title.strip(), abbr.abbr]
|
||||
company_abbr = frappe.get_cached_value("Company", company, "abbr")
|
||||
parts = [doc_title.strip(), company_abbr]
|
||||
|
||||
if cstr(number_value).strip():
|
||||
parts.insert(0, cstr(number_value).strip())
|
||||
|
||||
return " - ".join(parts)
|
||||
|
||||
|
||||
@@ -1156,6 +1167,10 @@ def _delete_gl_entries(voucher_type, voucher_no):
|
||||
where voucher_type=%s and voucher_no=%s""",
|
||||
(voucher_type, voucher_no),
|
||||
)
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
qb.from_(ple).delete().where(
|
||||
(ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)
|
||||
).run()
|
||||
|
||||
|
||||
def sort_stock_vouchers_by_posting_date(
|
||||
@@ -1353,9 +1368,8 @@ def check_and_delete_linked_reports(report):
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
|
||||
):
|
||||
def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
ple_map = []
|
||||
if gl_entries:
|
||||
ple = None
|
||||
|
||||
@@ -1395,43 +1409,57 @@ def create_payment_ledger_entry(
|
||||
dr_or_cr *= -1
|
||||
dr_or_cr_account_currency *= -1
|
||||
|
||||
ple = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Ledger Entry",
|
||||
"posting_date": gle.posting_date,
|
||||
"company": gle.company,
|
||||
"account_type": account_type,
|
||||
"account": gle.account,
|
||||
"party_type": gle.party_type,
|
||||
"party": gle.party,
|
||||
"cost_center": gle.cost_center,
|
||||
"finance_book": gle.finance_book,
|
||||
"due_date": gle.due_date,
|
||||
"voucher_type": gle.voucher_type,
|
||||
"voucher_no": gle.voucher_no,
|
||||
"against_voucher_type": gle.against_voucher_type
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
"account_currency": gle.account_currency,
|
||||
"amount": dr_or_cr,
|
||||
"amount_in_account_currency": dr_or_cr_account_currency,
|
||||
"delinked": True if cancel else False,
|
||||
}
|
||||
ple = frappe._dict(
|
||||
doctype="Payment Ledger Entry",
|
||||
posting_date=gle.posting_date,
|
||||
company=gle.company,
|
||||
account_type=account_type,
|
||||
account=gle.account,
|
||||
party_type=gle.party_type,
|
||||
party=gle.party,
|
||||
cost_center=gle.cost_center,
|
||||
finance_book=gle.finance_book,
|
||||
due_date=gle.due_date,
|
||||
voucher_type=gle.voucher_type,
|
||||
voucher_no=gle.voucher_no,
|
||||
against_voucher_type=gle.against_voucher_type
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
account_currency=gle.account_currency,
|
||||
amount=dr_or_cr,
|
||||
amount_in_account_currency=dr_or_cr_account_currency,
|
||||
delinked=True if cancel else False,
|
||||
remarks=gle.remarks,
|
||||
)
|
||||
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
ple.set(dimension.fieldname, gle.get(dimension.fieldname))
|
||||
ple[dimension.fieldname] = gle.get(dimension.fieldname)
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.flags.adv_adj = adv_adj
|
||||
ple.flags.from_repost = from_repost
|
||||
ple.flags.update_outstanding = update_outstanding
|
||||
ple.submit()
|
||||
ple_map.append(ple)
|
||||
return ple_map
|
||||
|
||||
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
|
||||
):
|
||||
if gl_entries:
|
||||
ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel)
|
||||
|
||||
for entry in ple_map:
|
||||
|
||||
ple = frappe.get_doc(entry)
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.flags.adv_adj = adv_adj
|
||||
ple.flags.from_repost = from_repost
|
||||
ple.flags.update_outstanding = update_outstanding
|
||||
ple.submit()
|
||||
|
||||
|
||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||
@@ -1451,7 +1479,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
|
||||
# on cancellation outstanding can be an empty list
|
||||
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
|
||||
if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding:
|
||||
if (
|
||||
voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and party_type
|
||||
and party
|
||||
and voucher_outstanding
|
||||
):
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
|
||||
@@ -1501,6 +1534,7 @@ class QueryPaymentLedger(object):
|
||||
# query filters
|
||||
self.vouchers = []
|
||||
self.common_filter = []
|
||||
self.voucher_posting_date = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
|
||||
@@ -1571,6 +1605,7 @@ class QueryPaymentLedger(object):
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.where(Criterion.all(self.voucher_posting_date))
|
||||
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
@@ -1652,6 +1687,7 @@ class QueryPaymentLedger(object):
|
||||
self,
|
||||
vouchers=None,
|
||||
common_filter=None,
|
||||
posting_date=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
get_payments=False,
|
||||
@@ -1671,6 +1707,7 @@ class QueryPaymentLedger(object):
|
||||
self.reset()
|
||||
self.vouchers = vouchers
|
||||
self.common_filter = common_filter or []
|
||||
self.voucher_posting_date = posting_date or []
|
||||
self.min_outstanding = min_outstanding
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
|
||||
@@ -230,7 +230,7 @@ frappe.ui.form.on('Asset', {
|
||||
datasets: [{
|
||||
color: 'green',
|
||||
values: asset_values,
|
||||
formatted: asset_values.map(d => d.toFixed(2))
|
||||
formatted: asset_values.map(d => d?.toFixed(2))
|
||||
}]
|
||||
},
|
||||
type: 'line'
|
||||
@@ -239,8 +239,10 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
|
||||
item_code: function(frm) {
|
||||
if(frm.doc.item_code) {
|
||||
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
||||
frm.trigger('set_finance_book');
|
||||
} else {
|
||||
frm.set_value('finance_books', []);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -381,6 +383,11 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
calculate_depreciation: function(frm) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
}
|
||||
},
|
||||
|
||||
gross_purchase_amount: function(frm) {
|
||||
|
||||
@@ -388,7 +388,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",
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -79,12 +79,12 @@ class Asset(AccountsController):
|
||||
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
|
||||
)
|
||||
|
||||
def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
|
||||
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
self.make_depreciation_schedule(date_of_sale)
|
||||
self.set_accumulated_depreciation(date_of_sale, date_of_return)
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
@@ -223,7 +223,7 @@ class Asset(AccountsController):
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_sale):
|
||||
def make_depreciation_schedule(self, date_of_disposal):
|
||||
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
|
||||
"schedules"
|
||||
):
|
||||
@@ -235,9 +235,9 @@ class Asset(AccountsController):
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
for finance_book in self.get("finance_books"):
|
||||
self._make_depreciation_schedule(finance_book, start, date_of_sale)
|
||||
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
|
||||
|
||||
def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
|
||||
def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
value_after_depreciation = self._get_value_after_depreciation(finance_book)
|
||||
@@ -274,15 +274,15 @@ class Asset(AccountsController):
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, from_date, date_of_sale
|
||||
finance_book, depreciation_amount, from_date, date_of_disposal
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self._add_depreciation_row(
|
||||
date_of_sale,
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
finance_book.depreciation_method,
|
||||
finance_book.finance_book,
|
||||
@@ -828,7 +828,9 @@ class Asset(AccountsController):
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1})
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.name)
|
||||
|
||||
@@ -10,6 +10,9 @@ frappe.listview_settings['Asset'] = {
|
||||
} 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 === "Scrapped") {
|
||||
return [__("Scrapped"), "grey", "status,=,Scrapped"];
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate, today
|
||||
from frappe.utils import add_months, cint, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
|
||||
def post_depreciation_entries(date=None):
|
||||
def post_depreciation_entries(date=None, commit=True):
|
||||
# Return if automatic booking of asset depreciation is disabled
|
||||
if not cint(
|
||||
frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")
|
||||
@@ -22,7 +23,8 @@ def post_depreciation_entries(date=None):
|
||||
date = today()
|
||||
for asset in get_depreciable_assets(date):
|
||||
make_depreciation_entry(asset, date)
|
||||
frappe.db.commit()
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_depreciable_assets(date):
|
||||
@@ -190,11 +192,16 @@ def scrap_asset(asset_name):
|
||||
|
||||
if asset.docstatus != 1:
|
||||
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped"):
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(
|
||||
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
|
||||
)
|
||||
|
||||
date = today()
|
||||
|
||||
depreciate_asset(asset, date)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, "series_for_depreciation_entry"
|
||||
)
|
||||
@@ -202,7 +209,7 @@ def scrap_asset(asset_name):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = today()
|
||||
je.posting_date = date
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
@@ -213,7 +220,7 @@ def scrap_asset(asset_name):
|
||||
je.flags.ignore_permissions = True
|
||||
je.submit()
|
||||
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", today())
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
|
||||
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||
asset.set_status("Scrapped")
|
||||
|
||||
@@ -224,6 +231,9 @@ def scrap_asset(asset_name):
|
||||
def restore_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
|
||||
reset_depreciation_schedule(asset, asset.disposal_date)
|
||||
|
||||
je = asset.journal_entry_for_scrap
|
||||
|
||||
asset.db_set("disposal_date", None)
|
||||
@@ -234,7 +244,94 @@ def restore_asset(asset_name):
|
||||
asset.set_status()
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
def depreciate_asset(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_disposal=date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, date)
|
||||
|
||||
|
||||
def reset_depreciation_schedule(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=date)
|
||||
|
||||
modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == date:
|
||||
if not disposal_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, date
|
||||
) or disposal_happens_in_the_future(date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
asset.finance_books[0].value_after_depreciation += depreciation_amount
|
||||
asset.save()
|
||||
|
||||
|
||||
def get_depreciation_amount_in_je(journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_disposal:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
if posting_date_of_disposal > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
):
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -246,28 +343,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
) = get_asset_details(asset, finance_book)
|
||||
|
||||
gl_entries = [
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
]
|
||||
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
for entry in gl_entries:
|
||||
entry["voucher_type"] = voucher_type
|
||||
entry["voucher_no"] = voucher_no
|
||||
|
||||
return gl_entries
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
|
||||
def get_gl_entries_on_asset_disposal(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
):
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -279,23 +393,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
|
||||
) = get_asset_details(asset, finance_book)
|
||||
|
||||
gl_entries = [
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
},
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": accumulated_depr_account,
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
]
|
||||
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
for entry in gl_entries:
|
||||
entry["voucher_type"] = voucher_type
|
||||
entry["voucher_no"] = voucher_no
|
||||
|
||||
return gl_entries
|
||||
|
||||
@@ -332,15 +461,21 @@ def get_asset_details(asset, finance_book=None):
|
||||
)
|
||||
|
||||
|
||||
def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
|
||||
def get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
):
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append(
|
||||
{
|
||||
"account": disposal_account,
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
}
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": disposal_account,
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
"posting_date": getdate(),
|
||||
},
|
||||
item=asset,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -358,3 +493,30 @@ def get_disposal_account_and_cost_center(company):
|
||||
frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company))
|
||||
|
||||
return disposal_account, depreciation_cost_center
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
|
||||
if asset_doc.calculate_depreciation:
|
||||
asset_doc.prepare_depreciation_data(getdate(disposal_date))
|
||||
|
||||
finance_book_id = 1
|
||||
if finance_book:
|
||||
for fb in asset_doc.finance_books:
|
||||
if fb.finance_book == finance_book:
|
||||
finance_book_id = fb.idx
|
||||
break
|
||||
|
||||
asset_schedules = [
|
||||
sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id
|
||||
]
|
||||
accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount
|
||||
|
||||
return flt(
|
||||
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
)
|
||||
else:
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
|
||||
@@ -4,10 +4,23 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cstr,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
make_sales_invoice,
|
||||
split_asset,
|
||||
update_maintenance_status,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
post_depreciation_entries,
|
||||
restore_asset,
|
||||
@@ -178,28 +191,48 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(doc.items[0].is_fixed_asset, 1)
|
||||
|
||||
def test_scrap_asset(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-01-01",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date=add_months("2020-01-01", 4))
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0)
|
||||
|
||||
scrap_asset(asset.name)
|
||||
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
self.assertEquals(accumulated_depr_amount, 18000.00 + pro_rata_amount)
|
||||
|
||||
self.assertEqual(asset.status, "Scrapped")
|
||||
self.assertTrue(asset.journal_entry_for_scrap)
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
|
||||
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -216,7 +249,64 @@ class TestAsset(AssetSetup):
|
||||
self.assertFalse(asset.journal_entry_for_scrap)
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
this_month_depr_amount = 9000.0 if get_last_day(date) == date else 0
|
||||
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
|
||||
|
||||
def test_gle_made_by_asset_sale(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = nowdate()
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-06-06",
|
||||
@@ -224,6 +314,7 @@ class TestAsset(AssetSetup):
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=10,
|
||||
maintenance_required=1,
|
||||
depreciation_start_date="2020-12-31",
|
||||
submit=1,
|
||||
)
|
||||
@@ -239,24 +330,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
update_maintenance_status()
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
def test_asset_splitting(self):
|
||||
asset = create_asset(
|
||||
@@ -1376,6 +1452,7 @@ def create_asset(**args):
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||
"location": args.location or "Test Location",
|
||||
@@ -1425,6 +1502,16 @@ def create_asset_category():
|
||||
"depreciation_expense_account": "_Test Depreciations - _TC",
|
||||
},
|
||||
)
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
"company_name": "_Test Company with perpetual inventory",
|
||||
"fixed_asset_account": "_Test Fixed Asset - TCP1",
|
||||
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
|
||||
"depreciation_expense_account": "_Test Depreciations - TCP1",
|
||||
},
|
||||
)
|
||||
|
||||
asset_category.insert()
|
||||
|
||||
|
||||
@@ -1454,12 +1541,14 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
|
||||
return item
|
||||
|
||||
|
||||
def set_depreciation_settings_in_company():
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC"
|
||||
company.depreciation_expense_account = "_Test Depreciations - _TC"
|
||||
company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC"
|
||||
company.depreciation_cost_center = "_Test Cost Center - _TC"
|
||||
def set_depreciation_settings_in_company(company=None):
|
||||
if not company:
|
||||
company = "_Test Company"
|
||||
company = frappe.get_doc("Company", company)
|
||||
company.accumulated_depreciation_account = "_Test Accumulated Depreciations - " + company.abbr
|
||||
company.depreciation_expense_account = "_Test Depreciations - " + company.abbr
|
||||
company.disposal_account = "_Test Gain/Loss on Asset Disposal - " + company.abbr
|
||||
company.depreciation_cost_center = "Main - " + company.abbr
|
||||
company.save()
|
||||
|
||||
# Enable booking asset depreciation entry automatically
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.provide("erpnext.assets");
|
||||
|
||||
|
||||
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
|
||||
setup() {
|
||||
this.setup_posting_date_time_check();
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
erpnext.hide_company();
|
||||
this.show_general_ledger();
|
||||
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
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});
|
||||
}
|
||||
});
|
||||
|
||||
me.frm.set_query("target_asset", function() {
|
||||
var filters = {};
|
||||
|
||||
if (me.frm.doc.target_item_code) {
|
||||
filters['item_code'] = me.frm.doc.target_item_code;
|
||||
}
|
||||
|
||||
filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]];
|
||||
filters['docstatus'] = 1;
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
|
||||
me.frm.set_query("asset", "asset_items", function() {
|
||||
var filters = {
|
||||
'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
|
||||
'docstatus': 1
|
||||
};
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
filters['name'] = ['!=', me.frm.doc.target_asset];
|
||||
}
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
|
||||
me.frm.set_query("item_code", "stock_items", function() {
|
||||
return erpnext.queries.item({"is_stock_item": 1});
|
||||
});
|
||||
|
||||
me.frm.set_query("item_code", "service_items", function() {
|
||||
return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0});
|
||||
});
|
||||
|
||||
me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) {
|
||||
var item = locals[cdt][cdn];
|
||||
if (!item.item_code) {
|
||||
frappe.throw(__("Please enter Item Code to get Batch Number"));
|
||||
} else {
|
||||
var filters = {
|
||||
'item_code': item.item_code,
|
||||
'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(),
|
||||
'warehouse': item.warehouse
|
||||
};
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_batch_no",
|
||||
filters: filters
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
me.frm.set_query('expense_account', 'service_items', function() {
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]],
|
||||
"is_group": 0,
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
target_item_code() {
|
||||
return this.get_target_item_details();
|
||||
}
|
||||
|
||||
target_asset() {
|
||||
return this.get_target_asset_details();
|
||||
}
|
||||
|
||||
item_code(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
this.get_consumed_stock_item_details(row);
|
||||
} else if (cdt == "Asset Capitalization Service Item") {
|
||||
this.get_service_item_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
warehouse(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
this.get_warehouse_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
asset(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Asset Item") {
|
||||
this.get_consumed_asset_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
posting_date() {
|
||||
if (this.frm.doc.posting_date) {
|
||||
frappe.run_serially([
|
||||
() => this.get_all_item_warehouse_details(),
|
||||
() => this.get_all_asset_values()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
posting_time() {
|
||||
if (this.frm.doc.posting_time) {
|
||||
this.get_all_item_warehouse_details();
|
||||
}
|
||||
}
|
||||
|
||||
finance_book(doc, cdt, cdn) {
|
||||
if (cdt === "Asset Capitalization Asset Item") {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.get_consumed_asset_details(row);
|
||||
} else {
|
||||
this.get_all_asset_values();
|
||||
}
|
||||
}
|
||||
|
||||
stock_qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
target_qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
rate() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
company() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.company) {
|
||||
frappe.model.set_value(me.frm.doc.doctype, me.frm.doc.name, "cost_center", null);
|
||||
$.each(me.frm.doc.stock_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
$.each(me.frm.doc.asset_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
$.each(me.frm.doc.service_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype);
|
||||
}
|
||||
|
||||
stock_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items');
|
||||
}
|
||||
|
||||
asset_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items');
|
||||
}
|
||||
|
||||
serivce_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items');
|
||||
}
|
||||
|
||||
get_target_item_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
item_code: me.frm.doc.target_item_code,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_target_asset_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
asset: me.frm.doc.target_asset,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_stock_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
item_code: row.item_code,
|
||||
warehouse: row.warehouse,
|
||||
stock_qty: flt(row.stock_qty),
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
company: me.frm.doc.company,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_asset_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
asset: row.asset,
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
company: me.frm.doc.company,
|
||||
finance_book: row.finance_book || me.frm.doc.finance_book,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_service_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
item_code: row.item_code,
|
||||
qty: flt(row.qty),
|
||||
expense_account: row.expense_account,
|
||||
company: me.frm.doc.company,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_warehouse_details(item) {
|
||||
var me = this;
|
||||
if (item.item_code && item.warehouse) {
|
||||
me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
|
||||
child: item,
|
||||
args: {
|
||||
args: {
|
||||
'item_code': item.item_code,
|
||||
'warehouse': cstr(item.warehouse),
|
||||
'qty': flt(item.stock_qty),
|
||||
'serial_no': item.serial_no,
|
||||
'posting_date': me.frm.doc.posting_date,
|
||||
'posting_time': me.frm.doc.posting_time,
|
||||
'company': me.frm.doc.company,
|
||||
'voucher_type': me.frm.doc.doctype,
|
||||
'voucher_no': me.frm.doc.name,
|
||||
'allow_zero_valuation': 1
|
||||
}
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_all_item_warehouse_details() {
|
||||
var me = this;
|
||||
return me.frm.call({
|
||||
method: "set_warehouse_details",
|
||||
doc: me.frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_all_asset_values() {
|
||||
var me = this;
|
||||
return me.frm.call({
|
||||
method: "set_asset_values",
|
||||
doc: me.frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculate_totals() {
|
||||
var me = this;
|
||||
|
||||
me.frm.doc.stock_items_total = 0;
|
||||
me.frm.doc.asset_items_total = 0;
|
||||
me.frm.doc.service_items_total = 0;
|
||||
|
||||
$.each(me.frm.doc.stock_items || [], function (i, d) {
|
||||
d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d));
|
||||
me.frm.doc.stock_items_total += d.amount;
|
||||
});
|
||||
|
||||
$.each(me.frm.doc.asset_items || [], function (i, d) {
|
||||
d.asset_value = flt(flt(d.asset_value), precision('asset_value', d));
|
||||
me.frm.doc.asset_items_total += d.asset_value;
|
||||
});
|
||||
|
||||
$.each(me.frm.doc.service_items || [], function (i, d) {
|
||||
d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d));
|
||||
me.frm.doc.service_items_total += d.amount;
|
||||
});
|
||||
|
||||
me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total'));
|
||||
me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total'));
|
||||
me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total'));
|
||||
|
||||
me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
|
||||
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value'));
|
||||
|
||||
me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty'));
|
||||
me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
|
||||
: me.frm.doc.total_value;
|
||||
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
};
|
||||
|
||||
cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm});
|
||||
@@ -0,0 +1,381 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2021-09-04 13:38:04.217187",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_code",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"column_break_5",
|
||||
"company",
|
||||
"finance_book",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"amended_from",
|
||||
"section_break_16",
|
||||
"stock_items",
|
||||
"stock_items_total",
|
||||
"section_break_26",
|
||||
"asset_items",
|
||||
"asset_items_total",
|
||||
"service_expenses_section",
|
||||
"service_items",
|
||||
"service_items_total",
|
||||
"totals_section",
|
||||
"total_value",
|
||||
"column_break_36",
|
||||
"target_incoming_rate",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"target_fixed_asset_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
|
||||
"fetch_from": "target_item_code.item_name",
|
||||
"fieldname": "target_item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Target Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.is_fixed_asset",
|
||||
"fieldname": "target_is_fixed_asset",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Is Fixed Asset",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Asset Name",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "set_posting_time",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Posting Date and Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"options": "ACC-ASC-.YYYY.-",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Asset Capitalization",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Stock Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items",
|
||||
"fieldtype": "Table",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Batch No",
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "target_is_fixed_asset"
|
||||
},
|
||||
{
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
"fieldname": "target_stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.has_batch_no",
|
||||
"fieldname": "target_has_batch_no",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Has Batch No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.has_serial_no",
|
||||
"fieldname": "target_has_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Has Serial No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "target_has_serial_no",
|
||||
"fieldname": "target_serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Target Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Asset Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_items",
|
||||
"fieldtype": "Table",
|
||||
"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",
|
||||
"label": "Consumed Stock Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Asset Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Expenses"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Services",
|
||||
"options": "Asset Capitalization Service Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Service Expense Total Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "totals_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Target Incoming Rate",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_fixed_asset_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-12 15:09:40.771332",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Quality Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
# import erpnext
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_value_after_depreciation_on_disposal_date,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
|
||||
get_current_asset_value,
|
||||
)
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_default_cost_center,
|
||||
get_default_expense_account,
|
||||
get_item_warehouse,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
force_fields = [
|
||||
"target_item_name",
|
||||
"target_asset_name",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_serial_no",
|
||||
"target_has_batch_no",
|
||||
"target_stock_uom",
|
||||
"stock_uom",
|
||||
"target_fixed_asset_account",
|
||||
"fixed_asset_account",
|
||||
"valuation_rate",
|
||||
]
|
||||
|
||||
|
||||
class AssetCapitalization(StockController):
|
||||
def validate(self):
|
||||
self.validate_posting_time()
|
||||
self.set_missing_values(for_validate=True)
|
||||
self.validate_target_item()
|
||||
self.validate_target_asset()
|
||||
self.validate_consumed_stock_item()
|
||||
self.validate_consumed_asset_item()
|
||||
self.validate_service_item()
|
||||
self.set_warehouse_details()
|
||||
self.set_asset_values()
|
||||
self.calculate_totals()
|
||||
self.set_title()
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_source_mandatory()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
target_item_details = get_target_item_details(self.target_item_code, self.company)
|
||||
for k, v in target_item_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
# Remove asset if item not a fixed asset
|
||||
if not self.target_is_fixed_asset:
|
||||
self.target_asset = None
|
||||
|
||||
target_asset_details = get_target_asset_details(self.target_asset, self.company)
|
||||
for k, v in target_asset_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
for d in self.stock_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
consumed_stock_item_details = get_consumed_stock_item_details(args)
|
||||
for k, v in consumed_stock_item_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
for d in self.asset_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
args.finance_book = d.get("finance_book") or self.get("finance_book")
|
||||
consumed_asset_details = get_consumed_asset_details(args)
|
||||
for k, v in consumed_asset_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
for d in self.service_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
service_item_details = get_service_item_details(args)
|
||||
for k, v in service_item_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
def validate_target_item(self):
|
||||
target_item = frappe.get_cached_doc("Item", self.target_item_code)
|
||||
|
||||
if not target_item.is_fixed_asset and not target_item.is_stock_item:
|
||||
frappe.throw(
|
||||
_("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name)
|
||||
)
|
||||
|
||||
if self.entry_type == "Capitalization" and not target_item.is_fixed_asset:
|
||||
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
|
||||
elif self.entry_type == "Decapitalization" and not target_item.is_stock_item:
|
||||
frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name))
|
||||
|
||||
if target_item.is_fixed_asset:
|
||||
self.target_qty = 1
|
||||
if flt(self.target_qty) <= 0:
|
||||
frappe.throw(_("Target Qty must be a positive number"))
|
||||
|
||||
if not target_item.is_stock_item:
|
||||
self.target_warehouse = None
|
||||
if not target_item.is_fixed_asset:
|
||||
self.target_asset = None
|
||||
self.target_fixed_asset_account = None
|
||||
if not target_item.has_batch_no:
|
||||
self.target_batch_no = None
|
||||
if not target_item.has_serial_no:
|
||||
self.target_serial_no = ""
|
||||
|
||||
if target_item.is_stock_item and not self.target_warehouse:
|
||||
frappe.throw(_("Target Warehouse is mandatory for Decapitalization"))
|
||||
|
||||
self.validate_item(target_item)
|
||||
|
||||
def validate_target_asset(self):
|
||||
if self.target_asset:
|
||||
target_asset = self.get_asset_for_validation(self.target_asset)
|
||||
|
||||
if target_asset.item_code != self.target_item_code:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
|
||||
)
|
||||
|
||||
self.validate_asset(target_asset)
|
||||
|
||||
def validate_consumed_stock_item(self):
|
||||
for d in self.stock_items:
|
||||
if d.item_code:
|
||||
item = frappe.get_cached_doc("Item", d.item_code)
|
||||
|
||||
if not item.is_stock_item:
|
||||
frappe.throw(_("Row #{0}: Item {1} is not a stock item").format(d.idx, d.item_code))
|
||||
|
||||
if flt(d.stock_qty) <= 0:
|
||||
frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx))
|
||||
|
||||
self.validate_item(item)
|
||||
|
||||
def validate_consumed_asset_item(self):
|
||||
for d in self.asset_items:
|
||||
if d.asset:
|
||||
if d.asset == self.target_asset:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset").format(
|
||||
d.idx, d.asset
|
||||
)
|
||||
)
|
||||
|
||||
asset = self.get_asset_for_validation(d.asset)
|
||||
self.validate_asset(asset)
|
||||
|
||||
def validate_service_item(self):
|
||||
for d in self.service_items:
|
||||
if d.item_code:
|
||||
item = frappe.get_cached_doc("Item", d.item_code)
|
||||
|
||||
if item.is_stock_item or item.is_fixed_asset:
|
||||
frappe.throw(_("Row #{0}: Item {1} is not a service item").format(d.idx, d.item_code))
|
||||
|
||||
if flt(d.qty) <= 0:
|
||||
frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx))
|
||||
|
||||
if flt(d.rate) <= 0:
|
||||
frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx))
|
||||
|
||||
self.validate_item(item)
|
||||
|
||||
if not d.cost_center:
|
||||
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
def validate_source_mandatory(self):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not self.get("stock_items") and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization"))
|
||||
|
||||
def validate_item(self, item):
|
||||
from erpnext.stock.doctype.item.item import validate_end_of_life
|
||||
|
||||
validate_end_of_life(item.name, item.end_of_life, item.disabled)
|
||||
|
||||
def get_asset_for_validation(self, asset):
|
||||
return frappe.db.get_value(
|
||||
"Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status))
|
||||
|
||||
if asset.docstatus == 0:
|
||||
frappe.throw(_("Asset {0} is Draft").format(asset.name))
|
||||
if asset.docstatus == 2:
|
||||
frappe.throw(_("Asset {0} is cancelled").format(asset.name))
|
||||
|
||||
if asset.company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company))
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_warehouse_details(self):
|
||||
for d in self.get("stock_items"):
|
||||
if d.item_code and d.warehouse:
|
||||
args = self.get_args_for_incoming_rate(d)
|
||||
warehouse_details = get_warehouse_details(args)
|
||||
d.update(warehouse_details)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_asset_values(self):
|
||||
for d in self.get("asset_items"):
|
||||
if d.asset:
|
||||
finance_book = d.get("finance_book") or self.get("finance_book")
|
||||
d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book))
|
||||
d.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
d.asset, self.posting_date, finance_book=finance_book
|
||||
)
|
||||
|
||||
def get_args_for_incoming_rate(self, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": -1 * flt(item.stock_qty),
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")),
|
||||
}
|
||||
)
|
||||
|
||||
def calculate_totals(self):
|
||||
self.stock_items_total = 0
|
||||
self.asset_items_total = 0
|
||||
self.service_items_total = 0
|
||||
|
||||
for d in self.stock_items:
|
||||
d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision("amount"))
|
||||
self.stock_items_total += d.amount
|
||||
|
||||
for d in self.asset_items:
|
||||
d.asset_value = flt(flt(d.asset_value), d.precision("asset_value"))
|
||||
self.asset_items_total += d.asset_value
|
||||
|
||||
for d in self.service_items:
|
||||
d.amount = flt(flt(d.qty) * flt(d.rate), d.precision("amount"))
|
||||
self.service_items_total += d.amount
|
||||
|
||||
self.stock_items_total = flt(self.stock_items_total, self.precision("stock_items_total"))
|
||||
self.asset_items_total = flt(self.asset_items_total, self.precision("asset_items_total"))
|
||||
self.service_items_total = flt(self.service_items_total, self.precision("service_items_total"))
|
||||
|
||||
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
|
||||
self.total_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
self.target_qty = flt(self.target_qty, self.precision("target_qty"))
|
||||
self.target_incoming_rate = self.total_value / self.target_qty
|
||||
|
||||
def update_stock_ledger(self):
|
||||
sl_entries = []
|
||||
|
||||
for d in self.stock_items:
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -flt(d.stock_qty),
|
||||
},
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset:
|
||||
sle = self.get_sl_entries(
|
||||
self,
|
||||
{
|
||||
"item_code": self.target_item_code,
|
||||
"warehouse": self.target_warehouse,
|
||||
"batch_no": self.target_batch_no,
|
||||
"serial_no": self.target_serial_no,
|
||||
"actual_qty": flt(self.target_qty),
|
||||
"incoming_rate": flt(self.target_incoming_rate),
|
||||
},
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
# reverse sl entries if cancel
|
||||
if self.docstatus == 2:
|
||||
sl_entries.reverse()
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
elif self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def get_gl_entries(
|
||||
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
|
||||
):
|
||||
# Stock GL Entries
|
||||
gl_entries = []
|
||||
|
||||
self.warehouse_account = warehouse_account
|
||||
if not self.warehouse_account:
|
||||
self.warehouse_account = get_warehouse_account_map(self.company)
|
||||
|
||||
precision = self.get_debit_field_precision()
|
||||
self.sle_map = self.get_stock_ledger_details()
|
||||
|
||||
target_account = self.get_target_account()
|
||||
target_against = set()
|
||||
|
||||
self.get_gl_entries_for_consumed_stock_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
self.get_gl_entries_for_consumed_asset_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
self.get_gl_entries_for_consumed_service_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
return self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
|
||||
def get_gl_entries_for_consumed_stock_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Consumed Stock Items
|
||||
for item_row in self.stock_items:
|
||||
sle_list = self.sle_map.get(item_row.name)
|
||||
if sle_list:
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
account = self.warehouse_account[sle.warehouse]["account"]
|
||||
else:
|
||||
account = self.get_company_default("default_expense_account")
|
||||
|
||||
target_against.add(account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": -1 * stock_value_difference,
|
||||
},
|
||||
self.warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_consumed_asset_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Consumed Assets
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.asset_value,
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
)
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
def get_gl_entries_for_consumed_service_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Service Expenses
|
||||
for item_row in self.service_items:
|
||||
expense_amount = flt(item_row.amount, precision)
|
||||
target_against.add(item_row.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item_row.expense_account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": expense_amount,
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.target_fixed_asset_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Target Stock Item
|
||||
sle_list = self.sle_map.get(self.name)
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
account = self.warehouse_account[sle.warehouse]["account"]
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": ", ".join(target_against),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"debit": stock_value_difference,
|
||||
},
|
||||
self.warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
if self.docstatus == 1 and self.entry_type == "Capitalization":
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.prepare_depreciation_data()
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
asset_doc.save()
|
||||
elif self.docstatus == 2:
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
asset.db_set("disposal_date", None)
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
|
||||
def get_asset(self, item):
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
|
||||
def set_consumed_asset_status(self, asset):
|
||||
if self.docstatus == 1:
|
||||
asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
|
||||
else:
|
||||
asset.set_status()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_item_details(item_code=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Item Details
|
||||
item = frappe._dict()
|
||||
if item_code:
|
||||
item = frappe.get_cached_doc("Item", item_code)
|
||||
|
||||
# Set Item Details
|
||||
out.target_item_name = item.item_name
|
||||
out.target_stock_uom = item.stock_uom
|
||||
out.target_is_fixed_asset = cint(item.is_fixed_asset)
|
||||
out.target_has_batch_no = cint(item.has_batch_no)
|
||||
out.target_has_serial_no = cint(item.has_serial_no)
|
||||
|
||||
if out.target_is_fixed_asset:
|
||||
out.target_qty = 1
|
||||
out.target_warehouse = None
|
||||
else:
|
||||
out.target_asset = None
|
||||
|
||||
if not out.target_has_batch_no:
|
||||
out.target_batch_no = None
|
||||
if not out.target_has_serial_no:
|
||||
out.target_serial_no = ""
|
||||
|
||||
# Cost Center
|
||||
item_defaults = get_item_defaults(item.name, company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, company)
|
||||
brand_defaults = get_brand_defaults(item.name, company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
frappe._dict({"item_code": item.name, "company": company}),
|
||||
item_defaults,
|
||||
item_group_defaults,
|
||||
brand_defaults,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_asset_details(asset=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Asset Details
|
||||
asset_details = frappe._dict()
|
||||
if asset:
|
||||
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(asset))
|
||||
|
||||
# Re-set item code from Asset
|
||||
out.target_item_code = asset_details.item_code
|
||||
|
||||
# Set Asset Details
|
||||
out.asset_name = asset_details.asset_name
|
||||
|
||||
if asset_details.item_code:
|
||||
out.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=company
|
||||
)
|
||||
else:
|
||||
out.target_fixed_asset_account = None
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_stock_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
item = frappe._dict()
|
||||
if args.item_code:
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
|
||||
out.item_name = item.item_name
|
||||
out.batch_no = None
|
||||
out.serial_no = ""
|
||||
|
||||
out.stock_qty = flt(args.stock_qty) or 1
|
||||
out.stock_uom = item.stock_uom
|
||||
|
||||
out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None
|
||||
|
||||
# Cost Center
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
if args.item_code and out.warehouse:
|
||||
incoming_rate_args = frappe._dict(
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"warehouse": out.warehouse,
|
||||
"posting_date": args.posting_date,
|
||||
"posting_time": args.posting_time,
|
||||
"qty": -1 * flt(out.stock_qty),
|
||||
"voucher_type": args.doctype,
|
||||
"voucher_no": args.name,
|
||||
"company": args.company,
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no,
|
||||
}
|
||||
)
|
||||
out.update(get_warehouse_details(incoming_rate_args))
|
||||
else:
|
||||
out.valuation_rate = 0
|
||||
out.actual_qty = 0
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_warehouse_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
out = {}
|
||||
if args.warehouse and args.item_code:
|
||||
out = {
|
||||
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
|
||||
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_asset_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
asset_details = frappe._dict()
|
||||
if args.asset:
|
||||
asset_details = frappe.db.get_value(
|
||||
"Asset", args.asset, ["asset_name", "item_code", "item_name"], as_dict=1
|
||||
)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(args.asset))
|
||||
|
||||
out.item_code = asset_details.item_code
|
||||
out.asset_name = asset_details.asset_name
|
||||
out.item_name = asset_details.item_name
|
||||
|
||||
if args.asset:
|
||||
out.current_asset_value = flt(
|
||||
get_current_asset_value(args.asset, finance_book=args.finance_book)
|
||||
)
|
||||
out.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
args.asset, args.posting_date, finance_book=args.finance_book
|
||||
)
|
||||
else:
|
||||
out.current_asset_value = 0
|
||||
out.asset_value = 0
|
||||
|
||||
# Account
|
||||
if asset_details.item_code:
|
||||
out.fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=args.company
|
||||
)
|
||||
else:
|
||||
out.fixed_asset_account = None
|
||||
|
||||
# Cost Center
|
||||
if asset_details.item_code:
|
||||
item = frappe.get_cached_doc("Item", asset_details.item_code)
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_service_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
item = frappe._dict()
|
||||
if args.item_code:
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
|
||||
out.item_name = item.item_name
|
||||
out.qty = flt(args.qty) or 1
|
||||
out.uom = item.purchase_uom or item.stock_uom
|
||||
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
|
||||
out.expense_account = get_default_expense_account(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
return out
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user