mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-23 12:50:40 +00:00
Compare commits
582 Commits
v13.5.0
...
enterprise
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca433fd56 | ||
|
|
80d0f432a3 | ||
|
|
4c6398665b | ||
|
|
8880d13227 | ||
|
|
8cb560c753 | ||
|
|
b86213c890 | ||
|
|
6fffc90b46 | ||
|
|
64267faa8f | ||
|
|
6eded547f5 | ||
|
|
d4ae1febe3 | ||
|
|
50b4b70589 | ||
|
|
9c7a9f3a13 | ||
|
|
57cd273f7c | ||
|
|
020b8ae110 | ||
|
|
a6d276a06f | ||
|
|
f8343890b9 | ||
|
|
c6fe601efa | ||
|
|
bfd824c56e | ||
|
|
6ac68f3bc7 | ||
|
|
8c7d9efa9d | ||
|
|
8ed7a21cd5 | ||
|
|
1cd2f36b23 | ||
|
|
868a6cb26d | ||
|
|
441adf763f | ||
|
|
5a7fad8a6a | ||
|
|
82d0cfc68e | ||
|
|
940356d28a | ||
|
|
a07423ebb5 | ||
|
|
7a97b6d6a8 | ||
|
|
9d8efbe1db | ||
|
|
831ce88d1c | ||
|
|
af84ce9216 | ||
|
|
61d014d249 | ||
|
|
93dd238577 | ||
|
|
3fcc5e3134 | ||
|
|
19a0ca1980 | ||
|
|
356a55258e | ||
|
|
cba847b051 | ||
|
|
a661667e2a | ||
|
|
5fe7d80a64 | ||
|
|
c468e4a93d | ||
|
|
50b76d04bf | ||
|
|
ed68f11a46 | ||
|
|
f3ae7584c5 | ||
|
|
64454a5dc8 | ||
|
|
7aa802a073 | ||
|
|
60a0b7fe77 | ||
|
|
017ed3f5c1 | ||
|
|
9052a3b1a8 | ||
|
|
9ef157b23b | ||
|
|
45d506c489 | ||
|
|
338f436a1c | ||
|
|
7551bcf421 | ||
|
|
56c67743ab | ||
|
|
5b32fa5ccd | ||
|
|
13466bba90 | ||
|
|
f46f25ba53 | ||
|
|
21d918c194 | ||
|
|
bad69f6c39 | ||
|
|
71642c3172 | ||
|
|
bf91fe985e | ||
|
|
f229e62703 | ||
|
|
da72b9b840 | ||
|
|
94c492c085 | ||
|
|
bc187c4b84 | ||
|
|
8e108473c2 | ||
|
|
b965bcc6e8 | ||
|
|
8387613fdf | ||
|
|
c03f8c9e3f | ||
|
|
20691f0ba2 | ||
|
|
3c83691353 | ||
|
|
f1ebe4ef8b | ||
|
|
86c2cf5ab9 | ||
|
|
2725e61087 | ||
|
|
d1f1345a7f | ||
|
|
d82fd8ded1 | ||
|
|
def7183979 | ||
|
|
d2541caded | ||
|
|
fe515321f2 | ||
|
|
02e7de0e45 | ||
|
|
4a294bead2 | ||
|
|
f7c73a3069 | ||
|
|
e042ed69a1 | ||
|
|
8635d39865 | ||
|
|
beeaf0b04a | ||
|
|
96812893f1 | ||
|
|
a2595a0931 | ||
|
|
509d507d6e | ||
|
|
e33be287b9 | ||
|
|
31256698a9 | ||
|
|
78f7c92549 | ||
|
|
9e1de9c206 | ||
|
|
6fab603a23 | ||
|
|
fa4d314f7b | ||
|
|
55690c8165 | ||
|
|
cb65c6ce64 | ||
|
|
9871e124d0 | ||
|
|
08fddac884 | ||
|
|
d1b396637c | ||
|
|
27bbb7002d | ||
|
|
2d7f04dd1a | ||
|
|
49c5cd66f3 | ||
|
|
9ab18b5341 | ||
|
|
9fa92c912b | ||
|
|
72eb72f66f | ||
|
|
50b188214d | ||
|
|
f9da88cb15 | ||
|
|
6928fc17c6 | ||
|
|
f3158ea448 | ||
|
|
ac6bfeaa8d | ||
|
|
f60cba28ab | ||
|
|
57514f7b1d | ||
|
|
41705acbd9 | ||
|
|
2d225e621f | ||
|
|
c14aa45720 | ||
|
|
d99d56b0e8 | ||
|
|
cbe5718db9 | ||
|
|
85c8daae9c | ||
|
|
92273cade0 | ||
|
|
ce79faa738 | ||
|
|
d5ff636159 | ||
|
|
d8ed9dfcf4 | ||
|
|
4e6f49209f | ||
|
|
e079a1bb33 | ||
|
|
9ddf33715e | ||
|
|
e8abb201c1 | ||
|
|
3362c080b6 | ||
|
|
13e9aa5956 | ||
|
|
0c6ca09e06 | ||
|
|
f6d6897f34 | ||
|
|
a0df79ee8c | ||
|
|
627a8a8cfd | ||
|
|
b164070a4f | ||
|
|
958edc7b83 | ||
|
|
2ac0a1a1a0 | ||
|
|
9b9b18c286 | ||
|
|
26a9d38547 | ||
|
|
d319e10883 | ||
|
|
2f775ee53c | ||
|
|
a51e230f4a | ||
|
|
9997cce1ea | ||
|
|
4d444559ff | ||
|
|
74b97b5ec9 | ||
|
|
cc73e7d6fa | ||
|
|
1524e7f5ba | ||
|
|
12f7befa13 | ||
|
|
4aebb64165 | ||
|
|
f225196028 | ||
|
|
219623279f | ||
|
|
f40da4ac4c | ||
|
|
fd197a3aab | ||
|
|
a5f8accd63 | ||
|
|
683d18dd95 | ||
|
|
9ea5072c52 | ||
|
|
b24c0bfbf9 | ||
|
|
e244560fb9 | ||
|
|
7558e7f115 | ||
|
|
03f4db0606 | ||
|
|
2c67894135 | ||
|
|
513375f264 | ||
|
|
7a89033163 | ||
|
|
72715956f1 | ||
|
|
ac721ae147 | ||
|
|
9c04079d04 | ||
|
|
9168bb369a | ||
|
|
59bf2df28e | ||
|
|
cbf7e1b676 | ||
|
|
adfdc71844 | ||
|
|
96eb147bb6 | ||
|
|
7ff6865817 | ||
|
|
75f66f78d7 | ||
|
|
c00d851a88 | ||
|
|
740d5c6c53 | ||
|
|
01c89eaad9 | ||
|
|
2f350bf450 | ||
|
|
9513d61a50 | ||
|
|
51ae46f0de | ||
|
|
2f89b5b38e | ||
|
|
41c5357602 | ||
|
|
fc20ff02d0 | ||
|
|
bd8699b8a9 | ||
|
|
fea29ae8cb | ||
|
|
7be9f8dab1 | ||
|
|
0bad696faf | ||
|
|
07d9f3f74b | ||
|
|
4acbeecbbe | ||
|
|
eef8e4a9d4 | ||
|
|
0d190bb930 | ||
|
|
ec5f6c78d0 | ||
|
|
621927d9f9 | ||
|
|
c76b4430cf | ||
|
|
d9b6fe0d0f | ||
|
|
53fb5f778d | ||
|
|
fad426fd5c | ||
|
|
84f23ac8d2 | ||
|
|
4a2e4748ac | ||
|
|
74b3fc1e1c | ||
|
|
1e8f598ba5 | ||
|
|
dd0a8f20e2 | ||
|
|
fedee0e8da | ||
|
|
f07f7e9305 | ||
|
|
1cb2af00a8 | ||
|
|
9965af166e | ||
|
|
30f771b26c | ||
|
|
855e9030f2 | ||
|
|
7fb64d1645 | ||
|
|
1298956482 | ||
|
|
ca6e4bed9f | ||
|
|
1ef1daf383 | ||
|
|
b75b556bbb | ||
|
|
0003938f2b | ||
|
|
01aada6c90 | ||
|
|
45e6cffa4f | ||
|
|
38994bd494 | ||
|
|
ae41b53cee | ||
|
|
470c7e773f | ||
|
|
10473b1195 | ||
|
|
bf03671a33 | ||
|
|
f60c3f0655 | ||
|
|
432d8efa3d | ||
|
|
caacd0ad2c | ||
|
|
f7748e4109 | ||
|
|
e282effaed | ||
|
|
c8a825c478 | ||
|
|
77f2d2d01e | ||
|
|
24a25cd834 | ||
|
|
14a9ec0592 | ||
|
|
034ad28387 | ||
|
|
eae7c1891f | ||
|
|
c13ac4ab11 | ||
|
|
171ee51507 | ||
|
|
904b8481cf | ||
|
|
38fa3a3f89 | ||
|
|
13d7043451 | ||
|
|
fe4f58d0f6 | ||
|
|
8e8434a78a | ||
|
|
9ac63da457 | ||
|
|
d53991857c | ||
|
|
eb0dea09bb | ||
|
|
01be96adb2 | ||
|
|
bf462abb00 | ||
|
|
afafe406c8 | ||
|
|
8f3c7ab402 | ||
|
|
e5a1bb7141 | ||
|
|
3888488b36 | ||
|
|
257cbd3b92 | ||
|
|
a82e9e42e1 | ||
|
|
cf28950327 | ||
|
|
8f945a9852 | ||
|
|
00f90c50c0 | ||
|
|
ea2470d174 | ||
|
|
a01264dae7 | ||
|
|
f130f8385e | ||
|
|
2bdc0e8993 | ||
|
|
0bd190b885 | ||
|
|
11d56903e7 | ||
|
|
67c7067778 | ||
|
|
422325bb74 | ||
|
|
3636efd7e6 | ||
|
|
5e99aa7f65 | ||
|
|
7f794cc0ea | ||
|
|
c8eca8a448 | ||
|
|
f0b62f70d5 | ||
|
|
842674ce79 | ||
|
|
dfe11aca6a | ||
|
|
7b781b1498 | ||
|
|
57d06a86f8 | ||
|
|
4f51b56928 | ||
|
|
fa9e67502c | ||
|
|
9b6d9a41f4 | ||
|
|
15b336df28 | ||
|
|
c69bc54297 | ||
|
|
5638fbb1aa | ||
|
|
db682d9e4c | ||
|
|
75fdf79376 | ||
|
|
a0599e5ac2 | ||
|
|
22c2d4f118 | ||
|
|
ddf87594a1 | ||
|
|
ea5b7d38bb | ||
|
|
cd036e115b | ||
|
|
3105332e3c | ||
|
|
63096014a7 | ||
|
|
311e277204 | ||
|
|
73db919c99 | ||
|
|
4503a38361 | ||
|
|
b6076f772d | ||
|
|
c0817838d9 | ||
|
|
877597bc16 | ||
|
|
18533e381a | ||
|
|
ad6f20c5c7 | ||
|
|
5173e74a04 | ||
|
|
d4146a91d6 | ||
|
|
991d3cdd76 | ||
|
|
ba2c3c776f | ||
|
|
752f099e9d | ||
|
|
865900fd2d | ||
|
|
d8bc514226 | ||
|
|
c2b5562a3f | ||
|
|
eee03fcbab | ||
|
|
117cf4eca6 | ||
|
|
d7c4818a5d | ||
|
|
9a4926bb0d | ||
|
|
f99f872946 | ||
|
|
87b4e6ea32 | ||
|
|
c14fc9530c | ||
|
|
a856624ccb | ||
|
|
0bfd56e615 | ||
|
|
68c697b354 | ||
|
|
cf4e29a604 | ||
|
|
4df54cc62c | ||
|
|
8492bf040d | ||
|
|
d482c41eeb | ||
|
|
815e6ec846 | ||
|
|
2a1d1e1cb1 | ||
|
|
713c92e6fd | ||
|
|
46b67b901b | ||
|
|
a287b22b55 | ||
|
|
61690775a8 | ||
|
|
9181dde86a | ||
|
|
1f10a99910 | ||
|
|
91dcc07e26 | ||
|
|
09b081ccad | ||
|
|
2d1c4fee1d | ||
|
|
9bcc402f41 | ||
|
|
7d7d797ffc | ||
|
|
ebc46c1e09 | ||
|
|
cf445eb7b4 | ||
|
|
1e5482cedd | ||
|
|
5abf905ff7 | ||
|
|
478360397d | ||
|
|
5d5dc56f94 | ||
|
|
c1853ddf73 | ||
|
|
170f2ad056 | ||
|
|
9f051a24db | ||
|
|
e5c47f8957 | ||
|
|
6588a936d5 | ||
|
|
9af3f12411 | ||
|
|
b4e7ee0e45 | ||
|
|
98c9b0e9ed | ||
|
|
5474c09c70 | ||
|
|
9e43445f36 | ||
|
|
6ec8875434 | ||
|
|
ff4aadc657 | ||
|
|
bb0b1e61d6 | ||
|
|
6eb8d19cc9 | ||
|
|
cd36ba7e64 | ||
|
|
2f0298ee11 | ||
|
|
e955a4ee72 | ||
|
|
2ca9b765ef | ||
|
|
532a224c44 | ||
|
|
58daf5f916 | ||
|
|
43f85a2877 | ||
|
|
8958f55890 | ||
|
|
21e44b19f0 | ||
|
|
67768faaef | ||
|
|
b4b76b75cd | ||
|
|
970d2af7ad | ||
|
|
1564d39bf4 | ||
|
|
e3ca177828 | ||
|
|
bdba29bab5 | ||
|
|
5708b7140b | ||
|
|
b3a0a7b432 | ||
|
|
1658107a92 | ||
|
|
e21e435a0d | ||
|
|
a57976660a | ||
|
|
9382b1f154 | ||
|
|
11591b3e3e | ||
|
|
27797ffa46 | ||
|
|
2330c41cca | ||
|
|
57307443f0 | ||
|
|
c878389050 | ||
|
|
6a9798f305 | ||
|
|
fcab53b238 | ||
|
|
6e81489095 | ||
|
|
f6dce4df73 | ||
|
|
1ca8f6a51d | ||
|
|
81d164134d | ||
|
|
9dc625c1c9 | ||
|
|
79daed00c1 | ||
|
|
755ebdf582 | ||
|
|
da82bd4b51 | ||
|
|
dbdf2515cd | ||
|
|
4704003c94 | ||
|
|
b5d1a7731c | ||
|
|
c1954ec72a | ||
|
|
7a8a449ed4 | ||
|
|
3b12601461 | ||
|
|
e39c3f2e69 | ||
|
|
ea70f6f933 | ||
|
|
54cc1dedf2 | ||
|
|
bf3e191570 | ||
|
|
b5ffaff6a8 | ||
|
|
f79a72dbf3 | ||
|
|
7fd44907ba | ||
|
|
98e98d25e6 | ||
|
|
ca2e914715 | ||
|
|
820a579051 | ||
|
|
4f0e6cd911 | ||
|
|
f913838373 | ||
|
|
9f305e983c | ||
|
|
ea2408744a | ||
|
|
5884f1aeb0 | ||
|
|
26bec9d7b4 | ||
|
|
ce99701150 | ||
|
|
a4f5dcaa9a | ||
|
|
6b15a05de4 | ||
|
|
437e972108 | ||
|
|
9d8e8f8bdf | ||
|
|
bd9317956b | ||
|
|
bbe64b5604 | ||
|
|
a9b5dc6030 | ||
|
|
f5fa1ee4b6 | ||
|
|
5e49bee755 | ||
|
|
03b629c851 | ||
|
|
4cf0b02ddc | ||
|
|
3007c9900b | ||
|
|
7e006496dd | ||
|
|
c9c1d10435 | ||
|
|
1f7b95f390 | ||
|
|
078826d510 | ||
|
|
f913e0dd05 | ||
|
|
b4863bfa50 | ||
|
|
1b9b72d12e | ||
|
|
0bfffddac4 | ||
|
|
d802d73973 | ||
|
|
9ec0ded28f | ||
|
|
35e11fbea6 | ||
|
|
208b5f9e73 | ||
|
|
40a4330ec1 | ||
|
|
44815393b3 | ||
|
|
673470705b | ||
|
|
fc98abece9 | ||
|
|
ea2c02738d | ||
|
|
c05496a5a7 | ||
|
|
219a87d530 | ||
|
|
889140fd8c | ||
|
|
cfdda21dd2 | ||
|
|
703c30f5f8 | ||
|
|
9f15ebe711 | ||
|
|
f97206b3cb | ||
|
|
ea0dea46e0 | ||
|
|
49ec0e5ac3 | ||
|
|
d6eacf556d | ||
|
|
8347eb1dba | ||
|
|
773aabae44 | ||
|
|
3956f82e7c | ||
|
|
6495a4d2ed | ||
|
|
94484d7766 | ||
|
|
9cab26b7bf | ||
|
|
e78364c191 | ||
|
|
4b32ccb124 | ||
|
|
fb89008a13 | ||
|
|
99531a35e0 | ||
|
|
582f187726 | ||
|
|
ab3cd7e283 | ||
|
|
29a1a8d2d6 | ||
|
|
3d7f660bec | ||
|
|
f5db407461 | ||
|
|
5cc3f14506 | ||
|
|
ddb0ec261a | ||
|
|
2fb5291785 | ||
|
|
9a2db0b5b1 | ||
|
|
a94b89727c | ||
|
|
a2bb1b8acd | ||
|
|
8c844e4515 | ||
|
|
8520edc952 | ||
|
|
be881c743e | ||
|
|
81c97c13ce | ||
|
|
ef6e005c04 | ||
|
|
3d8f82459b | ||
|
|
b066fe9519 | ||
|
|
a58b571ccb | ||
|
|
3bb9f4a742 | ||
|
|
ef97269386 | ||
|
|
59e2e4989b | ||
|
|
ddef85ae97 | ||
|
|
f9390f596d | ||
|
|
d3ccb024f8 | ||
|
|
510077b3d4 | ||
|
|
b8442816ca | ||
|
|
913e92c29f | ||
|
|
5c19a9251f | ||
|
|
6f9de8c86f | ||
|
|
41b7c1aec0 | ||
|
|
3b1b4684ba | ||
|
|
8c73f6f19e | ||
|
|
60ce00531d | ||
|
|
86f689e54a | ||
|
|
b1c72da7d7 | ||
|
|
1fd80992d7 | ||
|
|
d70c6996f2 | ||
|
|
53a9ac4466 | ||
|
|
79dc0f0afc | ||
|
|
fb9cb78980 | ||
|
|
b5a1491176 | ||
|
|
5149101045 | ||
|
|
4cd0f6ce23 | ||
|
|
cf1fc40b06 | ||
|
|
75b30efb05 | ||
|
|
27ec51f021 | ||
|
|
18833b5ea1 | ||
|
|
3b070d1b05 | ||
|
|
9eac4d0af6 | ||
|
|
8718013c96 | ||
|
|
0511ffcf30 | ||
|
|
ad58182395 | ||
|
|
856484aeb0 | ||
|
|
8230569f23 | ||
|
|
098a833424 | ||
|
|
580346360f | ||
|
|
28cdff10cf | ||
|
|
cf349aadf9 | ||
|
|
17550fb4bf | ||
|
|
400205cc8a | ||
|
|
d7d5c4c329 | ||
|
|
c4d851e45f | ||
|
|
a9c84f749a | ||
|
|
5bb89b0ead | ||
|
|
ec4a3723cc | ||
|
|
687ad9b942 | ||
|
|
88176e65e4 | ||
|
|
dad6a596eb | ||
|
|
bcbb1c68b4 | ||
|
|
1e898c0f5b | ||
|
|
88c0cd6f7a | ||
|
|
2924ff830e | ||
|
|
bc490e246e | ||
|
|
60de73b904 | ||
|
|
af13348817 | ||
|
|
16b309e66e | ||
|
|
12fb02ed2a | ||
|
|
223efec26e | ||
|
|
10e8316240 | ||
|
|
8106d70e70 | ||
|
|
c368c5b20b | ||
|
|
b45d90153c | ||
|
|
701cba7f03 | ||
|
|
edecd5b0c6 | ||
|
|
6f79c4c348 | ||
|
|
23b907df1a | ||
|
|
672c8bb112 | ||
|
|
48036b4bab | ||
|
|
7c7c084159 | ||
|
|
ca34eec2d6 | ||
|
|
b3ed807b70 | ||
|
|
c6dcc9d94a | ||
|
|
6432aaa07a | ||
|
|
48b1a82fa1 | ||
|
|
4311fdc3a1 | ||
|
|
bb22972708 | ||
|
|
6ac69672f2 | ||
|
|
34997ae7c5 | ||
|
|
3e29361549 | ||
|
|
39a7a4f0ea | ||
|
|
f9656ed790 | ||
|
|
c5a43a59e0 | ||
|
|
f5f883b6a0 | ||
|
|
390bafef08 | ||
|
|
342d0d1bbb | ||
|
|
e6be20a94d | ||
|
|
25b3507809 | ||
|
|
0a9a4d5f88 | ||
|
|
d66559b6e4 | ||
|
|
e16a632edb | ||
|
|
ccd0237ae8 | ||
|
|
e93ed7d324 | ||
|
|
ec2d4c41d6 | ||
|
|
15a89f83c3 | ||
|
|
de03bc2cb3 | ||
|
|
4367f1c4b6 | ||
|
|
0ae702aab9 | ||
|
|
1bac72b74d | ||
|
|
72e602ae54 | ||
|
|
f6610c96dd | ||
|
|
a66184fe3c | ||
|
|
204ea1027f | ||
|
|
66a71bdd1a | ||
|
|
50997709d5 | ||
|
|
2acc66db2c | ||
|
|
3130ed52ff | ||
|
|
e379f083bb | ||
|
|
f68205468c |
@@ -98,8 +98,6 @@ rules:
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
|
||||
15
.github/helper/semgrep_rules/security.yml
vendored
15
.github/helper/semgrep_rules/security.yml
vendored
@@ -8,18 +8,3 @@ rules:
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-sqli-format-strings
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
@frappe.whitelist()
|
||||
def $FUNC(...):
|
||||
...
|
||||
- pattern-either:
|
||||
- pattern: frappe.db.sql("..." % ...)
|
||||
- pattern: frappe.db.sql(f"...", ...)
|
||||
- pattern: frappe.db.sql("...".format(...), ...)
|
||||
message: |
|
||||
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
|
||||
36
.github/workflows/semgrep.yml
vendored
36
.github/workflows/semgrep.yml
vendored
@@ -1,34 +1,18 @@
|
||||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
|
||||
- name: Semgrep errors
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
|
||||
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||
|
||||
- name: Semgrep warnings
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||
- uses: actions/checkout@v2
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
.github/helper/semgrep_rules
|
||||
|
||||
43
CODEOWNERS
43
CODEOWNERS
@@ -3,16 +3,33 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
manufacturing/ @rohitwaghchaure @marination
|
||||
accounts/ @deepeshgarg007 @nextchamp-saqib
|
||||
loan_management/ @deepeshgarg007 @rohitwaghchaure
|
||||
pos* @nextchamp-saqib @rohitwaghchaure
|
||||
assets/ @nextchamp-saqib @deepeshgarg007
|
||||
stock/ @marination @rohitwaghchaure
|
||||
buying/ @marination @deepeshgarg007
|
||||
hr/ @Anurag810 @rohitwaghchaure
|
||||
projects/ @hrwX @nextchamp-saqib
|
||||
support/ @hrwX @marination
|
||||
healthcare/ @ruchamahabal @marination
|
||||
erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
|
||||
requirements.txt @gavindsouza
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/erpnext_integrations/ @nextchamp-saqib
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/portal/ @marination
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure
|
||||
erpnext/shopping_cart/ @marination
|
||||
erpnext/stock/ @marination @rohitwaghchaure @ankush
|
||||
|
||||
erpnext/crm/ @ruchamahabal
|
||||
erpnext/education/ @ruchamahabal
|
||||
erpnext/healthcare/ @ruchamahabal
|
||||
erpnext/hr/ @ruchamahabal
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
erpnext/payroll @ruchamahabal
|
||||
erpnext/projects/ @ruchamahabal
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
|
||||
.github/ @surajshetty3416 @ankush
|
||||
requirements.txt @gavindsouza
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.5.0'
|
||||
__version__ = '13.8.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
filters.append(["Address", "name", "=", address])
|
||||
if not address:
|
||||
filters.append(["Address", "is_shipping_address", "=", 1])
|
||||
|
||||
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
|
||||
total_days, total_booking_days, account_currency)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||
@@ -298,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
|
||||
start_date = add_months(today(), -1)
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
companies = frappe.get_all('Company')
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
for company in companies:
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
|
||||
|
||||
@@ -19,7 +19,7 @@ class AccountingDimension(Document):
|
||||
|
||||
def validate(self):
|
||||
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
|
||||
'Cost Center', 'Accounting Dimension Detail', 'Company') :
|
||||
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
|
||||
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
@@ -257,9 +258,17 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Post Ledger Entries for Given Change"
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
|
||||
"fieldname": "enable_discount_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Discount Accounting"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -267,7 +276,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-25 12:34:05.858669",
|
||||
"modified": "2021-07-12 18:54:29.084958",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -21,6 +21,7 @@ class AccountsSettings(Document):
|
||||
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.toggle_discount_accounting_fields()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@@ -33,3 +34,22 @@ class AccountsSettings(Document):
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
|
||||
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
|
||||
|
||||
def toggle_discount_accounting_fields(self):
|
||||
enable_discount_accounting = cint(self.enable_discount_accounting)
|
||||
|
||||
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
|
||||
make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
if enable_discount_accounting:
|
||||
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
|
||||
else:
|
||||
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
if enable_discount_accounting:
|
||||
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
|
||||
else:
|
||||
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
|
||||
|
||||
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
|
||||
@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
|
||||
if 'Bank Account' not in json.dumps(preview):
|
||||
if 'Bank Account' not in json.dumps(preview['columns']):
|
||||
frappe.throw(_("Please add the Bank Account column"))
|
||||
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
|
||||
@@ -54,7 +54,7 @@ class CForm(Document):
|
||||
frappe.throw(_("Please enter atleast 1 invoice in the table"))
|
||||
|
||||
def set_total_invoiced_amount(self):
|
||||
total = sum([flt(d.grand_total) for d in self.get('invoices')])
|
||||
total = sum(flt(d.grand_total) for d in self.get('invoices'))
|
||||
frappe.db.set(self, 'total_invoiced_amount', total)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
||||
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
|
||||
|
||||
class ChartofAccountsImporter(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
validate_accounts(self.import_file)
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_company(company):
|
||||
@@ -301,28 +302,27 @@ def validate_accounts(file_name):
|
||||
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
|
||||
accounts_dict[account["parent_account"]]["is_group"] = 1
|
||||
|
||||
message = validate_root(accounts_dict)
|
||||
if message: return message
|
||||
message = validate_account_types(accounts_dict)
|
||||
if message: return message
|
||||
validate_root(accounts_dict)
|
||||
|
||||
validate_account_types(accounts_dict)
|
||||
|
||||
return [True, len(accounts)]
|
||||
|
||||
def validate_root(accounts):
|
||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||
if len(roots) < 4:
|
||||
return _("Number of root accounts cannot be less than 4")
|
||||
frappe.throw(_("Number of root accounts cannot be less than 4"))
|
||||
|
||||
error_messages = []
|
||||
|
||||
for account in roots:
|
||||
if not account.get("root_type") and account.get("account_name"):
|
||||
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
|
||||
error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
|
||||
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
||||
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
|
||||
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
||||
|
||||
if error_messages:
|
||||
return "<br>".join(error_messages)
|
||||
frappe.throw("<br>".join(error_messages))
|
||||
|
||||
def get_root_types():
|
||||
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
|
||||
@@ -356,7 +356,7 @@ def validate_account_types(accounts):
|
||||
|
||||
missing = list(set(account_types_for_ledger) - set(account_types))
|
||||
if missing:
|
||||
return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
|
||||
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
|
||||
|
||||
account_types_for_group = ["Bank", "Cash", "Stock"]
|
||||
# fix logic bug
|
||||
@@ -364,7 +364,7 @@ def validate_account_types(accounts):
|
||||
|
||||
missing = list(set(account_types_for_group) - set(account_groups))
|
||||
if missing:
|
||||
return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))
|
||||
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
|
||||
|
||||
def unset_existing_data(company):
|
||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||
@@ -391,5 +391,5 @@ def set_default_accounts(company):
|
||||
})
|
||||
|
||||
company.save()
|
||||
install_country_fixtures(company.name)
|
||||
install_country_fixtures(company.name, company.country)
|
||||
company.create_default_tax_template()
|
||||
|
||||
@@ -14,7 +14,7 @@ class CouponCode(Document):
|
||||
|
||||
if not self.coupon_code:
|
||||
if self.coupon_type == "Promotional":
|
||||
self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper()
|
||||
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
|
||||
elif self.coupon_type == "Gift Card":
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class Dunning(AccountsController):
|
||||
|
||||
def validate_amount(self):
|
||||
amounts = calculate_interest_and_amount(
|
||||
self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
|
||||
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
|
||||
if self.interest_amount != amounts.get('interest_amount'):
|
||||
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
|
||||
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||
@@ -86,18 +86,18 @@ def resolve_dunning(doc, state):
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list('Dunning', filters={
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||
|
||||
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||
interest_amount = 0
|
||||
grand_total = 0
|
||||
grand_total = flt(outstanding_amount) + flt(dunning_fee)
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total += flt(interest_amount)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
'interest_amount': interest_amount,
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
create_dunning_type()
|
||||
create_dunning_type_with_zero_interest_rate()
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@classmethod
|
||||
@@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase):
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
|
||||
|
||||
def test_dunning_with_zero_interest_rate(self):
|
||||
dunning = create_dunning_with_zero_interest_rate()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120)
|
||||
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
@@ -83,6 +93,27 @@ def create_dunning():
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
def create_dunning_with_zero_interest_rate():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
|
||||
dunning = frappe.new_doc("Dunning")
|
||||
dunning.sales_invoice = sales_invoice.name
|
||||
dunning.customer_name = sales_invoice.customer_name
|
||||
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||
dunning.debit_to = sales_invoice.debit_to
|
||||
dunning.currency = sales_invoice.currency
|
||||
dunning.company = sales_invoice.company
|
||||
dunning.posting_date = nowdate()
|
||||
dunning.due_date = sales_invoice.due_date
|
||||
dunning.dunning_type = 'First Notice with 0% Rate of Interest'
|
||||
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||
dunning.dunning_fee = dunning_type.dunning_fee
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
def create_dunning_type():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice'
|
||||
@@ -98,3 +129,19 @@ def create_dunning_type():
|
||||
}
|
||||
)
|
||||
dunning_type.save()
|
||||
|
||||
def create_dunning_type_with_zero_interest_rate():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
|
||||
dunning_type.start_day = 10
|
||||
dunning_type.end_day = 20
|
||||
dunning_type.dunning_fee = 20
|
||||
dunning_type.rate_of_interest = 0
|
||||
dunning_type.append(
|
||||
"dunning_letter_text", {
|
||||
'language': 'en',
|
||||
'body_text': 'We have still not received payment for our invoice ',
|
||||
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
|
||||
}
|
||||
)
|
||||
dunning_type.save()
|
||||
@@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ('GL Entry')
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_journal_entry_condition(self):
|
||||
total_debit = frappe.db.get_value("Journal Entry Account", {
|
||||
@@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document):
|
||||
sum(debit) - sum(credit) as balance
|
||||
from `tabGL Entry`
|
||||
where account in (%s)
|
||||
group by account, party_type, party
|
||||
and posting_date <= %s
|
||||
and is_cancelled = 0
|
||||
group by account, NULLIF(party_type,''), NULLIF(party,'')
|
||||
having sum(debit) != sum(credit)
|
||||
order by account
|
||||
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
|
||||
""" % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
|
||||
|
||||
return account_details
|
||||
|
||||
@@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"party_type": d.get("party_type"),
|
||||
"party": d.get("party"),
|
||||
"account_currency": d.get("account_currency"),
|
||||
"balance": d.get("balance_in_account_currency"),
|
||||
dr_or_cr: abs(d.get("balance_in_account_currency")),
|
||||
"exchange_rate":d.get("new_exchange_rate"),
|
||||
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
|
||||
dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
})
|
||||
@@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document):
|
||||
"party_type": d.get("party_type"),
|
||||
"party": d.get("party"),
|
||||
"account_currency": d.get("account_currency"),
|
||||
"balance": d.get("balance_in_account_currency"),
|
||||
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
|
||||
"exchange_rate": d.get("current_exchange_rate"),
|
||||
"balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
|
||||
reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name
|
||||
})
|
||||
@@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
|
||||
|
||||
account_details = {}
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
|
||||
balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
|
||||
if balance:
|
||||
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
|
||||
balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
|
||||
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
|
||||
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
|
||||
|
||||
@@ -121,8 +121,7 @@ class GLEntry(Document):
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
|
||||
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
|
||||
|
||||
def calculate_total_amount(self):
|
||||
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
|
||||
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
|
||||
|
||||
def on_submit(self):
|
||||
self.update_sales_invoice()
|
||||
|
||||
@@ -196,8 +196,8 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
|
||||
|
||||
def check_credit_limit(self):
|
||||
customers = list(set([d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
|
||||
customers = list(set(d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
for customer in customers:
|
||||
|
||||
@@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
|
||||
idx += 1
|
||||
|
||||
def validate(self):
|
||||
total = sum([flt(d.percentage_allocation) for d in self.get("percentages")])
|
||||
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
|
||||
|
||||
if flt(total, 2) != 100.0:
|
||||
frappe.throw(_("Percentage Allocation should be equal to 100%") + \
|
||||
|
||||
@@ -531,8 +531,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
source_exchange_rate: function(frm) {
|
||||
if (frm.doc.paid_amount) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
if(!frm.set_paid_amount_based_on_received_amount &&
|
||||
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
|
||||
@@ -667,6 +667,7 @@
|
||||
{
|
||||
"fieldname": "base_paid_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Paid Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
@@ -693,21 +694,25 @@
|
||||
"depends_on": "eval:doc.received_amount",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Received Amount After Tax",
|
||||
"options": "paid_to_account_currency"
|
||||
"options": "paid_to_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Received Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-09 11:55:04.215050",
|
||||
"modified": "2021-07-09 08:58:15.008761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -45,7 +45,7 @@ class PaymentEntry(AccountsController):
|
||||
self.party_account = self.paid_to
|
||||
self.party_account_currency = self.paid_to_account_currency
|
||||
|
||||
def validate(self):
|
||||
def validate(self, on_reference_unlink=False):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.validate_payment_type()
|
||||
@@ -55,16 +55,18 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.apply_taxes()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
self.clear_unallocated_reference_document_rows()
|
||||
self.validate_payment_against_negative_invoice()
|
||||
self.validate_transaction_reference()
|
||||
self.set_title()
|
||||
self.set_remarks()
|
||||
self.validate_duplicate_entry()
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
if not on_reference_unlink:
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_status()
|
||||
|
||||
@@ -183,6 +185,13 @@ class PaymentEntry(AccountsController):
|
||||
d.reference_name, self.party_account_currency)
|
||||
|
||||
for field, value in iteritems(ref_details):
|
||||
if d.exchange_gain_loss:
|
||||
# for cases where gain/loss is booked into invoice
|
||||
# exchange_gain_loss is calculated from invoice & populated
|
||||
# and row.exchange_rate is already set to payment entry's exchange rate
|
||||
# refer -> `update_reference_in_payment_entry()` in utils.py
|
||||
continue
|
||||
|
||||
if field == 'exchange_rate' or not d.get(field) or force:
|
||||
d.db_set(field, value)
|
||||
|
||||
@@ -229,7 +238,9 @@ class PaymentEntry(AccountsController):
|
||||
self.company_currency, self.posting_date)
|
||||
|
||||
def set_target_exchange_rate(self, ref_doc=None):
|
||||
if self.paid_to and not self.target_exchange_rate:
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||
self.target_exchange_rate = self.source_exchange_rate
|
||||
elif self.paid_to and not self.target_exchange_rate:
|
||||
if ref_doc:
|
||||
if self.paid_to_account_currency == ref_doc.currency:
|
||||
self.target_exchange_rate = ref_doc.get("exchange_rate")
|
||||
@@ -310,7 +321,7 @@ class PaymentEntry(AccountsController):
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
|
||||
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
|
||||
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
|
||||
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
|
||||
title=_("Warning"), indicator="orange")
|
||||
|
||||
@@ -404,9 +415,15 @@ class PaymentEntry(AccountsController):
|
||||
if not self.advance_tax_account:
|
||||
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
|
||||
|
||||
reference_doclist = []
|
||||
net_total = self.paid_amount
|
||||
included_in_paid_amount = 0
|
||||
|
||||
for reference in self.get("references"):
|
||||
net_total_for_tds = 0
|
||||
if reference.reference_doctype == 'Purchase Order':
|
||||
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
|
||||
|
||||
if net_total_for_tds:
|
||||
net_total = net_total_for_tds
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict({
|
||||
@@ -423,7 +440,7 @@ class PaymentEntry(AccountsController):
|
||||
return
|
||||
|
||||
tax_withholding_details.update({
|
||||
'included_in_paid_amount': included_in_paid_amount,
|
||||
'add_deduct_tax': 'Add',
|
||||
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
})
|
||||
|
||||
@@ -460,6 +477,14 @@ class PaymentEntry(AccountsController):
|
||||
self.set_unallocated_amount()
|
||||
self.set_difference_amount()
|
||||
|
||||
def validate_amounts(self):
|
||||
self.validate_received_amount()
|
||||
|
||||
def validate_received_amount(self):
|
||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||
if self.paid_amount != self.received_amount:
|
||||
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
|
||||
|
||||
def set_received_amount(self):
|
||||
self.base_received_amount = self.base_paid_amount
|
||||
|
||||
@@ -505,23 +530,28 @@ class PaymentEntry(AccountsController):
|
||||
base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate),
|
||||
self.precision("base_paid_amount"))
|
||||
|
||||
self.total_allocated_amount = abs(total_allocated_amount)
|
||||
self.base_total_allocated_amount = abs(base_total_allocated_amount)
|
||||
# Do not use absolute values as only credit notes could be allocated
|
||||
# and total allocated should be negative in that scenario
|
||||
self.total_allocated_amount = total_allocated_amount
|
||||
self.base_total_allocated_amount = base_total_allocated_amount
|
||||
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if self.payment_type == "Receive" \
|
||||
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount_after_tax + total_deductions -
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount + total_deductions -
|
||||
self.base_total_allocated_amount) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif self.payment_type == "Pay" \
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions +
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
|
||||
self.base_total_allocated_amount)) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
|
||||
@@ -530,17 +560,29 @@ class PaymentEntry(AccountsController):
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount_after_tax
|
||||
self.difference_amount = base_party_amount - self.base_received_amount
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount_after_tax - base_party_amount
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
|
||||
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions,
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
|
||||
self.precision("difference_amount"))
|
||||
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get('taxes'):
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == 'Add':
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
# Paid amount is auto allocated in the reference document by default.
|
||||
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
|
||||
def clear_unallocated_reference_document_rows(self):
|
||||
@@ -552,8 +594,8 @@ class PaymentEntry(AccountsController):
|
||||
if ((self.payment_type=="Pay" and self.party_type=="Customer")
|
||||
or (self.payment_type=="Receive" and self.party_type=="Supplier")):
|
||||
|
||||
total_negative_outstanding = sum([abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0])
|
||||
total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
|
||||
additional_charges = sum([flt(d.amount) for d in self.deductions])
|
||||
@@ -664,8 +706,8 @@ class PaymentEntry(AccountsController):
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
base_unallocated_amount = self.unallocated_amount * \
|
||||
(self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = (self.unallocated_amount * exchange_rate)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
@@ -683,8 +725,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_from,
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type=="Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount_after_tax,
|
||||
"credit": self.base_paid_amount_after_tax,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@@ -694,8 +736,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_to,
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount_after_tax,
|
||||
"debit": self.base_received_amount_after_tax,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@@ -706,37 +748,44 @@ class PaymentEntry(AccountsController):
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
|
||||
|
||||
if self.payment_type == 'Pay':
|
||||
if self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
against = self.party or self.paid_from
|
||||
elif self.payment_type == 'Receive':
|
||||
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
|
||||
against = self.party or self.paid_to
|
||||
|
||||
payment_or_advance_account = self.get_party_account_for_taxes()
|
||||
tax_amount = d.tax_amount
|
||||
base_tax_amount = d.base_tax_amount
|
||||
|
||||
if self.advance_tax_account:
|
||||
tax_amount = -1 * tax_amount
|
||||
base_tax_amount = -1 * base_tax_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": d.account_head,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": d.base_tax_amount
|
||||
"against": against,
|
||||
dr_or_cr: tax_amount,
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": d.cost_center
|
||||
}, account_currency, item=d))
|
||||
|
||||
#Intentionally use -1 to get net values in party account
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: -1 * d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1*d.base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party
|
||||
}, account_currency, item=d))
|
||||
if not d.included_in_paid_amount or self.advance_tax_account:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": against,
|
||||
dr_or_cr: -1 * tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1 * base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
}, account_currency, item=d))
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
@@ -760,9 +809,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.advance_tax_account:
|
||||
return self.advance_tax_account
|
||||
elif self.payment_type == 'Receive':
|
||||
return self.paid_from
|
||||
elif self.payment_type == 'Pay':
|
||||
return self.paid_to
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_from
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
@@ -806,10 +855,17 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if account_details:
|
||||
row.update(account_details)
|
||||
|
||||
if not row.get('amount'):
|
||||
# if no difference amount
|
||||
return
|
||||
|
||||
self.append('deductions', row)
|
||||
self.set_unallocated_amount()
|
||||
|
||||
def get_exchange_rate(self):
|
||||
return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
|
||||
|
||||
def initialize_taxes(self):
|
||||
for tax in self.get("taxes"):
|
||||
validate_taxes_and_charges(tax)
|
||||
@@ -1318,9 +1374,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
return frappe._dict({
|
||||
"due_date": ref_doc.get("due_date"),
|
||||
"total_amount": total_amount,
|
||||
"outstanding_amount": outstanding_amount,
|
||||
"exchange_rate": exchange_rate,
|
||||
"total_amount": flt(total_amount),
|
||||
"outstanding_amount": flt(outstanding_amount),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"bill_no": bill_no
|
||||
})
|
||||
|
||||
@@ -1634,12 +1690,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
||||
if dt == "Employee Advance":
|
||||
paid_amount = received_amount * doc.get('exchange_rate', 1)
|
||||
|
||||
if dt == "Purchase Order" and doc.apply_tds:
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = doc.base_net_total
|
||||
else:
|
||||
paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1)
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
|
||||
@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
|
||||
|
||||
self.assertEqual(pe.cost_center, si.cost_center)
|
||||
self.assertEqual(expected_account_balance, account_balance)
|
||||
self.assertEqual(expected_party_balance, party_balance)
|
||||
self.assertEqual(expected_party_account_balance, party_account_balance)
|
||||
self.assertEqual(flt(expected_account_balance), account_balance)
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
"allocated_amount",
|
||||
"exchange_rate"
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -90,12 +91,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Term",
|
||||
"options": "Payment Term"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-10 11:25:47.144392",
|
||||
"modified": "2021-04-21 13:30:11.605388",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.submit()
|
||||
@@ -112,7 +112,7 @@ class PaymentRequest(Document):
|
||||
if not data_of_completed_requests:
|
||||
return self.grand_total
|
||||
|
||||
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
|
||||
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
|
||||
return request_amounts
|
||||
|
||||
def on_cancel(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
|
||||
def check_duplicate_terms(self):
|
||||
terms = []
|
||||
for term in self.terms:
|
||||
term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
if term_info in terms:
|
||||
frappe.msgprint(
|
||||
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
|
||||
|
||||
@@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
|
||||
class MultiplePricingRuleConflict(frappe.ValidationError): pass
|
||||
|
||||
apply_on_table = {
|
||||
'Item Code': 'items',
|
||||
'Item Group': 'item_groups',
|
||||
'Brand': 'brands'
|
||||
'Item Code': 'items',
|
||||
'Item Group': 'item_groups',
|
||||
'Brand': 'brands'
|
||||
}
|
||||
|
||||
def get_pricing_rules(args, doc=None):
|
||||
@@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
frappe.throw(_("Invalid {0}").format(args.get(field)))
|
||||
|
||||
parent_groups = frappe.db.sql_list("""select name from `tab%s`
|
||||
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
|
||||
where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
|
||||
|
||||
if parenttype in ["Customer Group", "Item Group", "Territory"]:
|
||||
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
|
||||
@@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
|
||||
table=table,
|
||||
field=field,
|
||||
parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups])
|
||||
parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
@@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
|
||||
# find pricing rule with highest priority
|
||||
if pricing_rules:
|
||||
max_priority = max([cint(p.priority) for p in pricing_rules])
|
||||
max_priority = max(cint(p.priority) for p in pricing_rules)
|
||||
if max_priority:
|
||||
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
|
||||
|
||||
@@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
pricing_rules = list(pricing_rules)
|
||||
|
||||
if len(pricing_rules) > 1:
|
||||
rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules]))
|
||||
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
|
||||
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
|
||||
pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
|
||||
or pricing_rules
|
||||
|
||||
if len(pricing_rules) > 1 and not args.for_shopping_cart:
|
||||
frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
|
||||
.format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict)
|
||||
.format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
|
||||
elif pricing_rules:
|
||||
return pricing_rules[0]
|
||||
|
||||
@@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
|
||||
if pricing_rule_args:
|
||||
items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
|
||||
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
|
||||
|
||||
for args in pricing_rule_args:
|
||||
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
|
||||
@@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type):
|
||||
elif transaction_type=='cancelled':
|
||||
if coupon.used>0:
|
||||
coupon.used=coupon.used-1
|
||||
coupon.save(ignore_permissions=True)
|
||||
coupon.save(ignore_permissions=True)
|
||||
|
||||
@@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
@frappe.whitelist()
|
||||
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
||||
billing_email = frappe.db.sql("""
|
||||
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
|
||||
WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
|
||||
c.is_billing_contact=1 \
|
||||
order by c.creation desc""")
|
||||
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
|
||||
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
|
||||
order by c.creation desc""", customer_name)
|
||||
|
||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||
if billing_and_primary:
|
||||
|
||||
@@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
});
|
||||
},
|
||||
|
||||
company: function() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
onload: function() {
|
||||
this._super();
|
||||
|
||||
@@ -369,7 +365,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
items_add: function(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row,
|
||||
["expense_account", "cost_center", "project"]);
|
||||
["expense_account", "discount_account", "cost_center", "project"]);
|
||||
},
|
||||
|
||||
on_submit: function() {
|
||||
@@ -503,6 +499,16 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
'Payment Entry': 'Payment'
|
||||
}
|
||||
|
||||
frm.set_query("additional_discount_account", function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
report_type: "Profit and Loss",
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict['items'].grid.get_field('deferred_expense_account').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -512,6 +518,16 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
'report_type': 'Profit and Loss',
|
||||
'company': doc.company,
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@@ -569,5 +585,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
})
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"section_break_44",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_46",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
@@ -706,13 +707,12 @@
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "supplied_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplied Items",
|
||||
"options": "Purchase Receipt Item Supplied",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Receipt Item Supplied"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_26",
|
||||
@@ -1686,13 +1686,19 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Additional Discount Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-09 12:30:25.632109",
|
||||
"modified": "2021-07-17 17:37:50.570595",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.set_consumed_qty_in_po()
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
|
||||
@@ -445,11 +446,13 @@ 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)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.allocate_advance_taxes(gl_entries)
|
||||
@@ -606,7 +609,11 @@ class PurchaseInvoice(BuyingController):
|
||||
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
|
||||
|
||||
if not item.is_fixed_asset:
|
||||
amount = flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
if frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'):
|
||||
amount = flt(item.base_amount, item.precision("base_amount"))
|
||||
else:
|
||||
amount = flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
|
||||
@@ -998,6 +1005,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
|
||||
@@ -230,6 +230,48 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
enable_discount_accounting()
|
||||
|
||||
discount_account = create_account(account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||
pi = make_purchase_invoice(discount_account=discount_account, discount_amount=100)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 350.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 250.0, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 100.0, nowdate()]
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
enable_discount_accounting()
|
||||
additional_discount_account = create_account(account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||
|
||||
pi = make_purchase_invoice(qty=1, rate=100, 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 = 20
|
||||
pi.append("taxes", {
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test",
|
||||
"tax_amount": 20
|
||||
})
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 100.0, 0.0, nowdate()],
|
||||
["_Test Account VAT - _TC", 20.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 96.0, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 24.0, 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()
|
||||
@@ -621,8 +663,10 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(actual_qty_0, get_qty_after_transaction())
|
||||
|
||||
def test_subcontracting_via_purchase_invoice(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on('BOM')
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
|
||||
qty=100, basic_rate=100)
|
||||
@@ -632,7 +676,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(pi.get("supplied_items")), 2)
|
||||
|
||||
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
|
||||
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
|
||||
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
|
||||
|
||||
def test_rejected_serial_no(self):
|
||||
@@ -951,6 +995,120 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings",
|
||||
"unlink_payment_on_cancel_of_invoice")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Accounts Settings", "Accounts Settings",
|
||||
"unlink_payment_on_cancel_of_invoice", 1)
|
||||
|
||||
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
|
||||
|
||||
pay = frappe.get_doc({
|
||||
'doctype': 'Payment Entry',
|
||||
'company': '_Test Company',
|
||||
'payment_type': 'Pay',
|
||||
'party_type': 'Supplier',
|
||||
'party': '_Test Supplier USD',
|
||||
'paid_to': '_Test Payable USD - _TC',
|
||||
'paid_from': 'Cash - _TC',
|
||||
'paid_amount': 70000,
|
||||
'target_exchange_rate': 70,
|
||||
'received_amount': 1000,
|
||||
})
|
||||
pay.insert()
|
||||
pay.submit()
|
||||
|
||||
pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
|
||||
conversion_rate=75, rate=500, do_not_save=1, qty=1)
|
||||
pi.cost_center = "_Test Cost Center - _TC"
|
||||
pi.advances = []
|
||||
pi.append("advances", {
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": pay.name,
|
||||
"advance_amount": 1000,
|
||||
"remarks": pay.remarks,
|
||||
"allocated_amount": 500,
|
||||
"ref_exchange_rate": 70
|
||||
})
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 37500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -2500.0]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""
|
||||
select account, sum(debit - credit) as balance from `tabGL Entry`
|
||||
where voucher_no=%s
|
||||
group by account
|
||||
order by account asc""", (pi.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
|
||||
conversion_rate=73, rate=500, do_not_save=1, qty=1)
|
||||
pi_2.cost_center = "_Test Cost Center - _TC"
|
||||
pi_2.advances = []
|
||||
pi_2.append("advances", {
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": pay.name,
|
||||
"advance_amount": 500,
|
||||
"remarks": pay.remarks,
|
||||
"allocated_amount": 500,
|
||||
"ref_exchange_rate": 70
|
||||
})
|
||||
pi_2.save()
|
||||
pi_2.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 36500.0],
|
||||
["_Test Payable USD - _TC", -35000.0],
|
||||
["Exchange Gain/Loss - _TC", -1500.0]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""
|
||||
select account, sum(debit - credit) as balance from `tabGL Entry`
|
||||
where voucher_no=%s
|
||||
group by account order by account asc""", (pi_2.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Payable USD - _TC", 70000.0],
|
||||
["Cash - _TC", -70000.0]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""
|
||||
select account, sum(debit - credit) as balance from `tabGL Entry`
|
||||
where voucher_no=%s and is_cancelled=0
|
||||
group by account order by account asc""", (pay.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.balance)
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
|
||||
pi_2.reload()
|
||||
pi_2.cancel()
|
||||
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -964,7 +1122,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
||||
|
||||
# Create Purchase Order with TDS applied
|
||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000)
|
||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
|
||||
po.apply_tds = 1
|
||||
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||
po.save()
|
||||
@@ -1000,6 +1158,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
# Create Purchase Invoice against Purchase Order
|
||||
purchase_invoice = get_mapped_purchase_invoice(po.name)
|
||||
purchase_invoice.allocate_advances_automatically = 1
|
||||
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
|
||||
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
|
||||
purchase_invoice.save()
|
||||
purchase_invoice.submit()
|
||||
@@ -1007,21 +1166,33 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
# Check GLE for Purchase Invoice
|
||||
# Zero net effect on final TDS Payable on invoice
|
||||
expected_gle = [
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
|
||||
['_Test Account Excise Duty - _TC', 0, 3000],
|
||||
['Creditors - _TC', 0, 27000],
|
||||
['TDS Payable - _TC', 3000, 3000]
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000],
|
||||
['_Test Account Excise Duty - _TC', -3000],
|
||||
['Creditors - _TC', -27000],
|
||||
['TDS Payable - _TC', 0]
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""", (purchase_invoice.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
||||
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
|
||||
order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
def update_tax_witholding_category(company, account, date):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -1053,6 +1224,11 @@ def unlink_payment_on_cancel_of_invoice(enable=1):
|
||||
accounts_settings.unlink_payment_on_cancellation_of_invoice = enable
|
||||
accounts_settings.save()
|
||||
|
||||
def enable_discount_accounting(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
accounts_settings.enable_discount_accounting = enable
|
||||
accounts_settings.save()
|
||||
|
||||
def make_purchase_invoice(**args):
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
args = frappe._dict(args)
|
||||
@@ -1075,6 +1251,7 @@ def make_purchase_invoice(**args):
|
||||
pi.return_against = args.return_against
|
||||
pi.is_subcontracted = args.is_subcontracted or "No"
|
||||
pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
|
||||
pi.cost_center = args.parent_cost_center
|
||||
|
||||
pi.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
@@ -1083,7 +1260,9 @@ def make_purchase_invoice(**args):
|
||||
"received_qty": args.received_qty or 0,
|
||||
"rejected_qty": args.rejected_qty or 0,
|
||||
"rate": args.rate or 50,
|
||||
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
|
||||
"expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC',
|
||||
"discount_account": args.discount_account or None,
|
||||
"discount_amount": args.discount_amount or 0,
|
||||
"conversion_factor": 1.0,
|
||||
"serial_no": args.serial_no,
|
||||
"stock_uom": args.uom or "_Test UOM",
|
||||
|
||||
@@ -1,235 +1,127 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-03-08 15:36:46",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2013-03-08 15:36:46",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"remarks",
|
||||
"reference_row",
|
||||
"col_break1",
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Reference Type",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "180px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"print_width": "180px",
|
||||
"read_only": 1,
|
||||
"width": "180px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Reference Row",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Date",
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "80px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1,
|
||||
"print_width": "80px",
|
||||
"read_only": 1,
|
||||
"width": "80px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Advance Amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance Amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "100px",
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ref_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2016-08-26 02:30:54.407138",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-20 16:26:53.820530",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -73,6 +73,7 @@
|
||||
"manufacturer_part_no",
|
||||
"accounting",
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"col_break5",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
@@ -272,7 +273,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate ",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
@@ -849,12 +850,18 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Discount Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 09:02:39.256602",
|
||||
"modified": "2021-07-13 02:04:37.787882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -347,7 +347,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
|
||||
items_add: function(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "cost_center"]);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "discount_account", "cost_center"]);
|
||||
},
|
||||
|
||||
set_dynamic_labels: function() {
|
||||
@@ -510,7 +510,6 @@ cur_frm.set_query("income_account", "items", function(doc) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Cost Center in Details Table
|
||||
// -----------------------------
|
||||
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
|
||||
@@ -592,6 +591,16 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("additional_discount_account", function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
report_type: "Profit and Loss",
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Delivery Note': 'Delivery',
|
||||
'Sales Invoice': 'Return / Credit Note',
|
||||
@@ -618,6 +627,17 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
}
|
||||
|
||||
// discount account
|
||||
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
'report_type': 'Profit and Loss',
|
||||
'company': doc.company,
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.fields_dict['items'].grid.get_field('deferred_revenue_account').get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"section_break_49",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
@@ -1966,6 +1967,12 @@
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Additional Discount Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -1978,7 +1985,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-20 22:48:33.988881",
|
||||
"modified": "2021-07-15 21:57:17.544279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -840,11 +840,13 @@ class SalesInvoice(SellingController):
|
||||
self.make_customer_gl_entry(gl_entries)
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_exchange_gain_loss_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.allocate_advance_taxes(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
@@ -884,18 +886,22 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting)
|
||||
|
||||
if flt(tax.base_tax_amount_after_discount_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": tax.account_head,
|
||||
"against": self.customer,
|
||||
"credit": flt(tax.base_tax_amount_after_discount_amount,
|
||||
"credit": flt(base_amount,
|
||||
tax.precision("tax_amount_after_discount_amount")),
|
||||
"credit_in_account_currency": (flt(tax.base_tax_amount_after_discount_amount,
|
||||
"credit_in_account_currency": (flt(base_amount,
|
||||
tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else
|
||||
flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))),
|
||||
flt(amount, tax.precision("tax_amount_after_discount_amount"))),
|
||||
"cost_center": tax.cost_center
|
||||
}, account_currency, item=tax)
|
||||
)
|
||||
@@ -914,6 +920,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
# income account gl entries
|
||||
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
if item.is_fixed_asset:
|
||||
@@ -939,15 +947,17 @@ class SalesInvoice(SellingController):
|
||||
income_account = (item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency==self.company_currency
|
||||
else flt(item.net_amount, item.precision("net_amount"))),
|
||||
else flt(amount, item.precision("net_amount"))),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project
|
||||
}, account_currency, item=item)
|
||||
@@ -958,6 +968,12 @@ class SalesInvoice(SellingController):
|
||||
erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
gl_entries += super(SalesInvoice, self).get_gl_entries()
|
||||
|
||||
def set_asset_status(self, asset):
|
||||
if self.is_return:
|
||||
asset.set_status()
|
||||
else:
|
||||
asset.set_status("Sold" if self.docstatus==1 else None)
|
||||
|
||||
def make_loyalty_point_redemption_gle(self, gl_entries):
|
||||
if cint(self.redeem_loyalty_points):
|
||||
gl_entries.append(
|
||||
@@ -989,7 +1005,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for payment_mode in self.payments:
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= self.change_amount
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
if payment_mode.amount:
|
||||
# POS, make payment entries
|
||||
|
||||
@@ -1957,6 +1957,80 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
item.set('taxes', [])
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"minimum_net_rate": 0,
|
||||
"maximum_net_rate": 500
|
||||
})
|
||||
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||
"minimum_net_rate": 501,
|
||||
"maximum_net_rate": 1000
|
||||
})
|
||||
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = 'Net Total'
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting
|
||||
|
||||
enable_discount_accounting()
|
||||
|
||||
discount_account = create_account(account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||
si = create_sales_invoice(discount_account=discount_account, discount_amount=100)
|
||||
|
||||
expected_gle = [
|
||||
["Debtors - _TC", 100.0, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 100.0, 0.0, nowdate()],
|
||||
["Sales - _TC", 0.0, 200.0, nowdate()]
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting
|
||||
|
||||
enable_discount_accounting()
|
||||
additional_discount_account = create_account(account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||
|
||||
si = create_sales_invoice(rate=100, parent_cost_center='Main - _TC', do_not_save=1)
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.additional_discount_account = additional_discount_account
|
||||
si.additional_discount_percentage = 20
|
||||
si.append("taxes", {
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test",
|
||||
"rate": 0,
|
||||
"tax_amount": 20
|
||||
})
|
||||
si.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account VAT - _TC", 0.0, 20.0, nowdate()],
|
||||
["Debtors - _TC", 96.0, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 24.0, 0.0, nowdate()],
|
||||
["Sales - _TC", 0.0, 100.0, nowdate()]
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
@@ -1985,32 +2059,6 @@ def get_sales_invoice_for_e_invoice():
|
||||
|
||||
return si
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
item.set('taxes', [])
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"minimum_net_rate": 0,
|
||||
"maximum_net_rate": 500
|
||||
})
|
||||
|
||||
item.append("taxes", {
|
||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||
"minimum_net_rate": 501,
|
||||
"maximum_net_rate": 1000
|
||||
})
|
||||
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = 'Net Total'
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
|
||||
def make_test_address_for_ewaybill():
|
||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
||||
@@ -2087,9 +2135,9 @@ def make_sales_invoice_for_ewaybill():
|
||||
if not gst_account:
|
||||
gst_settings.append("gst_accounts", {
|
||||
"company": "_Test Company",
|
||||
"cgst_account": "CGST - _TC",
|
||||
"sgst_account": "SGST - _TC",
|
||||
"igst_account": "IGST - _TC",
|
||||
"cgst_account": "Output Tax CGST - _TC",
|
||||
"sgst_account": "Output Tax SGST - _TC",
|
||||
"igst_account": "Output Tax IGST - _TC",
|
||||
})
|
||||
|
||||
gst_settings.save()
|
||||
@@ -2106,7 +2154,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "CGST - _TC",
|
||||
"account_head": "Output Tax CGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9
|
||||
@@ -2114,7 +2162,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "SGST - _TC",
|
||||
"account_head": "Output Tax SGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9
|
||||
@@ -2151,6 +2199,7 @@ def create_sales_invoice(**args):
|
||||
si.currency=args.currency or "INR"
|
||||
si.conversion_rate = args.conversion_rate or 1
|
||||
si.naming_series = args.naming_series or "T-SINV-"
|
||||
si.cost_center = args.parent_cost_center
|
||||
|
||||
si.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
@@ -2164,6 +2213,8 @@ def create_sales_invoice(**args):
|
||||
"rate": args.rate if args.get("rate") is not None else 100,
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"discount_account": args.discount_account or None,
|
||||
"discount_amount": args.discount_amount or 0,
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no,
|
||||
"conversion_factor": 1
|
||||
|
||||
@@ -1,235 +1,128 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:27:41",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2013-02-22 01:27:41",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"remarks",
|
||||
"reference_row",
|
||||
"col_break1",
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Reference Type",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "250px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"print_width": "250px",
|
||||
"read_only": 1,
|
||||
"width": "250px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Reference Row",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Data",
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"width": "120px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Advance amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"width": "120px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated amount",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "120px",
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ref_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2016-08-26 02:36:10.718057",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-04 20:25:49.832052",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -63,6 +63,7 @@
|
||||
"finance_book",
|
||||
"col_break4",
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"deferred_revenue",
|
||||
"deferred_revenue_account",
|
||||
"service_stop_date",
|
||||
@@ -821,12 +822,18 @@
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Discount Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-23 01:05:22.123527",
|
||||
"modified": "2021-07-05 15:07:22.857128",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_tax_amount_after_discount_amount",
|
||||
"item_wise_tax_detail"
|
||||
"item_wise_tax_detail",
|
||||
"dont_recompute_tax"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -200,13 +201,22 @@
|
||||
"fieldname": "included_in_paid_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "dont_recompute_tax",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Dont Recompute tax",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-14 01:44:36.899147",
|
||||
"modified": "2021-07-27 12:40:59.051803",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges",
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
|
||||
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
|
||||
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
|
||||
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
|
||||
if(frm.doc.__islocal) {
|
||||
frm.set_value("use_for_shopping_cart", 1);
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
|
||||
frappe.ui.form.trigger("Tax Rule", "tax_type");
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
|
||||
if(frm.doc.customer) {
|
||||
frappe.call({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
|
||||
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
|
||||
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
|
||||
tax_rule1.save()
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
|
||||
"_Test Sales Taxes and Charges Template - _TC")
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
|
||||
@@ -1,263 +1,151 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"beta": 0,
|
||||
"creation": "2018-04-13 18:42:06.431683",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2018-04-13 18:42:06.431683",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"category_details_section",
|
||||
"category_name",
|
||||
"round_off_tax_amount",
|
||||
"column_break_2",
|
||||
"consider_party_ledger_amount",
|
||||
"tax_on_excess_amount",
|
||||
"section_break_8",
|
||||
"rates",
|
||||
"section_break_7",
|
||||
"accounts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "category_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Category Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax Withholding Rates",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "rates",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Rates",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Tax Withholding Rate",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Account Details",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Accounts",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Tax Withholding Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Accounts",
|
||||
"options": "Tax Withholding Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "category_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Category Details",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
|
||||
"fieldname": "tax_on_excess_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Deduct Tax On Excess Amount ",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "Checking this will round off the tax amount to the nearest integer",
|
||||
"fieldname": "round_off_tax_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Off Tax Amount",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-07-17 22:53:26.193179",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-27 21:47:34.396071",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, getdate
|
||||
from frappe.utils import flt, getdate, cint
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
@@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
|
||||
"rate": tax_rate_detail.tax_withholding_rate,
|
||||
"threshold": tax_rate_detail.single_threshold,
|
||||
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
|
||||
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category
|
||||
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
|
||||
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
|
||||
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
|
||||
"round_off_tax_amount": tax_withholding.round_off_tax_amount
|
||||
})
|
||||
|
||||
def get_tax_withholding_rates(tax_withholding, fiscal_year):
|
||||
@@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
|
||||
fiscal_year = fiscal_year_details[0]
|
||||
|
||||
|
||||
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
||||
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
@@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {
|
||||
'name': ('in', vouchers),
|
||||
'docstatus': 1
|
||||
}
|
||||
|
||||
supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
|
||||
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
|
||||
}, 'sum(net_total)') or 0.0
|
||||
field = 'sum(net_total)'
|
||||
|
||||
if not cint(tax_details.consider_party_ledger_amount):
|
||||
invoice_filters.update({'apply_tds': 1})
|
||||
field = 'sum(grand_total)'
|
||||
|
||||
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
|
||||
'parent': ('in', vouchers), 'docstatus': 1,
|
||||
@@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||
|
||||
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
|
||||
# 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 += inv.net_total
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto,
|
||||
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
|
||||
@@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tds_amount = round(tds_amount)
|
||||
|
||||
return tds_amount
|
||||
|
||||
|
||||
@@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_tax_withholding_category_checks(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||
|
||||
# First Invoice with no tds check
|
||||
pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
|
||||
pi.apply_tds = 0
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Second Invoice will apply TDS checked
|
||||
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 30000
|
||||
# Threshold calculation should be on both the invoices
|
||||
# TDS should be applied only on 1000
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
|
||||
invoices = []
|
||||
@@ -112,7 +137,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
|
||||
si.submit()
|
||||
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
|
||||
tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
|
||||
self.assertEqual(tcs_charged, 500)
|
||||
invoices.append(si)
|
||||
|
||||
@@ -195,7 +220,7 @@ def create_sales_invoice(**args):
|
||||
|
||||
def create_records():
|
||||
# create a new suppliers
|
||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
|
||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
|
||||
if frappe.db.exists('Supplier', name):
|
||||
continue
|
||||
|
||||
@@ -311,3 +336,23 @@ def create_tax_with_holding_category():
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "New TDS Category",
|
||||
"category_name": "New TDS Category",
|
||||
"round_off_tax_amount": 1,
|
||||
"consider_party_ledger_amount": 1,
|
||||
"tax_on_excess_amount": 1,
|
||||
"rates": [{
|
||||
'fiscal_year': fiscal_year,
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 0,
|
||||
'cumulative_threshold': 30000
|
||||
}],
|
||||
"accounts": [{
|
||||
'company': '_Test Company',
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
@@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
||||
'cost_center', 'project']
|
||||
'cost_center', 'project', 'voucher_detail_no']
|
||||
|
||||
if dimensions:
|
||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||
@@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
def validate_cwip_accounts(gl_map):
|
||||
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
|
||||
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
|
||||
|
||||
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
|
||||
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
|
||||
|
||||
@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry`
|
||||
where party_type = %s and party=%s
|
||||
and is_cancelled = 0
|
||||
group by company""", (party_type, party)))
|
||||
|
||||
for d in companies:
|
||||
|
||||
@@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
|
||||
voucher_no = gle.voucher_no,
|
||||
party = gle.party,
|
||||
posting_date = gle.posting_date,
|
||||
remarks = gle.remarks,
|
||||
account_currency = gle.account_currency,
|
||||
invoiced = 0.0,
|
||||
paid = 0.0,
|
||||
@@ -579,7 +578,7 @@ class ReceivablePayableReport(object):
|
||||
self.gl_entries = frappe.db.sql("""
|
||||
select
|
||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||
against_voucher_type, against_voucher, account_currency, remarks, {0}
|
||||
against_voucher_type, against_voucher, account_currency, {0}
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
@@ -792,8 +791,6 @@ class ReceivablePayableReport(object):
|
||||
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
|
||||
options='Supplier Group')
|
||||
|
||||
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)
|
||||
if fieldtype=='Currency': options='currency'
|
||||
|
||||
@@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data):
|
||||
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
|
||||
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
|
||||
]
|
||||
}
|
||||
},
|
||||
'type' : 'bar'
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names):
|
||||
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
|
||||
where cfma.parent in (%s)
|
||||
order by cfm.is_working_capital
|
||||
''', (', '.join(['"%s"' % d for d in mapping_names])))
|
||||
''', (', '.join('"%s"' % d for d in mapping_names)))
|
||||
|
||||
|
||||
def setup_mappers(mappers):
|
||||
@@ -83,8 +83,8 @@ def setup_mappers(mappers):
|
||||
|
||||
account_types_labels = sorted(
|
||||
set(
|
||||
[(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types]
|
||||
(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types
|
||||
),
|
||||
key=lambda x: x[1]
|
||||
)
|
||||
@@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
|
||||
total = 0
|
||||
for period in period_list:
|
||||
start_date = get_start_date(period, accumulated_values, company)
|
||||
accounts = ', '.join(['"%s"' % d for d in account_names])
|
||||
accounts = ', '.join('"%s"' % d for d in account_names)
|
||||
|
||||
if opening_balances:
|
||||
date_info = dict(date=start_date)
|
||||
|
||||
@@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
|
||||
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||
acc.account_name, acc.account_number
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
|
||||
{
|
||||
|
||||
@@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
|
||||
out = []
|
||||
for party, row in iteritems(self.party_data):
|
||||
if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount:
|
||||
total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))])
|
||||
total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {})))
|
||||
row.paid_amount -= total_party_adjustment
|
||||
|
||||
adjustments = self.party_adjustment_details.get(party, {})
|
||||
|
||||
@@ -369,7 +369,7 @@ def set_gl_entries_by_account(
|
||||
|
||||
if accounts:
|
||||
additional_conditions += " and account in ({})"\
|
||||
.format(", ".join([frappe.db.escape(d) for d in accounts]))
|
||||
.format(", ".join(frappe.db.escape(d) for d in accounts))
|
||||
|
||||
gl_filters = {
|
||||
"company": company,
|
||||
|
||||
@@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
"get_query": function() {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
"doctype": "Account",
|
||||
"filters": {
|
||||
"company": company,
|
||||
}
|
||||
}
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Cost Center"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Cost Center', txt);
|
||||
return frappe.db.get_link_options('Cost Center', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Project"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Project', txt);
|
||||
return frappe.db.get_link_options('Project', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,13 +48,18 @@ def validate_filters(filters, account_details):
|
||||
|
||||
if not filters.get("from_date") and not filters.get("to_date"):
|
||||
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
|
||||
|
||||
if filters.get('account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
for account in filters.account:
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get("account") and not account_details.get(filters.account):
|
||||
frappe.throw(_("Account {0} does not exists").format(filters.account))
|
||||
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
|
||||
and account_details[filters.account].is_group == 0):
|
||||
frappe.throw(_("Can not filter based on Account, if grouped by Account"))
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if (filters.get("voucher_no")
|
||||
and filters.get("group_by") in [_('Group by Voucher')]):
|
||||
@@ -87,7 +92,19 @@ def set_account_currency(filters):
|
||||
account_currency = None
|
||||
|
||||
if filters.get("account"):
|
||||
account_currency = get_account_currency(filters.account)
|
||||
if len(filters.get("account")) == 1:
|
||||
account_currency = get_account_currency(filters.account[0])
|
||||
else:
|
||||
currency = get_account_currency(filters.account[0])
|
||||
is_same_account_currency = True
|
||||
for account in filters.get("account"):
|
||||
if get_account_currency(account) != currency:
|
||||
is_same_account_currency = False
|
||||
break
|
||||
|
||||
if is_same_account_currency:
|
||||
account_currency = currency
|
||||
|
||||
elif filters.get("party"):
|
||||
gle_currency = frappe.db.get_value(
|
||||
"GL Entry", {
|
||||
@@ -205,10 +222,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
if filters.get("account") and not filters.get("include_dimensions"):
|
||||
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
|
||||
conditions.append("""account in (select name from tabAccount
|
||||
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@@ -266,6 +283,20 @@ def get_conditions(filters):
|
||||
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
def get_accounts_with_children(accounts):
|
||||
if not isinstance(accounts, list):
|
||||
accounts = [d.strip() for d in accounts.strip().split(',') if d]
|
||||
|
||||
all_accounts = []
|
||||
for d in accounts:
|
||||
if frappe.db.exists("Account", d):
|
||||
lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_accounts += [c.name for c in children]
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
|
||||
@@ -241,6 +241,7 @@ class GrossProfitGenerator(object):
|
||||
sle.voucher_detail_no == row.item_row:
|
||||
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))
|
||||
else:
|
||||
@@ -335,7 +336,7 @@ class GrossProfitGenerator(object):
|
||||
res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
|
||||
voucher_detail_no, stock_value, warehouse, actual_qty as qty
|
||||
from `tabStock Ledger Entry`
|
||||
where company=%(company)s
|
||||
where company=%(company)s and is_cancelled = 0
|
||||
order by
|
||||
item_code desc, warehouse desc, posting_date desc,
|
||||
posting_time desc, creation desc""", self.filters, as_dict=True)
|
||||
|
||||
@@ -334,7 +334,7 @@ def get_aii_accounts():
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
po_pr_map = frappe._dict()
|
||||
po_item_rows = list(set([d.po_detail for d in item_list]))
|
||||
po_item_rows = list(set(d.po_detail for d in item_list))
|
||||
|
||||
if po_item_rows:
|
||||
purchase_receipts = frappe.db.sql("""
|
||||
|
||||
@@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
data = []
|
||||
|
||||
@@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
|
||||
), filters, as_dict=1)
|
||||
|
||||
def concat_mode_of_payments(pos_entries):
|
||||
mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
|
||||
for entry in pos_entries:
|
||||
if mode_of_payments.get(entry.pos_invoice):
|
||||
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
|
||||
|
||||
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
|
||||
grand_total = sum([d.grand_total for d in group_invoices])
|
||||
paid_amount = sum([d.paid_amount for d in group_invoices])
|
||||
grand_total = sum(d.grand_total for d in group_invoices)
|
||||
paid_amount = sum(d.paid_amount for d in group_invoices)
|
||||
data.append({
|
||||
group_by_field: group_by_value,
|
||||
"grand_total": grand_total,
|
||||
|
||||
@@ -168,21 +168,24 @@ def get_columns(filters):
|
||||
"label": _("Income"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "expense",
|
||||
"label": _("Expense"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 305
|
||||
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_profit_loss",
|
||||
"label": _("Gross Profit / Loss"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120
|
||||
"width": 307
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
|
||||
invoice_expense_map, expense_accounts)
|
||||
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
|
||||
suppliers = list(set([d.supplier for d in invoice_list]))
|
||||
suppliers = list(set(d.supplier for d in invoice_list))
|
||||
supplier_details = get_supplier_details(suppliers)
|
||||
|
||||
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
|
||||
@@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
and docstatus = 1 and (account_head is not null and account_head != '')
|
||||
and category in ('Total', 'Valuation and Total')
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
|
||||
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
|
||||
@@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
group by parent, expense_account
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_expense_map = {}
|
||||
for d in expense_details:
|
||||
@@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
|
||||
and is_internal_supplier = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
group by parent, account_head, add_deduct_tax
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_po_pr_map = {}
|
||||
for d in pi_items:
|
||||
|
||||
@@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
|
||||
def get_mode_of_payments(filters):
|
||||
mode_of_payments = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
|
||||
from `tabSales Invoice` a, `tabSales Invoice Payment` b
|
||||
@@ -197,7 +197,7 @@ def get_invoices(filters):
|
||||
def get_mode_of_payment_details(filters):
|
||||
mode_of_payment_details = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
|
||||
|
||||
@@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
income_accounts = frappe.db.sql_list("""select distinct income_account
|
||||
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
|
||||
order by income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
tax_accounts = frappe.db.sql_list("""select distinct account_head
|
||||
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
|
||||
and docstatus = 1 and base_tax_amount_after_discount_amount != 0
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabSales Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
for account in income_accounts:
|
||||
income_columns.append({
|
||||
@@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
|
||||
def get_invoice_income_map(invoice_list):
|
||||
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount
|
||||
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_income_map = {}
|
||||
for d in income_details:
|
||||
@@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabSales Invoice` where name in (%s)
|
||||
and is_internal_customer = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
|
||||
tax_details = frappe.db.sql("""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_so_dn_map = {}
|
||||
for d in si_items:
|
||||
@@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, cost_center, warehouse
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_cc_wh_map = {}
|
||||
for d in si_items:
|
||||
|
||||
@@ -75,10 +75,11 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
select voucher_no, credit
|
||||
from `tabGL Entry`
|
||||
where party in (%s) and credit > 0
|
||||
and company=%s and posting_date between %s and %s
|
||||
and company=%s and is_cancelled = 0
|
||||
and posting_date between %s and %s
|
||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
||||
|
||||
supplier_credit_amount = flt(sum([d.credit for d in entries]))
|
||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
||||
vouchers = [d.voucher_no for d in entries]
|
||||
vouchers += get_advance_vouchers([supplier], company=company,
|
||||
@@ -91,7 +92,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
from `tabGL Entry`
|
||||
where account=%s and posting_date between %s and %s
|
||||
and company=%s and credit > 0 and voucher_no in ({0})
|
||||
""".format(', '.join(["'%s'" % d for d in vouchers])),
|
||||
""".format(', '.join("'%s'" % d for d in vouchers)),
|
||||
(account, from_date, to_date, company))[0][0])
|
||||
|
||||
date_range_filter = [fiscal_year, from_date, to_date]
|
||||
|
||||
@@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N
|
||||
gross_profit_data = GrossProfitGenerator(filters)
|
||||
result = gross_profit_data.grouped_data
|
||||
if not with_item_data:
|
||||
result = sum([d.gross_profit for d in result])
|
||||
result = sum(d.gross_profit for d in result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
"total_amount": d.grand_total,
|
||||
"outstanding_amount": d.outstanding_amount,
|
||||
"allocated_amount": d.allocated_amount,
|
||||
"exchange_rate": d.exchange_rate
|
||||
"exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation
|
||||
}
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
payment_entry.set_amounts()
|
||||
|
||||
if d.difference_amount and d.difference_account:
|
||||
payment_entry.set_gain_or_loss(account_details={
|
||||
account_details = {
|
||||
'account': d.difference_account,
|
||||
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
|
||||
payment_entry.company, "cost_center"),
|
||||
'amount': d.difference_amount
|
||||
})
|
||||
payment_entry.company, "cost_center")
|
||||
}
|
||||
if d.difference_amount:
|
||||
account_details['amount'] = d.difference_amount
|
||||
|
||||
payment_entry.set_gain_or_loss(account_details=account_details)
|
||||
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
@@ -549,10 +553,14 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
|
||||
and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no))
|
||||
|
||||
for pe in linked_pe:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_total_allocated_amount()
|
||||
pe_doc.set_unallocated_amount()
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.validate(on_reference_unlink=True)
|
||||
except Exception as e:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += '<br>'
|
||||
msg += _("Please cancel payment entry manually first and then resubmit")
|
||||
frappe.throw(msg, title=_("Payment Unlink Error"))
|
||||
|
||||
frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s,
|
||||
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
|
||||
@@ -635,7 +643,7 @@ def get_held_invoices(party_type, party):
|
||||
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
|
||||
as_dict=1
|
||||
)
|
||||
held_invoices = set([d['name'] for d in held_invoices])
|
||||
held_invoices = set(d['name'] for d in held_invoices)
|
||||
|
||||
return held_invoices
|
||||
|
||||
@@ -784,7 +792,7 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
return acc
|
||||
|
||||
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
if not company:
|
||||
@@ -962,7 +970,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
for e in existing_gle:
|
||||
if entry.account == e.account:
|
||||
account_existed = True
|
||||
if (entry.account == e.account and entry.against_account == e.against_account
|
||||
if (entry.account == e.account
|
||||
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
|
||||
and ( flt(entry.debit, precision) != flt(e.debit, precision) or
|
||||
flt(entry.credit, precision) != flt(e.credit, precision))):
|
||||
|
||||
@@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
|
||||
})
|
||||
asset.insert()
|
||||
accumulated_depreciation_after_full_schedule = \
|
||||
max([d.accumulated_depreciation_amount for d in asset.get("schedules")])
|
||||
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
|
||||
|
||||
asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
|
||||
flt(accumulated_depreciation_after_full_schedule))
|
||||
|
||||
@@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
|
||||
d.value_after_depreciation = asset_value
|
||||
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx])
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
from_date = self.date
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"maintain_same_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@@ -108,6 +109,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -115,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-04 20:01:44.087066",
|
||||
"modified": "2021-06-24 10:38:28.934525",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
|
||||
} else {
|
||||
frm.set_value("tax_withholding_category", frm.supplier_tds);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: { purchase_order: frm.doc.name, po_details: po_details },
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
},
|
||||
|
||||
has_unsupplied_items: function() {
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty)
|
||||
},
|
||||
|
||||
make_stock_entry: function() {
|
||||
@@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.utils.add_comment",
|
||||
args: {
|
||||
reference_doctype: me.frm.doctype,
|
||||
reference_name: me.frm.docname,
|
||||
content: __('Reason for hold:') + " " +data.reason_for_hold,
|
||||
content: __(reason_for_hold),
|
||||
comment_email: frappe.session.user,
|
||||
comment_by: frappe.session.user_fullname
|
||||
},
|
||||
|
||||
@@ -609,6 +609,7 @@
|
||||
"fieldname": "supplied_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplied Items",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "po_raw_material_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Order Item Supplied",
|
||||
@@ -1377,7 +1378,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 00:55:30.781375",
|
||||
"modified": "2021-05-30 15:17:53.663648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
|
||||
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
|
||||
unlink_inter_company_doc
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
|
||||
update_linked_doc, unlink_inter_company_doc)
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@@ -139,7 +138,7 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
def validate_minimum_order_qty(self):
|
||||
if not self.get("items"): return
|
||||
items = list(set([d.item_code for d in self.get("items")]))
|
||||
items = list(set(d.item_code for d in self.get("items")))
|
||||
|
||||
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
|
||||
from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
|
||||
@@ -326,10 +325,10 @@ class PurchaseOrder(BuyingController):
|
||||
so.notify_update()
|
||||
|
||||
def has_drop_ship_item(self):
|
||||
return any([d.delivered_by_supplier for d in self.items])
|
||||
return any(d.delivered_by_supplier for d in self.items)
|
||||
|
||||
def is_against_so(self):
|
||||
return any([d.sales_order for d in self.items if d.sales_order])
|
||||
return any(d.sales_order for d in self.items if d.sales_order)
|
||||
|
||||
def set_received_qty_for_drop_ship_items(self):
|
||||
for item in self.items:
|
||||
@@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(purchase_order, rm_items):
|
||||
if isinstance(rm_items, string_types):
|
||||
rm_items_list = rm_items
|
||||
|
||||
if isinstance(rm_items, str):
|
||||
rm_items_list = json.loads(rm_items)
|
||||
else:
|
||||
elif not rm_items:
|
||||
frappe.throw(_("No Items available for transfer"))
|
||||
|
||||
if rm_items_list:
|
||||
@@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
||||
'qty': rm_item_data["qty"],
|
||||
'from_warehouse': rm_item_data["warehouse"],
|
||||
'stock_uom': rm_item_data["stock_uom"],
|
||||
'serial_no': rm_item_data.get('serial_no'),
|
||||
'batch_no': rm_item_data.get('batch_no'),
|
||||
'main_item_code': rm_item_data["item_code"],
|
||||
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
|
||||
}
|
||||
@@ -582,3 +585,58 @@ def update_status(status, name):
|
||||
def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(purchase_order, po_details):
|
||||
if isinstance(po_details, str):
|
||||
po_details = json.loads(po_details)
|
||||
|
||||
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
|
||||
doc.initialized_fields()
|
||||
doc.purchase_orders = [doc.name]
|
||||
doc.get_available_materials()
|
||||
|
||||
if not doc.available_materials:
|
||||
frappe.throw(_('Materials are already received against the purchase order {0}')
|
||||
.format(purchase_order))
|
||||
|
||||
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
|
||||
|
||||
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
|
||||
ste_doc = frappe.new_doc('Stock Entry')
|
||||
ste_doc.purpose = 'Material Transfer'
|
||||
ste_doc.purchase_order = po_doc.name
|
||||
ste_doc.company = po_doc.company
|
||||
ste_doc.is_return = 1
|
||||
|
||||
for key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
ste_doc.calculate_rate_and_amount()
|
||||
|
||||
return ste_doc
|
||||
|
||||
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
|
||||
item = ste_doc.append('items', row.item_details)
|
||||
|
||||
po_detail = list(set(row.po_details).intersection(po_details))
|
||||
item.update({
|
||||
'qty': qty,
|
||||
'batch_no': batch_no,
|
||||
'basic_rate': row.item_details['rate'],
|
||||
'po_detail': po_detail[0] if po_detail else '',
|
||||
's_warehouse': row.item_details['t_warehouse'],
|
||||
't_warehouse': row.item_details['s_warehouse'],
|
||||
'item_code': row.item_details['rm_item_code'],
|
||||
'subcontracted_item': row.item_details['main_item_code'],
|
||||
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
|
||||
})
|
||||
@@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
@@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
||||
po.reload()
|
||||
|
||||
total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
|
||||
total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items"))
|
||||
|
||||
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
|
||||
|
||||
@@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
item_code = "_Test Subcontracted FG Item 11"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
@@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
for item in rm_items:
|
||||
transferred_rm_map[item.get('rm_item_code')] = item
|
||||
|
||||
for item in pr.get('supplied_items'):
|
||||
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_backflushed_based_on_for_multiple_batches(self):
|
||||
item_code = "_Test Subcontracted FG Item 2"
|
||||
make_item('Sub Contracted Raw Material 2', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
|
||||
raw_materials=["Sub Contracted Raw Material 2"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
|
||||
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
|
||||
make_new_batch(batch_id=batch, item_code=item_code)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
# partial receipt
|
||||
pr.get('items')[0].qty = 30
|
||||
pr.get('items')[0].batch_no = "ABCD1"
|
||||
|
||||
purchase_order = po.name
|
||||
purchase_order_item = po.items[0].name
|
||||
|
||||
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
|
||||
pr.append("items", {
|
||||
"item_code": pr.get('items')[0].item_code,
|
||||
"item_name": pr.get('items')[0].item_name,
|
||||
"uom": pr.get('items')[0].uom,
|
||||
"stock_uom": pr.get('items')[0].stock_uom,
|
||||
"warehouse": pr.get('items')[0].warehouse,
|
||||
"conversion_factor": pr.get('items')[0].conversion_factor,
|
||||
"cost_center": pr.get('items')[0].cost_center,
|
||||
"rate": pr.get('items')[0].rate,
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"purchase_order": purchase_order,
|
||||
"purchase_order_item": purchase_order_item
|
||||
})
|
||||
|
||||
pr.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.get('items')[0].qty = 300
|
||||
pr1.get('items')[0].batch_no = "ABCD1"
|
||||
pr1.save()
|
||||
|
||||
pr_key = ("Sub Contracted Raw Material 2", po.name)
|
||||
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
|
||||
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_supplied_qty_against_subcontracted_po(self):
|
||||
@@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
|
||||
po.conversion_factor = args.conversion_factor or 1
|
||||
po.supplier_warehouse = args.supplier_warehouse or None
|
||||
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
if args.rm_items:
|
||||
for row in args.rm_items:
|
||||
po.append("items", row)
|
||||
else:
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
supp_items = po.get("supplied_items")
|
||||
for d in supp_items:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
if not d.reserve_warehouse:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
po.submit()
|
||||
|
||||
return po
|
||||
|
||||
@@ -97,6 +97,9 @@
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"section_break_72",
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
@@ -803,13 +806,37 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Production Plan",
|
||||
"options": "Production Plan",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Item",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Sub Assembly Item",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-22 11:46:12.357435",
|
||||
"modified": "2021-06-28 19:22:22.715365",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -6,21 +6,25 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"bom_detail_no",
|
||||
"rm_item_code",
|
||||
"column_break_3",
|
||||
"stock_uom",
|
||||
"reserve_warehouse",
|
||||
"conversion_factor",
|
||||
"column_break_6",
|
||||
"rm_item_code",
|
||||
"bom_detail_no",
|
||||
"reference_name",
|
||||
"reserve_warehouse",
|
||||
"section_break2",
|
||||
"rate",
|
||||
"col_break2",
|
||||
"amount",
|
||||
"section_break1",
|
||||
"required_qty",
|
||||
"supplied_qty",
|
||||
"col_break1",
|
||||
"supplied_qty"
|
||||
"consumed_qty",
|
||||
"returned_qty",
|
||||
"total_supplied_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -125,6 +129,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -142,13 +148,42 @@
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Total Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-09 15:17:58.128242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item Supplied",
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"description",
|
||||
"rm_item_code",
|
||||
"item_name",
|
||||
"bom_detail_no",
|
||||
"col_break1",
|
||||
"rm_item_code",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"reference_name",
|
||||
@@ -25,7 +26,8 @@
|
||||
"secbreak_3",
|
||||
"batch_no",
|
||||
"col_break4",
|
||||
"serial_no"
|
||||
"serial_no",
|
||||
"purchase_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -52,7 +54,6 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -81,18 +82,20 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"label": "Available Qty For Consumption",
|
||||
"oldfieldname": "required_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty to Be Consumed",
|
||||
"oldfieldname": "consumed_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -183,12 +186,28 @@
|
||||
{
|
||||
"fieldname": "col_break4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Order",
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-19 19:33:04.431213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Receipt Item Supplied",
|
||||
|
||||
@@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
sq_doc.append('items', {
|
||||
"item_code": data.item_code,
|
||||
"item_name": data.item_name,
|
||||
"description": data.description,
|
||||
"qty": data.qty,
|
||||
"rate": data.rate,
|
||||
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
|
||||
"warehouse": data.warehouse or '',
|
||||
args = {}
|
||||
|
||||
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
|
||||
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update({
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier",
|
||||
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
|
||||
})
|
||||
|
||||
sq_doc.append('items', args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
@@ -391,7 +393,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
|
||||
def get_supplier_tag():
|
||||
if not frappe.cache().hget("Supplier", "Tags"):
|
||||
filters = {"document_type": "Supplier"}
|
||||
tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
|
||||
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
||||
frappe.cache().hset("Supplier", "Tags", tags)
|
||||
|
||||
return frappe.cache().hget("Supplier", "Tags")
|
||||
|
||||
@@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", {
|
||||
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
|
||||
}, __('Create'));
|
||||
|
||||
frm.add_custom_button(__('Get Supplier Group Details'), function () {
|
||||
frm.trigger("get_supplier_group_details");
|
||||
}, __('Actions'));
|
||||
|
||||
// indicators
|
||||
erpnext.utils.set_party_dashboard_indicators(frm);
|
||||
}
|
||||
},
|
||||
get_supplier_group_details: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_supplier_group_details",
|
||||
doc: frm.doc,
|
||||
callback: function() {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
is_internal_supplier: function(frm) {
|
||||
if (frm.doc.is_internal_supplier == 1) {
|
||||
|
||||
@@ -51,6 +51,23 @@ class Supplier(TransactionBase):
|
||||
validate_party_accounts(self)
|
||||
self.validate_internal_supplier()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supplier_group_details(self):
|
||||
doc = frappe.get_doc('Supplier Group', self.supplier_group)
|
||||
self.payment_terms = ""
|
||||
self.accounts = []
|
||||
|
||||
if doc.accounts:
|
||||
for account in doc.accounts:
|
||||
child = self.append('accounts')
|
||||
child.company = account.company
|
||||
child.account = account.account
|
||||
|
||||
if doc.payment_terms:
|
||||
self.payment_terms = doc.payment_terms
|
||||
|
||||
self.save()
|
||||
|
||||
def validate_internal_supplier(self):
|
||||
internal_supplier = frappe.db.get_value("Supplier",
|
||||
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
|
||||
@@ -86,4 +103,4 @@ class Supplier(TransactionBase):
|
||||
create_contact(supplier, 'Supplier',
|
||||
doc.name, args.get('supplier_email_' + str(i)))
|
||||
except frappe.NameError:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier')
|
||||
|
||||
|
||||
class TestSupplier(unittest.TestCase):
|
||||
def test_get_supplier_group_details(self):
|
||||
doc = frappe.new_doc("Supplier Group")
|
||||
doc.supplier_group_name = "_Testing Supplier Group"
|
||||
doc.payment_terms = "_Test Payment Term Template 3"
|
||||
doc.accounts = []
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": "Creditors - _TC",
|
||||
}
|
||||
doc.append("accounts", test_account_details)
|
||||
doc.save()
|
||||
s_doc = frappe.new_doc("Supplier")
|
||||
s_doc.supplier_name = "Testing Supplier"
|
||||
s_doc.supplier_group = "_Testing Supplier Group"
|
||||
s_doc.payment_terms = ""
|
||||
s_doc.accounts = []
|
||||
s_doc.insert()
|
||||
s_doc.get_supplier_group_details()
|
||||
self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
|
||||
self.assertEqual(s_doc.accounts[0].company, "_Test Company")
|
||||
self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
|
||||
s_doc.delete()
|
||||
doc.delete()
|
||||
|
||||
def test_supplier_default_payment_terms(self):
|
||||
# Payment Term based on Days after invoice date
|
||||
frappe.db.set_value(
|
||||
@@ -136,4 +160,4 @@ def create_supplier(**args):
|
||||
return doc
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Supplier", args.supplier_name)
|
||||
return frappe.get_doc("Supplier", args.supplier_name)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Subcontract Order Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("From Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Purchase Order"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Order",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
is_subcontracted: 'Yes',
|
||||
company: frappe.query_report.get_filter_value('company')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-05-31 14:43:32.417694",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-05-31 14:43:32.417694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Subcontract Order Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Subcontract Order Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
data = []
|
||||
orders = get_subcontracted_orders(report_filters)
|
||||
|
||||
if orders:
|
||||
supplied_items = get_supplied_items(orders, report_filters)
|
||||
po_details = prepare_subcontracted_data(orders, supplied_items)
|
||||
get_subcontracted_data(po_details, data)
|
||||
|
||||
return data
|
||||
|
||||
def get_subcontracted_orders(report_filters):
|
||||
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
|
||||
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
|
||||
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
|
||||
|
||||
filters = get_filters(report_filters)
|
||||
|
||||
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
|
||||
|
||||
def get_filters(report_filters):
|
||||
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
|
||||
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
|
||||
|
||||
for field in ['name', 'company']:
|
||||
if report_filters.get(field):
|
||||
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
|
||||
|
||||
return filters
|
||||
|
||||
def get_supplied_items(orders, report_filters):
|
||||
if not orders:
|
||||
return []
|
||||
|
||||
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
|
||||
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
|
||||
|
||||
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
|
||||
|
||||
supplied_items = {}
|
||||
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
|
||||
new_key = (row.parent, row.reference_name, row.main_item_code)
|
||||
|
||||
supplied_items.setdefault(new_key, []).append(row)
|
||||
|
||||
return supplied_items
|
||||
|
||||
def prepare_subcontracted_data(orders, supplied_items):
|
||||
po_details = {}
|
||||
for row in orders:
|
||||
key = (row.po_id, row.name, row.item_code)
|
||||
if key not in po_details:
|
||||
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
|
||||
|
||||
details = po_details[key]
|
||||
|
||||
if supplied_items.get(key):
|
||||
for supplied_item in supplied_items[key]:
|
||||
details['supplied_items'].append(supplied_item)
|
||||
|
||||
return po_details
|
||||
|
||||
def get_subcontracted_data(po_details, data):
|
||||
for key, details in po_details.items():
|
||||
res = details.po_item
|
||||
for index, row in enumerate(details.supplied_items):
|
||||
if index != 0:
|
||||
res = {}
|
||||
|
||||
res.update(row)
|
||||
data.append(res)
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldname": "po_id",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"width": 80
|
||||
},
|
||||
{
|
||||
"label": _("Subcontracted Item"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Order Qty"),
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 90
|
||||
},
|
||||
{
|
||||
"label": _("Received Qty"),
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Item"),
|
||||
"fieldname": "rm_item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Required Qty"),
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Qty"),
|
||||
"fieldname": "supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Consumed Qty"),
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": _("Returned Qty"),
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
}
|
||||
]
|
||||
@@ -9,10 +9,10 @@ def execute(filters=None):
|
||||
if filters.from_date >= filters.to_date:
|
||||
frappe.msgprint(_("To Date must be greater than From Date"))
|
||||
|
||||
data = []
|
||||
columns = get_columns()
|
||||
get_data(data , filters)
|
||||
return columns, data
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data or []
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
@@ -21,13 +21,12 @@ def get_columns():
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"width": 150
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Date"),
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "date",
|
||||
"hidden": 1,
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
@@ -41,97 +40,58 @@ def get_columns():
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "rm_item_code",
|
||||
"width": 100
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Required Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "r_qty",
|
||||
"width": 100
|
||||
"fieldname": "reqd_qty",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Transferred Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "t_qty",
|
||||
"width": 100
|
||||
"fieldname": "transferred_qty",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Pending Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "p_qty",
|
||||
"width": 100
|
||||
"width": 150
|
||||
}
|
||||
]
|
||||
|
||||
def get_data(data, filters):
|
||||
po = get_po(filters)
|
||||
po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
|
||||
def get_data(filters):
|
||||
po_rm_item_details = get_po_items_to_supply(filters)
|
||||
|
||||
sub_items = get_purchase_order_item_supplied([v.name for v in po])
|
||||
data = []
|
||||
for row in po_rm_item_details:
|
||||
transferred_qty = row.get("transferred_qty") or 0
|
||||
if transferred_qty < row.get("reqd_qty", 0):
|
||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||
row.p_qty = pending_qty if pending_qty > 0 else 0
|
||||
data.append(row)
|
||||
|
||||
for order in po:
|
||||
for item in sub_items:
|
||||
if order.name == item.parent and order.name in po_transferred_qty_map and \
|
||||
item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code):
|
||||
transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \
|
||||
if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0
|
||||
row ={
|
||||
'purchase_order': item.parent,
|
||||
'date': order.transaction_date,
|
||||
'supplier': order.supplier,
|
||||
'rm_item_code': item.rm_item_code,
|
||||
'r_qty': item.required_qty,
|
||||
't_qty':transferred_qty,
|
||||
'p_qty':item.required_qty - transferred_qty
|
||||
}
|
||||
return data
|
||||
|
||||
data.append(row)
|
||||
|
||||
return(data)
|
||||
|
||||
def get_po(filters):
|
||||
record_filters = [
|
||||
["is_subcontracted", "=", "Yes"],
|
||||
["supplier", "=", filters.supplier],
|
||||
["transaction_date", "<=", filters.to_date],
|
||||
["transaction_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1]
|
||||
]
|
||||
return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"])
|
||||
|
||||
def get_transferred_quantity(po_name):
|
||||
stock_entries = get_stock_entry(po_name)
|
||||
stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries])
|
||||
po_transferred_qty_map = {}
|
||||
|
||||
|
||||
for entry in stock_entries:
|
||||
for details in stock_entries_detail:
|
||||
if details.parent == entry.name:
|
||||
details["Purchase_order"] = entry.purchase_order
|
||||
if entry.purchase_order not in po_transferred_qty_map:
|
||||
po_transferred_qty_map[entry.purchase_order] = {}
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty
|
||||
else:
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty
|
||||
|
||||
return po_transferred_qty_map
|
||||
|
||||
|
||||
def get_stock_entry(po):
|
||||
return frappe.get_all("Stock Entry", filters=[
|
||||
('purchase_order', 'IN', po),
|
||||
('stock_entry_type', '=', 'Send to Subcontractor'),
|
||||
('docstatus', '=', 1)
|
||||
], fields=["name", "purchase_order"])
|
||||
|
||||
def get_stock_entry_detail(se):
|
||||
return frappe.get_all("Stock Entry Detail", filters=[
|
||||
["parent", "in", se]
|
||||
def get_po_items_to_supply(filters):
|
||||
return frappe.db.get_all(
|
||||
"Purchase Order",
|
||||
fields=[
|
||||
"name as purchase_order",
|
||||
"transaction_date as date",
|
||||
"supplier as supplier",
|
||||
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
|
||||
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
|
||||
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
|
||||
],
|
||||
fields=["parent", "item_code", "qty"])
|
||||
|
||||
def get_purchase_order_item_supplied(po):
|
||||
return frappe.get_all("Purchase Order Item Supplied", filters=[
|
||||
('parent', 'IN', po)
|
||||
], fields=['parent', 'rm_item_code', 'required_qty'])
|
||||
filters = [
|
||||
["Purchase Order", "per_received", "<", "100"],
|
||||
["Purchase Order", "is_subcontracted", "=", "Yes"],
|
||||
["Purchase Order", "supplier", "=", filters.supplier],
|
||||
["Purchase Order", "transaction_date", "<=", filters.to_date],
|
||||
["Purchase Order", "transaction_date", ">=", filters.from_date],
|
||||
["Purchase Order", "docstatus", "=", 1]
|
||||
]
|
||||
)
|
||||
@@ -12,34 +12,80 @@ import json, frappe, unittest
|
||||
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
# Material Receipt of RMs
|
||||
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
transfer_subcontracted_raw_materials(po.name)
|
||||
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
|
||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))}))
|
||||
self.assertEqual(data[0]['purchase_order'], po.name)
|
||||
self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[0]['p_qty'], [9, 18])
|
||||
self.assertIn(data[0]['t_qty'], [1, 2])
|
||||
|
||||
self.assertEqual(data[1]['purchase_order'], po.name)
|
||||
self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[1]['p_qty'], [9, 18])
|
||||
self.assertIn(data[1]['t_qty'], [1, 2])
|
||||
se = transfer_subcontracted_raw_materials(po)
|
||||
|
||||
col, data = execute(filters=frappe._dict(
|
||||
{
|
||||
'supplier': po.supplier,
|
||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))
|
||||
}
|
||||
))
|
||||
po.reload()
|
||||
|
||||
po_data = [row for row in data if row.get('purchase_order') == po.name]
|
||||
# Alphabetically sort to be certain of order
|
||||
po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
|
||||
|
||||
self.assertEqual(len(po_data), 2)
|
||||
self.assertEqual(po_data[0]['purchase_order'], po.name)
|
||||
|
||||
self.assertEqual(po_data[0]['rm_item_code'], '_Test Item')
|
||||
self.assertEqual(po_data[0]['p_qty'], 8)
|
||||
self.assertEqual(po_data[0]['transferred_qty'], 2)
|
||||
|
||||
self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100')
|
||||
self.assertEqual(po_data[1]['p_qty'], 19)
|
||||
self.assertEqual(po_data[1]['transferred_qty'], 1)
|
||||
|
||||
se.cancel()
|
||||
po.cancel()
|
||||
|
||||
def transfer_subcontracted_raw_materials(po):
|
||||
# Order of supplied items fetched in PO is flaky
|
||||
transfer_qty_map = {
|
||||
'_Test Item': 2,
|
||||
'_Test Item Home Desktop 100': 1
|
||||
}
|
||||
|
||||
item_1 = po.supplied_items[0].rm_item_code
|
||||
item_2 = po.supplied_items[1].rm_item_code
|
||||
|
||||
rm_item = [
|
||||
{'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'},
|
||||
{'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
|
||||
{
|
||||
'name': po.supplied_items[0].name,
|
||||
'item_code': item_1,
|
||||
'rm_item_code': item_1,
|
||||
'item_name': item_1,
|
||||
'qty': transfer_qty_map[item_1],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_1],
|
||||
'stock_uom': 'Nos'
|
||||
},
|
||||
{
|
||||
'name': po.supplied_items[1].name,
|
||||
'item_code': item_2,
|
||||
'rm_item_code': item_2,
|
||||
'item_name': item_2,
|
||||
'qty': transfer_qty_map[item_2],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_2],
|
||||
'stock_uom': 'Nos'
|
||||
}
|
||||
]
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se.to_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse - _TC'
|
||||
se.to_warehouse = '_Test Warehouse - _TC'
|
||||
se.stock_entry_type = 'Send to Subcontractor'
|
||||
se.save()
|
||||
se.submit()
|
||||
return se
|
||||
|
||||
72
erpnext/change_log/v13/v13_6_0.md
Normal file
72
erpnext/change_log/v13/v13_6_0.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Version 13.6.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
|
||||
- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
|
||||
- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
|
||||
- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
|
||||
- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
|
||||
- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
|
||||
- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
|
||||
- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
|
||||
- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
|
||||
- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
|
||||
- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
|
||||
- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
|
||||
- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
|
||||
- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
|
||||
- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
|
||||
- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
|
||||
- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
|
||||
- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
|
||||
- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
|
||||
- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
|
||||
- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
|
||||
- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
|
||||
- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
|
||||
- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
|
||||
- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
|
||||
- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
|
||||
- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
|
||||
- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
|
||||
- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
|
||||
- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
|
||||
- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
|
||||
- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
|
||||
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
|
||||
- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
|
||||
- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
|
||||
- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
|
||||
- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
|
||||
- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
|
||||
- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
|
||||
- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
|
||||
- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
|
||||
- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
|
||||
- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
|
||||
- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
|
||||
- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
|
||||
- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
|
||||
- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
|
||||
- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
|
||||
- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
|
||||
- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
|
||||
- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
|
||||
- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
|
||||
- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
|
||||
- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
|
||||
- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
|
||||
- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
|
||||
- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
|
||||
- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
|
||||
- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
|
||||
- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
|
||||
- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
|
||||
- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
|
||||
- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
|
||||
- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))
|
||||
69
erpnext/change_log/v13/v13_7_0.md
Normal file
69
erpnext/change_log/v13/v13_7_0.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Version 13.7.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133))
|
||||
- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415))
|
||||
- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454))
|
||||
- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240))
|
||||
- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432))
|
||||
|
||||
### Fixes
|
||||
- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277))
|
||||
- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218))
|
||||
- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488))
|
||||
- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358))
|
||||
- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394))
|
||||
- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287))
|
||||
- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253))
|
||||
- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497))
|
||||
- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353))
|
||||
- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333))
|
||||
- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331))
|
||||
- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271))
|
||||
- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927))
|
||||
- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886))
|
||||
- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368))
|
||||
- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486))
|
||||
- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231))
|
||||
- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829))
|
||||
- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462))
|
||||
- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388))
|
||||
- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493))
|
||||
- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298))
|
||||
- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507))
|
||||
- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259))
|
||||
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
|
||||
- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165))
|
||||
- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468))
|
||||
- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472))
|
||||
- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158))
|
||||
- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279))
|
||||
- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252))
|
||||
- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405))
|
||||
- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323))
|
||||
- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470))
|
||||
- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320))
|
||||
- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466))
|
||||
- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334))
|
||||
- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411))
|
||||
- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382))
|
||||
- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350))
|
||||
- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300))
|
||||
- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255))
|
||||
- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312))
|
||||
- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290))
|
||||
- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332))
|
||||
- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284))
|
||||
- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226))
|
||||
- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349))
|
||||
- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241))
|
||||
- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367))
|
||||
- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391))
|
||||
- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244))
|
||||
- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458))
|
||||
- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509))
|
||||
- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303))
|
||||
- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213))
|
||||
- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278))
|
||||
- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461))
|
||||
- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285))
|
||||
39
erpnext/change_log/v13/v13_8_0.md
Normal file
39
erpnext/change_log/v13/v13_8_0.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Version 13.8.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
|
||||
- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
|
||||
- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
|
||||
|
||||
### Fixes
|
||||
- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
|
||||
- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
|
||||
- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
|
||||
- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
|
||||
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
|
||||
- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
|
||||
- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
|
||||
- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
|
||||
- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
|
||||
- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
|
||||
- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
|
||||
- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
|
||||
- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
|
||||
- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
|
||||
- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
|
||||
- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
|
||||
- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
|
||||
- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
|
||||
- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
|
||||
- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
|
||||
- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
|
||||
- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
|
||||
- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
|
||||
- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
|
||||
- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
|
||||
- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
|
||||
- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
|
||||
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
|
||||
- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
|
||||
- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
|
||||
- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))
|
||||
@@ -124,6 +124,8 @@ class AccountsController(TransactionBase):
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
@@ -584,15 +586,18 @@ class AccountsController(TransactionBase):
|
||||
allocated_amount = min(amount - advance_allocated, d.amount)
|
||||
advance_allocated += flt(allocated_amount)
|
||||
|
||||
self.append("advances", {
|
||||
advance_row = {
|
||||
"doctype": self.doctype + " Advance",
|
||||
"reference_type": d.reference_type,
|
||||
"reference_name": d.reference_name,
|
||||
"reference_row": d.reference_row,
|
||||
"remarks": d.remarks,
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount
|
||||
})
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry
|
||||
}
|
||||
|
||||
self.append("advances", advance_row)
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
if self.doctype == "Sales Invoice":
|
||||
@@ -610,8 +615,8 @@ class AccountsController(TransactionBase):
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(party_type, party, party_account,
|
||||
amount_field, order_doctype, order_list, include_unallocated)
|
||||
@@ -635,8 +640,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
if not order_list: return
|
||||
|
||||
@@ -650,6 +655,71 @@ class AccountsController(TransactionBase):
|
||||
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
|
||||
.format(d.reference_name, d.against_order))
|
||||
|
||||
def set_advance_gain_or_loss(self):
|
||||
if not self.get("advances"):
|
||||
return
|
||||
|
||||
for d in self.get("advances"):
|
||||
advance_exchange_rate = d.ref_exchange_rate
|
||||
if (d.allocated_amount and self.conversion_rate != 1
|
||||
and self.conversion_rate != advance_exchange_rate):
|
||||
|
||||
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
|
||||
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
|
||||
difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
|
||||
|
||||
d.exchange_gain_loss = difference
|
||||
|
||||
def make_exchange_gain_loss_gl_entries(self, gl_entries):
|
||||
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
|
||||
for d in self.get("advances"):
|
||||
if d.exchange_gain_loss:
|
||||
is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
|
||||
party = self.supplier if is_purchase_invoice else self.customer
|
||||
party_account = self.credit_to if is_purchase_invoice else self.debit_to
|
||||
party_type = "Supplier" if is_purchase_invoice else "Customer"
|
||||
|
||||
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
||||
if not gain_loss_account:
|
||||
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
|
||||
.format(self.get('company')))
|
||||
account_currency = get_account_currency(gain_loss_account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
|
||||
|
||||
# for purchase
|
||||
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
|
||||
if not is_purchase_invoice:
|
||||
# just reverse for sales?
|
||||
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": gain_loss_account,
|
||||
"account_currency": account_currency,
|
||||
"against": party,
|
||||
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
||||
dr_or_cr: abs(d.exchange_gain_loss),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project
|
||||
}, item=d)
|
||||
)
|
||||
|
||||
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"against": gain_loss_account,
|
||||
dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
|
||||
dr_or_cr: abs(d.exchange_gain_loss),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project
|
||||
}, self.party_account_currency, item=self)
|
||||
)
|
||||
|
||||
def update_against_document_in_jv(self):
|
||||
"""
|
||||
Links invoice and advance voucher:
|
||||
@@ -690,7 +760,9 @@ class AccountsController(TransactionBase):
|
||||
if self.party_account_currency != self.company_currency else 1),
|
||||
'grand_total': (self.base_grand_total
|
||||
if self.party_account_currency == self.company_currency else self.grand_total),
|
||||
'outstanding_amount': self.outstanding_amount
|
||||
'outstanding_amount': self.outstanding_amount,
|
||||
'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'),
|
||||
'exchange_gain_loss': flt(d.get('exchange_gain_loss'))
|
||||
})
|
||||
lst.append(args)
|
||||
|
||||
@@ -741,6 +813,89 @@ class AccountsController(TransactionBase):
|
||||
tax_map[tax.account_head] -= allocated_amount
|
||||
allocated_tax_map[tax.account_head] -= allocated_amount
|
||||
|
||||
def get_amount_and_base_amount(self, item, enable_discount_accounting):
|
||||
amount = item.net_amount
|
||||
base_amount = item.base_net_amount
|
||||
|
||||
if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'):
|
||||
amount = item.amount
|
||||
base_amount = item.base_amount
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
def get_tax_amounts(self, tax, enable_discount_accounting):
|
||||
amount = tax.tax_amount_after_discount_amount
|
||||
base_amount = tax.base_tax_amount_after_discount_amount
|
||||
|
||||
if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account') \
|
||||
and self.get('apply_discount_on') == 'Grand Total':
|
||||
amount = tax.tax_amount
|
||||
base_amount = tax.base_tax_amount
|
||||
|
||||
return amount, base_amount
|
||||
|
||||
def make_discount_gl_entries(self, gl_entries):
|
||||
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||
|
||||
if enable_discount_accounting:
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit"
|
||||
rev_dr_cr = "debit"
|
||||
supplier_or_customer = self.supplier
|
||||
|
||||
else:
|
||||
dr_or_cr = "debit"
|
||||
rev_dr_cr = "credit"
|
||||
supplier_or_customer = self.customer
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get('discount_amount') and item.get('discount_account'):
|
||||
discount_amount = item.discount_amount * item.qty
|
||||
if self.doctype == "Purchase Invoice":
|
||||
income_or_expense_account = (item.expense_account
|
||||
if (not item.enable_deferred_expense or self.is_return)
|
||||
else item.deferred_expense_account)
|
||||
else:
|
||||
income_or_expense_account = (item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account)
|
||||
|
||||
account_currency = get_account_currency(item.discount_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": item.discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: flt(discount_amount, item.precision('discount_amount')),
|
||||
dr_or_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'),
|
||||
item.precision('discount_amount')),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project
|
||||
}, account_currency, item=item)
|
||||
)
|
||||
|
||||
account_currency = get_account_currency(income_or_expense_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": income_or_expense_account,
|
||||
"against": supplier_or_customer,
|
||||
rev_dr_cr: flt(discount_amount, item.precision('discount_amount')),
|
||||
rev_dr_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'),
|
||||
item.precision('discount_amount')),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project
|
||||
}, account_currency, item=item)
|
||||
)
|
||||
|
||||
if self.get('discount_amount') and self.get('additional_discount_account'):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.additional_discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: self.discount_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
|
||||
def allocate_advance_taxes(self, gl_entries):
|
||||
tax_map = self.get_tax_map()
|
||||
for pe in self.get("advances"):
|
||||
@@ -751,11 +906,11 @@ class AccountsController(TransactionBase):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
else:
|
||||
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
else:
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
|
||||
|
||||
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
||||
unallocated_amount = tax.tax_amount - tax.allocated_amount
|
||||
@@ -828,8 +983,14 @@ class AccountsController(TransactionBase):
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
def get_company_default(self, fieldname):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
@@ -1039,8 +1200,11 @@ class AccountsController(TransactionBase):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
|
||||
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
|
||||
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
|
||||
|
||||
|
||||
def set_due_date(self):
|
||||
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
|
||||
@@ -1283,6 +1447,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
|
||||
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
|
||||
payment_type = "Receive" if party_type == "Customer" else "Pay"
|
||||
exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
|
||||
|
||||
payment_entries_against_order, unallocated_payment_entries = [], []
|
||||
limit_cond = "limit %s" % limit if limit else ""
|
||||
|
||||
@@ -1299,27 +1465,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
"Payment Entry" as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t1.posting_date,
|
||||
t1.{0} as currency
|
||||
t1.{0} as currency, t1.{4} as exchange_rate
|
||||
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
|
||||
where
|
||||
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
|
||||
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
|
||||
and t2.reference_doctype = %s {2}
|
||||
order by t1.posting_date {3}
|
||||
""".format(currency_field, party_account_field, reference_condition, limit_cond),
|
||||
""".format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field),
|
||||
[party_account, payment_type, party_type, party,
|
||||
order_doctype] + order_list, as_dict=1)
|
||||
|
||||
if include_unallocated:
|
||||
unallocated_payment_entries = frappe.db.sql("""
|
||||
select "Payment Entry" as reference_type, name as reference_name,
|
||||
remarks, unallocated_amount as amount
|
||||
remarks, unallocated_amount as amount, {2} as exchange_rate
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
{0} = %s and party_type = %s and party = %s and payment_type = %s
|
||||
and docstatus = 1 and unallocated_amount > 0
|
||||
order by posting_date {1}
|
||||
""".format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1)
|
||||
""".format(party_account_field, limit_cond, exchange_rate_field),
|
||||
(party_account, party_type, party, payment_type), as_dict=1)
|
||||
|
||||
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
||||
|
||||
|
||||
@@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
class BuyingController(StockController):
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting import Subcontracting
|
||||
|
||||
class BuyingController(StockController, Subcontracting):
|
||||
|
||||
def get_feed(self):
|
||||
if self.get("supplier_name"):
|
||||
@@ -57,6 +58,11 @@ class BuyingController(StockController):
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_valuation_rate()
|
||||
|
||||
def onload(self):
|
||||
super(BuyingController, self).onload()
|
||||
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
|
||||
'backflush_raw_materials_of_subcontract_based_on'))
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -171,18 +177,19 @@ class BuyingController(StockController):
|
||||
|
||||
TODO: rename item_tax_amount to valuation_tax_amount
|
||||
"""
|
||||
stock_and_asset_items = []
|
||||
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
|
||||
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
for d in self.get("items"):
|
||||
if d.item_code and d.item_code in stock_and_asset_items:
|
||||
if (d.item_code and d.item_code in stock_and_asset_items):
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
last_item_idx = d.idx
|
||||
|
||||
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"]])
|
||||
total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"])
|
||||
|
||||
valuation_amount_adjustment = total_valuation_amount
|
||||
for i, item in enumerate(self.get("items")):
|
||||
@@ -255,7 +262,7 @@ class BuyingController(StockController):
|
||||
supplied_items_cost = 0.0
|
||||
for d in self.get("supplied_items"):
|
||||
if d.reference_name == item_row_id:
|
||||
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
rate = get_incoming_rate({
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
@@ -285,11 +292,13 @@ class BuyingController(StockController):
|
||||
if item in self.sub_contracted_items and not item.bom:
|
||||
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
|
||||
|
||||
if self.doctype == "Purchase Order":
|
||||
for supplied_item in self.get("supplied_items"):
|
||||
if not supplied_item.reserve_warehouse:
|
||||
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
|
||||
if self.doctype != "Purchase Order":
|
||||
return
|
||||
|
||||
for row in self.get("supplied_items"):
|
||||
if not row.reserve_warehouse:
|
||||
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
|
||||
frappe.throw(_(msg))
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if item.bom:
|
||||
@@ -297,23 +306,7 @@ class BuyingController(StockController):
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table):
|
||||
if self.is_subcontracted=="Yes":
|
||||
parent_items = []
|
||||
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on")
|
||||
if (self.doctype == 'Purchase Receipt' and
|
||||
backflush_raw_materials_based_on != 'BOM'):
|
||||
self.update_raw_materials_supplied_based_on_stock_entries()
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
item.rm_supp_cost = 0.0
|
||||
if item.bom and item.item_code in self.sub_contracted_items:
|
||||
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
|
||||
|
||||
if [item.item_code, item.name] not in parent_items:
|
||||
parent_items.append([item.item_code, item.name])
|
||||
|
||||
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
for item in self.get("items"):
|
||||
@@ -322,176 +315,6 @@ class BuyingController(StockController):
|
||||
if self.is_subcontracted == "No" and self.get("supplied_items"):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
def update_raw_materials_supplied_based_on_stock_entries(self):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
purchase_orders = set([d.purchase_order for d in self.items])
|
||||
|
||||
# qty of raw materials backflushed (for each item per purchase order)
|
||||
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
|
||||
|
||||
# qty of "finished good" item yet to be received
|
||||
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
|
||||
|
||||
for item in self.get('items'):
|
||||
if not item.purchase_order:
|
||||
continue
|
||||
|
||||
# reset raw_material cost
|
||||
item.rm_supp_cost = 0
|
||||
|
||||
# qty of raw materials transferred to the supplier
|
||||
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
|
||||
|
||||
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
|
||||
|
||||
item_key = '{}{}'.format(item.item_code, item.purchase_order)
|
||||
|
||||
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
|
||||
|
||||
if not fg_yet_to_be_received:
|
||||
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
|
||||
.format(item.idx, frappe.bold(item.item_code),
|
||||
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
|
||||
title=_("Limit Crossed"))
|
||||
|
||||
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
|
||||
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||
|
||||
for raw_material in transferred_raw_materials + non_stock_items:
|
||||
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
|
||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||
|
||||
consumed_qty = raw_material_data.get('qty', 0)
|
||||
consumed_serial_nos = raw_material_data.get('serial_no', '')
|
||||
consumed_batch_nos = raw_material_data.get('batch_nos', '')
|
||||
|
||||
transferred_qty = raw_material.qty
|
||||
|
||||
rm_qty_to_be_consumed = transferred_qty - consumed_qty
|
||||
|
||||
# backflush all remaining transferred qty in the last Purchase Receipt
|
||||
if fg_yet_to_be_received == item.qty:
|
||||
qty = rm_qty_to_be_consumed
|
||||
else:
|
||||
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
|
||||
|
||||
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
|
||||
qty = frappe.utils.ceil(qty)
|
||||
|
||||
if qty > rm_qty_to_be_consumed:
|
||||
qty = rm_qty_to_be_consumed
|
||||
|
||||
if not qty: continue
|
||||
|
||||
if raw_material.serial_nos:
|
||||
set_serial_nos(raw_material, consumed_serial_nos, qty)
|
||||
|
||||
if raw_material.batch_nos:
|
||||
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
|
||||
|
||||
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
if qty > 0:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
else:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
|
||||
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
|
||||
rm = self.append('supplied_items', {})
|
||||
rm.update(raw_material_data)
|
||||
|
||||
if not rm.main_item_code:
|
||||
rm.main_item_code = fg_item_row.item_code
|
||||
|
||||
rm.reference_name = fg_item_row.name
|
||||
rm.required_qty = qty
|
||||
rm.consumed_qty = qty
|
||||
|
||||
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
|
||||
exploded_item = 1
|
||||
if hasattr(item, 'include_exploded_items'):
|
||||
exploded_item = item.get('include_exploded_items')
|
||||
|
||||
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
|
||||
|
||||
used_alternative_items = []
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
|
||||
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
|
||||
|
||||
raw_materials_cost = 0
|
||||
items = list(set([d.item_code for d in bom_items]))
|
||||
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
|
||||
from `tabItem` i, `tabItem Default` id
|
||||
where id.parent=i.name and id.company=%s and i.name in ({0})"""
|
||||
.format(", ".join(["%s"] * len(items))), [self.company] + items))
|
||||
|
||||
for bom_item in bom_items:
|
||||
if self.doctype == "Purchase Order":
|
||||
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
|
||||
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
|
||||
reserve_warehouse = None
|
||||
|
||||
conversion_factor = item.conversion_factor
|
||||
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
|
||||
bom_item.item_code in used_alternative_items):
|
||||
alternative_item_data = used_alternative_items.get(bom_item.item_code)
|
||||
bom_item.item_code = alternative_item_data.item_code
|
||||
bom_item.item_name = alternative_item_data.item_name
|
||||
bom_item.stock_uom = alternative_item_data.stock_uom
|
||||
conversion_factor = alternative_item_data.conversion_factor
|
||||
bom_item.description = alternative_item_data.description
|
||||
|
||||
# check if exists
|
||||
exists = 0
|
||||
for d in self.get(raw_material_table):
|
||||
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
|
||||
and d.reference_name == item.name:
|
||||
rm, exists = d, 1
|
||||
break
|
||||
|
||||
if not exists:
|
||||
rm = self.append(raw_material_table, {})
|
||||
|
||||
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
|
||||
flt(conversion_factor), rm.precision("required_qty"))
|
||||
rm.reference_name = item.name
|
||||
rm.bom_detail_no = bom_item.name
|
||||
rm.main_item_code = item.item_code
|
||||
rm.rm_item_code = bom_item.item_code
|
||||
rm.stock_uom = bom_item.stock_uom
|
||||
rm.required_qty = required_qty
|
||||
rm.rate = bom_item.rate
|
||||
rm.conversion_factor = conversion_factor
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
rm.consumed_qty = required_qty
|
||||
rm.description = bom_item.description
|
||||
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
|
||||
rm.batch_no = item.batch_no
|
||||
elif not rm.reserve_warehouse:
|
||||
rm.reserve_warehouse = reserve_warehouse
|
||||
|
||||
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
|
||||
"""Remove all those child items which are no longer present in main item table"""
|
||||
delete_list = []
|
||||
for d in self.get(raw_material_table):
|
||||
if [d.main_item_code, d.reference_name] not in parent_items:
|
||||
# mark for deletion from doclist
|
||||
delete_list.append(d)
|
||||
|
||||
# delete from doclist
|
||||
if delete_list:
|
||||
rm_supplied_details = self.get(raw_material_table)
|
||||
self.set(raw_material_table, [])
|
||||
for d in rm_supplied_details:
|
||||
if d not in delete_list:
|
||||
self.append(raw_material_table, d)
|
||||
|
||||
@property
|
||||
def sub_contracted_items(self):
|
||||
if not hasattr(self, "_sub_contracted_items"):
|
||||
@@ -683,7 +506,8 @@ class BuyingController(StockController):
|
||||
self.process_fixed_asset()
|
||||
self.update_fixed_asset(field)
|
||||
|
||||
update_last_purchase_rate(self, is_submit = 1)
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 1)
|
||||
|
||||
def on_cancel(self):
|
||||
super(BuyingController, self).on_cancel()
|
||||
@@ -691,7 +515,9 @@ class BuyingController(StockController):
|
||||
if self.get('is_return'):
|
||||
return
|
||||
|
||||
update_last_purchase_rate(self, is_submit = 0)
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 0)
|
||||
|
||||
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
|
||||
|
||||
@@ -863,104 +689,6 @@ class BuyingController(StockController):
|
||||
else:
|
||||
validate_item_type(self, "is_purchase_item", "purchase")
|
||||
|
||||
|
||||
def get_items_from_bom(item_code, bom, exploded_item=1):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
|
||||
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
|
||||
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
|
||||
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
|
||||
from
|
||||
`tabBOM` t1, `tab{0}` t2, tabItem t3
|
||||
where
|
||||
t2.parent = t1.name and t1.item = %s
|
||||
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
|
||||
and t2.sourced_by_supplier = 0
|
||||
and t2.item_code = t3.name""".format(doctype),
|
||||
(item_code, bom), as_dict=1)
|
||||
|
||||
if not bom_items:
|
||||
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
|
||||
|
||||
return bom_items
|
||||
|
||||
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
common_query = """
|
||||
SELECT
|
||||
sed.item_code AS rm_item_code,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.description,
|
||||
sed.stock_uom,
|
||||
sed.subcontracted_item AS main_item_code,
|
||||
{serial_no_concat_syntax} AS serial_nos,
|
||||
{batch_no_concat_syntax} AS batch_nos
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND IFNULL(sed.t_warehouse, '') != ''
|
||||
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
|
||||
GROUP BY sed.item_code, sed.subcontracted_item
|
||||
"""
|
||||
raw_materials = frappe.db.multisql({
|
||||
'mariadb': common_query.format(
|
||||
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
|
||||
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
|
||||
),
|
||||
'postgres': common_query.format(
|
||||
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
|
||||
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
|
||||
)
|
||||
}, (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
return raw_materials
|
||||
|
||||
def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
||||
purchase_receipts = frappe.get_all("Purchase Receipt Item",
|
||||
fields = ["purchase_order", "item_code", "name", "parent"],
|
||||
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
|
||||
|
||||
distinct_purchase_receipts = {}
|
||||
for pr in purchase_receipts:
|
||||
key = (pr.purchase_order, pr.item_code, pr.parent)
|
||||
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
|
||||
|
||||
backflushed_raw_materials_map = frappe._dict()
|
||||
for args, references in iteritems(distinct_purchase_receipts):
|
||||
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
||||
|
||||
for data in purchase_receipt_supplied_items:
|
||||
pr_key = (data.rm_item_code, data.main_item_code, args[0])
|
||||
if pr_key not in backflushed_raw_materials_map:
|
||||
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
||||
"qty": 0.0,
|
||||
"serial_no": [],
|
||||
"batch_no": [],
|
||||
"consumed_batch": {}
|
||||
}))
|
||||
|
||||
row = backflushed_raw_materials_map.get(pr_key)
|
||||
row.qty += data.consumed_qty
|
||||
|
||||
for field in ["serial_no", "batch_no"]:
|
||||
if data.get(field):
|
||||
row[field].append(data.get(field))
|
||||
|
||||
if data.get("batch_no"):
|
||||
if data.get("batch_no") in row.consumed_batch:
|
||||
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
|
||||
else:
|
||||
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
|
||||
|
||||
return backflushed_raw_materials_map
|
||||
|
||||
def get_supplied_items(item_code, purchase_receipt, references):
|
||||
return frappe.get_all("Purchase Receipt Item Supplied",
|
||||
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
|
||||
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
||||
|
||||
def get_asset_item_details(asset_items):
|
||||
asset_items_data = {}
|
||||
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
|
||||
@@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
|
||||
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
def get_qty_to_be_received(purchase_orders):
|
||||
return frappe._dict(frappe.db.sql("""
|
||||
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
|
||||
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
|
||||
FROM `tabPurchase Order Item` poi
|
||||
WHERE
|
||||
poi.`parent` in %s
|
||||
GROUP BY poi.`item_code`, poi.`parent`
|
||||
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
|
||||
""", (purchase_orders)))
|
||||
|
||||
def get_non_stock_items(purchase_order, fg_item_code):
|
||||
return frappe.db.sql("""
|
||||
SELECT
|
||||
pois.main_item_code,
|
||||
pois.rm_item_code,
|
||||
item.description,
|
||||
pois.required_qty AS qty,
|
||||
pois.rate,
|
||||
1 as non_stock_item,
|
||||
pois.stock_uom
|
||||
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
|
||||
WHERE
|
||||
pois.`rm_item_code` = item.`name`
|
||||
AND item.is_stock_item = 0
|
||||
AND pois.`parent` = %s
|
||||
AND pois.`main_item_code` = %s
|
||||
""", (purchase_order, fg_item_code), as_dict=1)
|
||||
|
||||
|
||||
def set_serial_nos(raw_material, consumed_serial_nos, qty):
|
||||
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
|
||||
set(get_serial_nos(consumed_serial_nos))
|
||||
if serial_nos and qty <= len(serial_nos):
|
||||
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
|
||||
|
||||
def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
transferred_batch_qty_map = {}
|
||||
transferred_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
sed.batch_no,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.item_code,
|
||||
sed.subcontracted_item
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND ifnull(sed.subcontracted_item, '') in ('', %s)
|
||||
AND sed.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
sed.batch_no,
|
||||
sed.item_code
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in transferred_batches:
|
||||
key = ((batch_data.item_code, fg_item)
|
||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
||||
transferred_batch_qty_map.setdefault(key, OrderedDict())
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
|
||||
def get_backflushed_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
backflushed_batch_qty_map = {}
|
||||
backflushed_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
pris.batch_no,
|
||||
SUM(pris.consumed_qty) AS qty,
|
||||
pris.rm_item_code AS item_code
|
||||
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
|
||||
WHERE
|
||||
pr.name = pri.parent
|
||||
AND pri.parent = pris.parent
|
||||
AND pri.purchase_order = %s
|
||||
AND pri.item_code = pris.main_item_code
|
||||
AND pr.docstatus = 1
|
||||
AND pris.main_item_code = %s
|
||||
AND pris.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
pris.rm_item_code, pris.batch_no
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in backflushed_batches:
|
||||
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
|
||||
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return backflushed_batch_qty_map
|
||||
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
|
||||
# Returns available batches to be backflushed based on requirements
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
|
||||
if not transferred_batches:
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
|
||||
|
||||
available_batches = []
|
||||
|
||||
for (batch, transferred_qty) in transferred_batches.items():
|
||||
backflushed_qty = backflushed_batches.get(batch, 0)
|
||||
available_qty = transferred_qty - backflushed_qty
|
||||
|
||||
if available_qty >= required_qty:
|
||||
available_batches.append({'batch': batch, 'qty': required_qty})
|
||||
break
|
||||
elif available_qty != 0:
|
||||
available_batches.append({'batch': batch, 'qty': available_qty})
|
||||
required_qty -= available_qty
|
||||
|
||||
for row in available_batches:
|
||||
if backflushed_batches.get(row.get('batch'), 0) > 0:
|
||||
backflushed_batches[row.get('batch')] += row.get('qty')
|
||||
else:
|
||||
backflushed_batches[row.get('batch')] = row.get('qty')
|
||||
|
||||
return available_batches
|
||||
|
||||
@@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
@@ -88,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Customer", fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabCustomer`
|
||||
where docstatus < 2
|
||||
@@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select {fields} from `tabProject`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
and {cond} {match_cond} {scond}
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
@@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
|
||||
where
|
||||
batch.disabled = 0
|
||||
and sle.is_cancelled = 0
|
||||
and sle.item_code = %(item_code)s
|
||||
and sle.warehouse = %(warehouse)s
|
||||
and (sle.batch_no like %(txt)s
|
||||
|
||||
@@ -99,9 +99,10 @@ def validate_returned_items(doc):
|
||||
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
|
||||
.format(d.idx, s, doc.doctype, doc.return_against))
|
||||
|
||||
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \
|
||||
and not d.get("warehouse"):
|
||||
frappe.throw(_("Warehouse is mandatory"))
|
||||
if (warehouse_mandatory and not d.get("warehouse") and
|
||||
frappe.db.get_value("Item", d.item_code, "is_stock_item")
|
||||
):
|
||||
frappe.throw(_("Warehouse is mandatory"))
|
||||
|
||||
items_returned = True
|
||||
|
||||
@@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc):
|
||||
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
|
||||
serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
return serial_nos
|
||||
return serial_nos
|
||||
|
||||
@@ -330,9 +330,15 @@ class SellingController(StockController):
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
if d.doctype == "Packed Item":
|
||||
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
|
||||
if d.incoming_rate != incoming_rate:
|
||||
d.incoming_rate = incoming_rate
|
||||
else:
|
||||
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
|
||||
d.discount_percentage = 0
|
||||
d.discount_amount = 0
|
||||
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
|
||||
@@ -428,7 +434,7 @@ class SellingController(StockController):
|
||||
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
|
||||
|
||||
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
|
||||
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
|
||||
doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)))
|
||||
if doc_list:
|
||||
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
|
||||
|
||||
|
||||
@@ -299,8 +299,8 @@ class StatusUpdater(Document):
|
||||
args['name'] = self.get(args['percent_join_field_parent'])
|
||||
self._update_percent_field(args, update_modified)
|
||||
else:
|
||||
distinct_transactions = set([d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt'])])
|
||||
distinct_transactions = set(d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt']))
|
||||
|
||||
for name in distinct_transactions:
|
||||
if name:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user