mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-28 15:08:17 +00:00
Compare commits
519 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
975010c987 | ||
|
|
1f6352532e | ||
|
|
aa7c81a0bc | ||
|
|
08729b49e9 | ||
|
|
a02469f09c | ||
|
|
4a8ce226f6 | ||
|
|
6f59fa9e5b | ||
|
|
f39ae9dbb1 | ||
|
|
4add1b4374 | ||
|
|
dffa682b80 | ||
|
|
74ffb1d59c | ||
|
|
2060a003c8 | ||
|
|
9d18b40fb0 | ||
|
|
25b3c7736b | ||
|
|
6a08d04706 | ||
|
|
cf14858909 | ||
|
|
7af03800c9 | ||
|
|
6a21d617ce | ||
|
|
48c2c82dc4 | ||
|
|
8b617fb75e | ||
|
|
79483cc90e | ||
|
|
81ef2babe9 | ||
|
|
043815e745 | ||
|
|
2cf871c21e | ||
|
|
0ccb5a34bd | ||
|
|
8372f038c0 | ||
|
|
a630db1b21 | ||
|
|
a59dffe42a | ||
|
|
32e5bbbb46 | ||
|
|
feb5d0089b | ||
|
|
059141df52 | ||
|
|
29e079d5d6 | ||
|
|
7737b9061f | ||
|
|
2077f6e89c | ||
|
|
dee82754ab | ||
|
|
e5055160fb | ||
|
|
a8ac2a088d | ||
|
|
0dd16fc97d | ||
|
|
35d488d909 | ||
|
|
0e82932de1 | ||
|
|
7798bab0d8 | ||
|
|
6140f13f3a | ||
|
|
c7c1a6e501 | ||
|
|
c7391c8a0f | ||
|
|
d82d1597ac | ||
|
|
307fadc8e2 | ||
|
|
57b502b9de | ||
|
|
5b92334e95 | ||
|
|
c0d82710b7 | ||
|
|
200166bc24 | ||
|
|
4ed701b477 | ||
|
|
9e71f3263d | ||
|
|
20e224224b | ||
|
|
90c79cb24f | ||
|
|
114cb98b8e | ||
|
|
4a62252e7e | ||
|
|
a6a7cc24a4 | ||
|
|
456c336899 | ||
|
|
caf886d4a6 | ||
|
|
e29ce12a58 | ||
|
|
1e09a020a9 | ||
|
|
6835efa132 | ||
|
|
2ee0229751 | ||
|
|
5c9506c8ca | ||
|
|
cc95cedfee | ||
|
|
841d2e4b2c | ||
|
|
5ba5fb1b1c | ||
|
|
9fc798efc0 | ||
|
|
b10dfba931 | ||
|
|
69b214b85b | ||
|
|
10afde75da | ||
|
|
33e8d05718 | ||
|
|
5fd00e7113 | ||
|
|
86801c29cb | ||
|
|
c17e537bb6 | ||
|
|
d231b19b9f | ||
|
|
09d3a98873 | ||
|
|
a6f334a15e | ||
|
|
bd75584c27 | ||
|
|
7b75f454d3 | ||
|
|
37d437a33c | ||
|
|
11440cca4c | ||
|
|
3673104949 | ||
|
|
71e4f34b86 | ||
|
|
0a9c764f31 | ||
|
|
fe1e2fec7a | ||
|
|
5a9452f4a3 | ||
|
|
e251180c1a | ||
|
|
f5b2b80707 | ||
|
|
b3da2f7c26 | ||
|
|
ca6607e800 | ||
|
|
70857bf157 | ||
|
|
dd7e5e019f | ||
|
|
7105648468 | ||
|
|
1ead0a3fef | ||
|
|
9854c84ad8 | ||
|
|
4017342c15 | ||
|
|
b96aa75ded | ||
|
|
1412c63158 | ||
|
|
cc7e267c35 | ||
|
|
b4e481a390 | ||
|
|
5345ebe242 | ||
|
|
19713f9f6f | ||
|
|
0bed06284e | ||
|
|
509b68404c | ||
|
|
6f0c7cf9a4 | ||
|
|
c7628c98c5 | ||
|
|
0c7efae858 | ||
|
|
984e32c34a | ||
|
|
2b75474649 | ||
|
|
eb5cc2cf66 | ||
|
|
10a1681e87 | ||
|
|
54fac5786a | ||
|
|
a59c205d2e | ||
|
|
151b599808 | ||
|
|
eb723637d3 | ||
|
|
aba322e086 | ||
|
|
20ceb6c617 | ||
|
|
591b7705ac | ||
|
|
7163d7d27f | ||
|
|
205899348a | ||
|
|
56ba7d6a8a | ||
|
|
9fcfab219c | ||
|
|
6fe42c937c | ||
|
|
1fe22f0fcf | ||
|
|
746a734257 | ||
|
|
989052c075 | ||
|
|
cd3991dd14 | ||
|
|
e586c07bdd | ||
|
|
8af6a113d1 | ||
|
|
9b9772eb14 | ||
|
|
88ecc933f7 | ||
|
|
6df9b53682 | ||
|
|
47e97e0b3d | ||
|
|
fec6e3724e | ||
|
|
668b092f6b | ||
|
|
fb7d3b7878 | ||
|
|
f6857b4698 | ||
|
|
ce601afc4e | ||
|
|
5b4b71d941 | ||
|
|
1b827f61f7 | ||
|
|
633a1703dc | ||
|
|
abfd975eb6 | ||
|
|
9106b01c04 | ||
|
|
3a0cdf30ce | ||
|
|
4620ed6e42 | ||
|
|
756ac6c587 | ||
|
|
b5b1b2da32 | ||
|
|
cdd378c518 | ||
|
|
14565ed8b1 | ||
|
|
44b290f58e | ||
|
|
d23b93a462 | ||
|
|
80f4a11d60 | ||
|
|
9caa39195c | ||
|
|
879946e2c8 | ||
|
|
bdf81a43c6 | ||
|
|
cb681e0b96 | ||
|
|
65554bdabf | ||
|
|
0d2d45f32f | ||
|
|
2631224e49 | ||
|
|
0a080efce2 | ||
|
|
48884366ea | ||
|
|
15c1af3d8a | ||
|
|
350f75877a | ||
|
|
aa7fede0dc | ||
|
|
e9f2ab5caf | ||
|
|
7076c23a4f | ||
|
|
bdfd682664 | ||
|
|
fb0f82eed3 | ||
|
|
fa9fa97e05 | ||
|
|
c41e1d7d71 | ||
|
|
8f8c0a597b | ||
|
|
6f96e5dcd4 | ||
|
|
f8c58b6893 | ||
|
|
0e7840301f | ||
|
|
cb9b4fbb91 | ||
|
|
1a3b9c5bdf | ||
|
|
50822f207e | ||
|
|
8dcb9302b4 | ||
|
|
19f08afcef | ||
|
|
42037f9f73 | ||
|
|
d9efa662d4 | ||
|
|
ee147e62d5 | ||
|
|
bc88415e73 | ||
|
|
5463f0b137 | ||
|
|
a8fc17e0ae | ||
|
|
0575b105d0 | ||
|
|
4c8dbeddec | ||
|
|
9a8ee62d5a | ||
|
|
3a7c69fc71 | ||
|
|
dd116a3071 | ||
|
|
3f8928be5c | ||
|
|
26928b395b | ||
|
|
fe9e0c2121 | ||
|
|
77cf2e5475 | ||
|
|
2772a911ed | ||
|
|
6d121b8107 | ||
|
|
c30dda3328 | ||
|
|
b2e6fdc0cb | ||
|
|
75af689f77 | ||
|
|
3697e8f1f9 | ||
|
|
1d8050d24d | ||
|
|
897a467846 | ||
|
|
f06a8fd01c | ||
|
|
b139ec6ec0 | ||
|
|
e97eaccdfb | ||
|
|
b02ed0d9a8 | ||
|
|
4206f01c05 | ||
|
|
ab56470171 | ||
|
|
54e822d83a | ||
|
|
8af1e49e96 | ||
|
|
1783594178 | ||
|
|
762a46a5e3 | ||
|
|
19f0676592 | ||
|
|
098603dd35 | ||
|
|
9b2b46737e | ||
|
|
457846e34c | ||
|
|
5a54296686 | ||
|
|
e3c1d736ce | ||
|
|
c8e3ce48e1 | ||
|
|
38e27a68d5 | ||
|
|
e509664d4f | ||
|
|
2fb3659694 | ||
|
|
1145149f0e | ||
|
|
dd20bf931b | ||
|
|
709f94c8d3 | ||
|
|
2933c4f1c5 | ||
|
|
8133be4868 | ||
|
|
c0f9ff4995 | ||
|
|
7b6a1e5184 | ||
|
|
362003ec5f | ||
|
|
30e137e9f2 | ||
|
|
08a4781de7 | ||
|
|
14706d4326 | ||
|
|
fd09d1c4c3 | ||
|
|
afbbf26f15 | ||
|
|
f83fcf5261 | ||
|
|
5879475a00 | ||
|
|
f42225bc82 | ||
|
|
c73b76fdb6 | ||
|
|
e451916803 | ||
|
|
72255fae80 | ||
|
|
42f5888426 | ||
|
|
545f956160 | ||
|
|
1226f3294f | ||
|
|
5c38645560 | ||
|
|
9a755ca23d | ||
|
|
a5a08c9889 | ||
|
|
ed5f39c2c2 | ||
|
|
a1842103b6 | ||
|
|
4d54430010 | ||
|
|
e855866820 | ||
|
|
580641f55c | ||
|
|
816ce879f9 | ||
|
|
736c34e61a | ||
|
|
f4858fbf8a | ||
|
|
b0c042de1b | ||
|
|
c463df4fd1 | ||
|
|
33cd14f859 | ||
|
|
f5f4902494 | ||
|
|
144594b3e1 | ||
|
|
64b416b4dd | ||
|
|
4e83d0baa6 | ||
|
|
a01e2ca9ac | ||
|
|
bf6e1b67a5 | ||
|
|
148342a132 | ||
|
|
d1a91177e5 | ||
|
|
8d6034de16 | ||
|
|
d306bb080a | ||
|
|
eb3e6ff145 | ||
|
|
82c3d862ce | ||
|
|
5eb5bf7102 | ||
|
|
51d9d0a454 | ||
|
|
344c339484 | ||
|
|
bef9dd79e7 | ||
|
|
815696964a | ||
|
|
f0ae02e921 | ||
|
|
f50b4d80f1 | ||
|
|
7ad795bec5 | ||
|
|
662d9cb5aa | ||
|
|
8ac718d98c | ||
|
|
3810b02023 | ||
|
|
25b37737a2 | ||
|
|
0e39e5e868 | ||
|
|
fedde7fe3b | ||
|
|
551c96e1e5 | ||
|
|
78c34d71e2 | ||
|
|
453249d868 | ||
|
|
35ec125b34 | ||
|
|
2f3ed23a9d | ||
|
|
6597c74d6c | ||
|
|
e33fb3b242 | ||
|
|
d844a2b990 | ||
|
|
3ba2b9ed2e | ||
|
|
cca2fcec54 | ||
|
|
80230fec3e | ||
|
|
7021e3adb1 | ||
|
|
69c0d81c55 | ||
|
|
28dfc13dc6 | ||
|
|
5f381cd520 | ||
|
|
8571e6c5d2 | ||
|
|
e4ce6fa195 | ||
|
|
ef3d352a17 | ||
|
|
fc611cf86b | ||
|
|
a5489ee2ac | ||
|
|
85f3a5d318 | ||
|
|
b1473c9932 | ||
|
|
82d8379188 | ||
|
|
39b5147768 | ||
|
|
d732083166 | ||
|
|
693007adfe | ||
|
|
f43ea0d6ff | ||
|
|
f9f42c7e98 | ||
|
|
06f204a8d6 | ||
|
|
47df41fdbd | ||
|
|
878d7477bc | ||
|
|
29aa4a0222 | ||
|
|
5045ad6be6 | ||
|
|
665021270f | ||
|
|
5923a80a0f | ||
|
|
5cc3978c16 | ||
|
|
5630e8189b | ||
|
|
d121282439 | ||
|
|
3c75e55cb9 | ||
|
|
aa22cccf99 | ||
|
|
7d9c9884dc | ||
|
|
e03eaa31fe | ||
|
|
f44a79fa73 | ||
|
|
943acbfea8 | ||
|
|
e955eeeabc | ||
|
|
d5910fba44 | ||
|
|
c8622fb46f | ||
|
|
c020789bfc | ||
|
|
28cd79a040 | ||
|
|
70014028e9 | ||
|
|
79cbe1cce8 | ||
|
|
aeac43ccf9 | ||
|
|
a5e6b371ac | ||
|
|
83a1b836f9 | ||
|
|
6e03c09017 | ||
|
|
a5fde5d933 | ||
|
|
a5823547d3 | ||
|
|
00968badf5 | ||
|
|
6aabab26d8 | ||
|
|
de86e8fb95 | ||
|
|
e5048812fd | ||
|
|
237bd6e831 | ||
|
|
c2953bc3e0 | ||
|
|
9d17d3ff06 | ||
|
|
33a16086ef | ||
|
|
6e0d22c23b | ||
|
|
2ec18eb4cf | ||
|
|
87595bdb7e | ||
|
|
789dfd6774 | ||
|
|
56ef0baa9d | ||
|
|
11e4fcb058 | ||
|
|
a3568c1b27 | ||
|
|
2a48a7b427 | ||
|
|
4418862965 | ||
|
|
e5aae90078 | ||
|
|
e2818afb62 | ||
|
|
7740ceb27e | ||
|
|
3d3da75726 | ||
|
|
8c5d644671 | ||
|
|
ca9b02fb53 | ||
|
|
74abe94711 | ||
|
|
1b827e6b67 | ||
|
|
3b23fc1eba | ||
|
|
1743413e00 | ||
|
|
c11b98fa9f | ||
|
|
a2c25ed49e | ||
|
|
2ad157bd77 | ||
|
|
7cc0129302 | ||
|
|
f7e436fe71 | ||
|
|
1ab83c5c02 | ||
|
|
6faf459f97 | ||
|
|
40058c2617 | ||
|
|
78ad0eaa74 | ||
|
|
7131ff28fd | ||
|
|
33d89b4a8b | ||
|
|
fef381e8eb | ||
|
|
9e148b4277 | ||
|
|
e7ca833929 | ||
|
|
322102e56b | ||
|
|
7c4f83ed60 | ||
|
|
55da91cb34 | ||
|
|
9f090d2861 | ||
|
|
93bc1c5382 | ||
|
|
b12bd5f558 | ||
|
|
be8a22d33b | ||
|
|
757c168a8d | ||
|
|
b0b00dc869 | ||
|
|
09b92fd78c | ||
|
|
f1a1fc6c5b | ||
|
|
8f26d62b35 | ||
|
|
8e9d72bd2e | ||
|
|
7461239218 | ||
|
|
2f356dcc6c | ||
|
|
4640febc83 | ||
|
|
c53dc06f80 | ||
|
|
a981b79865 | ||
|
|
050339c88e | ||
|
|
461780da22 | ||
|
|
e8db543028 | ||
|
|
79fd38cf3f | ||
|
|
dfb1e105bd | ||
|
|
2e7043ca90 | ||
|
|
7c8194a1a8 | ||
|
|
15f5e8d4ff | ||
|
|
3738ea5794 | ||
|
|
c11aebaaae | ||
|
|
0cf6144b3f | ||
|
|
41a7d3fd60 | ||
|
|
2b0b24f7c3 | ||
|
|
f72fc73913 | ||
|
|
8a331e0f26 | ||
|
|
da913d49a7 | ||
|
|
21aea52c32 | ||
|
|
9b90323d53 | ||
|
|
994272b966 | ||
|
|
fee4cd5f40 | ||
|
|
3023dbbe95 | ||
|
|
cc21241887 | ||
|
|
66fdd1c13f | ||
|
|
5723a200c5 | ||
|
|
35c9493336 | ||
|
|
b235b95bed | ||
|
|
50abbded34 | ||
|
|
31b479d71f | ||
|
|
769736ffea | ||
|
|
b79ddbbf60 | ||
|
|
1c5e36c7b6 | ||
|
|
eaf577f078 | ||
|
|
e02ad91c39 | ||
|
|
d4a6035c83 | ||
|
|
99226d3811 | ||
|
|
2a8c9f8e69 | ||
|
|
f4473b36a5 | ||
|
|
a1f7e35914 | ||
|
|
b6ae9a4a72 | ||
|
|
313e1a5e04 | ||
|
|
c7cee86685 | ||
|
|
915c4819b6 | ||
|
|
d817c50581 | ||
|
|
33ee958cfb | ||
|
|
8ba1e0f31e | ||
|
|
3ad5d676ab | ||
|
|
05d24e3665 | ||
|
|
3c0cc024aa | ||
|
|
88c8c36805 | ||
|
|
7c4a9b56ff | ||
|
|
16e554dd7b | ||
|
|
3896d41e95 | ||
|
|
12625d87b0 | ||
|
|
dfadfdc32c | ||
|
|
e4def081f5 | ||
|
|
cbb8dd6aa6 | ||
|
|
da354362be | ||
|
|
9372d46c08 | ||
|
|
dff61ab759 | ||
|
|
551190af30 | ||
|
|
2c54e763e4 | ||
|
|
6b866e24f6 | ||
|
|
be2990ec88 | ||
|
|
c0f7f7da42 | ||
|
|
b34c78c4e7 | ||
|
|
4ada090cb2 | ||
|
|
5677f25215 | ||
|
|
f1687cfb14 | ||
|
|
a00459aec3 | ||
|
|
8757435898 | ||
|
|
dcf62dc548 | ||
|
|
b835760b0b | ||
|
|
bd0c20f789 | ||
|
|
5ef98fcea1 | ||
|
|
985b232251 | ||
|
|
5e28d0234e | ||
|
|
5e03a4e9e2 | ||
|
|
e98e64f925 | ||
|
|
9ce281d008 | ||
|
|
27a3f2ce55 | ||
|
|
61858a60c2 | ||
|
|
bb5eeb6bd6 | ||
|
|
3494c9ccb6 | ||
|
|
a0df23415b | ||
|
|
8510c398a4 | ||
|
|
bae476cc99 | ||
|
|
4dfc660cc0 | ||
|
|
f47be46717 | ||
|
|
8d9305ee5f | ||
|
|
bc94358e98 | ||
|
|
5e98679f91 | ||
|
|
6819f0106d | ||
|
|
e3de229b82 | ||
|
|
3e67994cc7 | ||
|
|
a60c8f0e18 | ||
|
|
efdbb91a21 | ||
|
|
cff35d7286 | ||
|
|
c671f3ddc9 | ||
|
|
76b782a03f | ||
|
|
6f502bdc54 | ||
|
|
a8567b09e6 | ||
|
|
fd6db41b6e | ||
|
|
ff24b3e40c | ||
|
|
477cb12240 | ||
|
|
835edbe80e | ||
|
|
18d813a656 | ||
|
|
67576ad5bd | ||
|
|
37f2ba882e | ||
|
|
57ecac4aa7 | ||
|
|
070dea1bc5 | ||
|
|
8fed33b03b | ||
|
|
90ddc4a1e2 | ||
|
|
d2ca6f8d1f | ||
|
|
db01bf5dec | ||
|
|
881e92e7b3 | ||
|
|
d2e9527563 | ||
|
|
096e5ef197 | ||
|
|
702d07ea7d |
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -13,7 +13,7 @@ exemptProjects: true
|
||||
exemptMilestones: true
|
||||
|
||||
pulls:
|
||||
daysUntilStale: 15
|
||||
daysUntilStale: 14
|
||||
daysUntilClose: 3
|
||||
exemptLabels:
|
||||
- hotfix
|
||||
|
||||
22
CODEOWNERS
22
CODEOWNERS
@@ -3,13 +3,13 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
@@ -18,12 +18,8 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
|
||||
|
||||
.github/ @ankush
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @ankush
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.19.0"
|
||||
__version__ = "14.27.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -120,12 +121,14 @@ def get_region(company=None):
|
||||
|
||||
You can also set global company flag in `frappe.flags.company`
|
||||
"""
|
||||
if company or frappe.flags.company:
|
||||
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
|
||||
elif frappe.flags.country:
|
||||
return frappe.flags.country
|
||||
else:
|
||||
return frappe.get_system_settings("country")
|
||||
|
||||
if not company:
|
||||
company = frappe.local.flags.company
|
||||
|
||||
if company:
|
||||
return frappe.get_cached_value("Company", company, "country")
|
||||
|
||||
return frappe.flags.country or frappe.get_system_settings("country")
|
||||
|
||||
|
||||
def allow_regional(fn):
|
||||
@@ -136,6 +139,7 @@ def allow_regional(fn):
|
||||
def myfunction():
|
||||
pass"""
|
||||
|
||||
@functools.wraps(fn)
|
||||
def caller(*args, **kwargs):
|
||||
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
||||
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_currency",
|
||||
"inter_company_account",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_type",
|
||||
@@ -34,15 +33,11 @@
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -53,9 +48,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "account_name",
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_number",
|
||||
@@ -63,17 +56,13 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_group",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Group",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Is Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@@ -85,9 +74,7 @@
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
@@ -95,9 +82,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "report_type",
|
||||
@@ -105,32 +90,18 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nBalance Sheet\nProfit and Loss",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_group==0",
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "inter_company_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Inter Company Account",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -142,9 +113,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Setting Account Type helps in selecting this Account in transactions.",
|
||||
@@ -154,9 +123,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -164,9 +131,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"description": "If the account is frozen, entries are allowed to restricted users.",
|
||||
@@ -175,17 +140,13 @@
|
||||
"label": "Frozen",
|
||||
"oldfieldname": "freeze_account",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_must_be",
|
||||
"fieldtype": "Select",
|
||||
"label": "Balance must be",
|
||||
"options": "\nDebit\nCredit",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nDebit\nCredit"
|
||||
},
|
||||
{
|
||||
"fieldname": "lft",
|
||||
@@ -194,9 +155,7 @@
|
||||
"label": "Lft",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rgt",
|
||||
@@ -205,9 +164,7 @@
|
||||
"label": "Rgt",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "old_parent",
|
||||
@@ -215,33 +172,27 @@
|
||||
"hidden": 1,
|
||||
"label": "Old Parent",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
|
||||
"fieldname": "include_in_gross",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include in gross",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Include in gross"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Disable"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-11 15:15:54.338622",
|
||||
"modified": "2023-04-11 16:08:46.983677",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -301,5 +252,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -201,8 +201,11 @@ class Account(NestedSet):
|
||||
)
|
||||
|
||||
def validate_account_currency(self):
|
||||
self.currency_explicitly_specified = True
|
||||
|
||||
if not self.account_currency:
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
@@ -248,8 +251,10 @@ class Account(NestedSet):
|
||||
{
|
||||
"company": company,
|
||||
# parent account's currency should be passed down to child account's curreny
|
||||
# if it is None, it picks it up from default company currency, which might be unintended
|
||||
"account_currency": erpnext.get_company_currency(company),
|
||||
# if currency explicitly specified by user, child will inherit. else, default currency will be used.
|
||||
"account_currency": self.account_currency
|
||||
if self.currency_explicitly_specified
|
||||
else erpnext.get_company_currency(company),
|
||||
"parent_account": parent_acc_name_map[company],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
|
||||
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
|
||||
|
||||
test_dependencies = ["Company"]
|
||||
|
||||
|
||||
class TestAccount(unittest.TestCase):
|
||||
def test_rename_account(self):
|
||||
@@ -188,6 +191,58 @@ class TestAccount(unittest.TestCase):
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
|
||||
|
||||
def test_account_currency_sync(self):
|
||||
"""
|
||||
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
|
||||
"""
|
||||
|
||||
make_test_records("Company")
|
||||
|
||||
frappe.local.flags.pop("ignore_root_company_validation", None)
|
||||
|
||||
def create_bank_account():
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "_Test Bank JPY"
|
||||
|
||||
acc.parent_account = "Temporary Accounts - _TC6"
|
||||
acc.company = "_Test Company 6"
|
||||
return acc
|
||||
|
||||
acc = create_bank_account()
|
||||
# Explicitly set currency
|
||||
acc.account_currency = "JPY"
|
||||
acc.insert()
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Bank JPY",
|
||||
"account_currency": "JPY",
|
||||
"company": "_Test Company 7",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
|
||||
|
||||
acc = create_bank_account()
|
||||
# default currency is used
|
||||
acc.insert()
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Bank JPY",
|
||||
"account_currency": "USD",
|
||||
"company": "_Test Company 7",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
|
||||
|
||||
def test_child_company_account_rename_sync(self):
|
||||
frappe.local.flags.pop("ignore_root_company_validation", None)
|
||||
|
||||
@@ -297,7 +352,7 @@ def _make_test_records(verbose=None):
|
||||
# fixed asset depreciation
|
||||
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
|
||||
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
|
||||
["_Test Depreciations", "Expenses", 0, None, None],
|
||||
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
|
||||
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
# Receivable / Payable Account
|
||||
["_Test Receivable", "Current Assets", 0, "Receivable", None],
|
||||
|
||||
@@ -50,13 +50,15 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
|
||||
def set_fieldname_and_label(self):
|
||||
if not self.label:
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -31,12 +33,16 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"book_tax_discount_loss",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"stale_days",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
@@ -167,11 +173,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Stale Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_settings_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Report Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||
@@ -181,6 +182,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
@@ -347,6 +349,42 @@
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account "
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
},
|
||||
{
|
||||
"fieldname": "journals_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Rows with Same Account heads will be merged on Ledger",
|
||||
"fieldname": "merge_similar_account_heads",
|
||||
"fieldtype": "Check",
|
||||
"label": "Merge Similar Account Heads"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jpd0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Reconciliations"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "auto_reconcile_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Reconcile Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -354,7 +392,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-27 21:49:52.538655",
|
||||
"modified": "2023-06-13 18:47:46.430291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -383,4 +421,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class BankClearance(Document):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
@@ -81,7 +81,7 @@ class BankClearance(Document):
|
||||
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
loan_disbursements = (
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||
@@ -90,17 +90,22 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("debit"),
|
||||
loan_disbursement.reference_number.as_("cheque_number"),
|
||||
loan_disbursement.reference_date.as_("cheque_date"),
|
||||
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.applicant.as_("against_account"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||
.where(loan_disbursement.clearance_date.isnull())
|
||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||
.orderby(loan_disbursement.disbursement_date)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
).run(as_dict=1)
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_disbursement.clearance_date.isnull())
|
||||
|
||||
loan_disbursements = query.run(as_dict=1)
|
||||
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
@@ -113,16 +118,19 @@ class BankClearance(Document):
|
||||
ConstantColumn(0).as_("credit"),
|
||||
loan_repayment.reference_number.as_("cheque_number"),
|
||||
loan_repayment.reference_date.as_("cheque_date"),
|
||||
loan_repayment.clearance_date.as_("clearance_date"),
|
||||
loan_repayment.applicant.as_("against_account"),
|
||||
loan_repayment.posting_date,
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.clearance_date.isnull())
|
||||
.where(loan_repayment.posting_date >= self.from_date)
|
||||
.where(loan_repayment.posting_date <= self.to_date)
|
||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||
)
|
||||
|
||||
if not self.include_reconciled_entries:
|
||||
query = query.where(loan_repayment.clearance_date.isnull())
|
||||
|
||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
@@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(
|
||||
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
|
||||
)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
@@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
@@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(credit_in_account_currency)",
|
||||
return abs(
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(debit_in_account_currency-credit_in_account_currency)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
|
||||
@@ -125,14 +125,27 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if not args.account:
|
||||
return
|
||||
|
||||
for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
|
||||
default_dimensions = [
|
||||
{
|
||||
"fieldname": "project",
|
||||
"document_type": "Project",
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"document_type": "Cost Center",
|
||||
},
|
||||
]
|
||||
|
||||
for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
|
||||
budget_against = dimension.get("fieldname")
|
||||
|
||||
if (
|
||||
args.get(budget_against)
|
||||
and args.account
|
||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
|
||||
):
|
||||
|
||||
doctype = frappe.unscrub(budget_against)
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
|
||||
@@ -325,14 +325,14 @@ def get_template(template_type):
|
||||
|
||||
if template_type == "Blank Template":
|
||||
for root_type in get_root_types():
|
||||
writer.writerow(["", "", "", 1, "", root_type])
|
||||
writer.writerow(["", "", "", "", 1, "", root_type])
|
||||
|
||||
for account in get_mandatory_group_accounts():
|
||||
writer.writerow(["", "", "", 1, account, "Asset"])
|
||||
writer.writerow(["", "", "", "", 1, account, "Asset"])
|
||||
|
||||
for account_type in get_mandatory_account_types():
|
||||
writer.writerow(
|
||||
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||
)
|
||||
else:
|
||||
writer = get_sample_template(writer)
|
||||
|
||||
@@ -35,6 +35,21 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
}
|
||||
},
|
||||
|
||||
validate_rounding_loss: function(frm) {
|
||||
let allowance = frm.doc.rounding_loss_allowance;
|
||||
if (!(allowance > 0 && allowance < 1)) {
|
||||
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
||||
}
|
||||
},
|
||||
|
||||
rounding_loss_allowance: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
get_entries: function(frm, account) {
|
||||
frappe.call({
|
||||
method: "get_accounts_data",
|
||||
@@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) {
|
||||
company: frm.doc.company,
|
||||
posting_date: frm.doc.posting_date,
|
||||
party_type: row.party_type,
|
||||
party: row.party
|
||||
party: row.party,
|
||||
rounding_loss_allowance: frm.doc.rounding_loss_allowance
|
||||
},
|
||||
callback: function(r){
|
||||
$.extend(row, r.message);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"rounding_loss_allowance",
|
||||
"column_break_2",
|
||||
"company",
|
||||
"section_break_4",
|
||||
@@ -96,11 +97,18 @@
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0.05",
|
||||
"description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||
"fieldname": "rounding_loss_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rounding Loss Allowance"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:24.416529",
|
||||
"modified": "2023-06-12 21:02:09.818208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -12,13 +12,19 @@ from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class ExchangeRateRevaluation(Document):
|
||||
def validate(self):
|
||||
self.validate_rounding_loss_allowance()
|
||||
self.set_total_gain_loss()
|
||||
|
||||
def validate_rounding_loss_allowance(self):
|
||||
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
|
||||
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
total_gain_loss = 0
|
||||
|
||||
@@ -91,7 +97,12 @@ class ExchangeRateRevaluation(Document):
|
||||
def get_accounts_data(self):
|
||||
self.validate_mandatory()
|
||||
account_details = self.get_account_balance_from_gle(
|
||||
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
|
||||
company=self.company,
|
||||
posting_date=self.posting_date,
|
||||
account=None,
|
||||
party_type=None,
|
||||
party=None,
|
||||
rounding_loss_allowance=self.rounding_loss_allowance,
|
||||
)
|
||||
accounts_with_new_balance = self.calculate_new_account_balance(
|
||||
self.company, self.posting_date, account_details
|
||||
@@ -103,7 +114,9 @@ class ExchangeRateRevaluation(Document):
|
||||
return accounts_with_new_balance
|
||||
|
||||
@staticmethod
|
||||
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
|
||||
def get_account_balance_from_gle(
|
||||
company, posting_date, account, party_type, party, rounding_loss_allowance
|
||||
):
|
||||
account_details = []
|
||||
|
||||
if company and posting_date:
|
||||
@@ -170,6 +183,23 @@ class ExchangeRateRevaluation(Document):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# round off balance based on currency precision
|
||||
# and consider debit-credit difference allowance
|
||||
currency_precision = get_currency_precision()
|
||||
rounding_loss_allowance = rounding_loss_allowance or 0.05
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
|
||||
acc.balance_in_account_currency = 0
|
||||
|
||||
acc.balance = flt(acc.balance, currency_precision)
|
||||
if abs(acc.balance) <= rounding_loss_allowance:
|
||||
acc.balance = 0
|
||||
|
||||
acc.zero_balance = (
|
||||
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@staticmethod
|
||||
@@ -490,6 +520,8 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
conditions.append(gl.company == company)
|
||||
conditions.append(gl.account == account)
|
||||
conditions.append(gl.is_cancelled == 0)
|
||||
conditions.append((gl.debit > 0) | (gl.credit > 0))
|
||||
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
|
||||
if party_type:
|
||||
conditions.append(gl.party_type == party_type)
|
||||
if party:
|
||||
@@ -519,7 +551,9 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(company, posting_date, account, party_type=None, party=None):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance=None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
@@ -537,7 +571,12 @@ def get_account_details(company, posting_date, account, party_type=None, party=N
|
||||
"account_currency": account_currency,
|
||||
}
|
||||
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
|
||||
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
|
||||
company=company,
|
||||
posting_date=posting_date,
|
||||
account=account,
|
||||
party_type=party_type,
|
||||
party=party,
|
||||
rounding_loss_allowance=rounding_loss_allowance,
|
||||
)
|
||||
|
||||
if account_balance and (
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_multi_currency()
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
@@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
@@ -130,6 +131,13 @@ class JournalEntry(AccountsController):
|
||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||
|
||||
def validate_depr_entry_voucher_type(self):
|
||||
if (
|
||||
any(d.account_type == "Depreciation" for d in self.get("accounts"))
|
||||
and self.voucher_type != "Depreciation Entry"
|
||||
):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
for account in stock_accounts:
|
||||
@@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
continue
|
||||
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation -= d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
|
||||
|
||||
asset.set_status()
|
||||
|
||||
@@ -316,35 +329,35 @@ class JournalEntry(AccountsController):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
fb_idx = None
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.set_status()
|
||||
|
||||
fb_idx = cint(s.finance_book_id) or 1
|
||||
break
|
||||
if not fb_idx:
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation += d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
|
||||
|
||||
asset.set_status()
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -659,7 +672,6 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
|
||||
|
||||
def validate_total_debit_and_credit(self):
|
||||
self.set_total_debit_credit()
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
if self.difference:
|
||||
frappe.throw(
|
||||
@@ -879,6 +891,8 @@ class JournalEntry(AccountsController):
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
|
||||
gl_map = self.build_gl_map()
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
update_outstanding = "No"
|
||||
@@ -886,7 +900,13 @@ class JournalEntry(AccountsController):
|
||||
update_outstanding = "Yes"
|
||||
|
||||
if gl_map:
|
||||
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
make_gl_entries(
|
||||
gl_map,
|
||||
cancel=cancel,
|
||||
adv_adj=adv_adj,
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self, difference_account=None):
|
||||
@@ -920,6 +940,7 @@ class JournalEntry(AccountsController):
|
||||
blank_row.debit_in_account_currency = abs(diff)
|
||||
blank_row.debit = abs(diff)
|
||||
|
||||
self.set_total_debit_credit()
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.submit()
|
||||
|
||||
def test_inter_company_jv(self):
|
||||
frappe.db.set_value("Account", "Sales Expenses - _TC", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
|
||||
jv = make_journal_entry(
|
||||
"Sales Expenses - _TC",
|
||||
"Buildings - _TC",
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
onload: function(frm) {
|
||||
if(frm.is_new()) {
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message) {
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frappe.model.set_default_values(frm.doc);
|
||||
|
||||
@@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message){
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
voucher_type: function(frm) {
|
||||
var add_accounts = function(doc, r) {
|
||||
|
||||
@@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
||||
party_account_currency, "references");
|
||||
|
||||
frm.set_currency_labels(["amount"], company_currency, "deductions");
|
||||
|
||||
cur_frm.set_df_property("source_exchange_rate", "description",
|
||||
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
||||
|
||||
@@ -907,7 +905,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
function(d) { return flt(d.amount) }));
|
||||
|
||||
frm.set_value("difference_amount", difference_amount - total_deductions +
|
||||
frm.doc.base_total_taxes_and_charges);
|
||||
flt(frm.doc.base_total_taxes_and_charges));
|
||||
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
@@ -973,29 +971,48 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
var write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
|
||||
return t.account==r.message[account] ? t : null; });
|
||||
|
||||
var row = [];
|
||||
|
||||
var difference_amount = flt(frm.doc.difference_amount,
|
||||
const difference_amount = flt(frm.doc.difference_amount,
|
||||
precision("difference_amount"));
|
||||
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = r.message[account];
|
||||
row.cost_center = r.message["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"))
|
||||
}
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"))
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
refresh_field("deductions");
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt({
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
}
|
||||
})
|
||||
}, (values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
}, __(frappe.unscrub(account)));
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate(self):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_missing_ref_details()
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
@@ -147,19 +148,57 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def validate_allocated_amount(self):
|
||||
for d in self.get("references"):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references").copy():
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and d.outstanding_amount != latest.outstanding_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
|
||||
).format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
|
||||
d.outstanding_amount = latest.outstanding_amount
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0:
|
||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0:
|
||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -219,11 +258,16 @@ class PaymentEntry(AccountsController):
|
||||
else self.paid_to_account_currency
|
||||
)
|
||||
|
||||
self.set_missing_ref_details()
|
||||
|
||||
def set_missing_ref_details(self, force=False):
|
||||
def set_missing_ref_details(
|
||||
self, force: bool = False, update_ref_details_only_for: list | None = None
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
if update_ref_details_only_for and (
|
||||
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
|
||||
):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
)
|
||||
@@ -367,7 +411,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."
|
||||
"{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry."
|
||||
).format(
|
||||
_(k),
|
||||
frappe.bold(", ".join(d.reference_name for d in v)),
|
||||
@@ -416,7 +460,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
for ref in self.get("references"):
|
||||
if ref.payment_term and ref.reference_name:
|
||||
key = (ref.payment_term, ref.reference_name)
|
||||
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||
|
||||
@@ -424,20 +468,37 @@ class PaymentEntry(AccountsController):
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": ref.reference_name},
|
||||
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
|
||||
fields=[
|
||||
"paid_amount",
|
||||
"payment_amount",
|
||||
"payment_term",
|
||||
"discount",
|
||||
"outstanding",
|
||||
"discount_type",
|
||||
],
|
||||
)
|
||||
for term in payment_schedule:
|
||||
invoice_key = (term.payment_term, ref.reference_name)
|
||||
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
if not (term.discount_type and term.discount):
|
||||
continue
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||
term.discount / 100
|
||||
)
|
||||
else:
|
||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||
|
||||
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
||||
|
||||
allocated_amount = self.get_allocated_amount_in_transaction_currency(
|
||||
allocated_amount, key[2], key[1]
|
||||
)
|
||||
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||
|
||||
@@ -472,6 +533,33 @@ class PaymentEntry(AccountsController):
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
self, allocated_amount, reference_doctype, reference_docname
|
||||
):
|
||||
"""
|
||||
Payment Entry could be in base currency while reference's payment schedule
|
||||
is always in transaction currency.
|
||||
E.g.
|
||||
* SI with base=INR and currency=USD
|
||||
* SI with payment schedule in USD
|
||||
* PE in INR (accounting done in base currency)
|
||||
"""
|
||||
ref_currency, ref_exchange_rate = frappe.db.get_value(
|
||||
reference_doctype, reference_docname, ["currency", "conversion_rate"]
|
||||
)
|
||||
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
|
||||
# PE in different currency
|
||||
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
|
||||
|
||||
if not (is_single_currency and reference_is_multi_currency):
|
||||
return allocated_amount
|
||||
|
||||
allocated_amount = flt(
|
||||
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||
)
|
||||
|
||||
return allocated_amount
|
||||
|
||||
def set_status(self):
|
||||
if self.docstatus == 2:
|
||||
self.status = "Cancelled"
|
||||
@@ -604,6 +692,28 @@ class PaymentEntry(AccountsController):
|
||||
self.precision("base_received_amount"),
|
||||
)
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
exchange_rate = 1
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.source_exchange_rate
|
||||
elif self.payment_type == "Pay":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
@@ -612,9 +722,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
total_allocated_amount += flt(d.allocated_amount)
|
||||
base_total_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
self.total_allocated_amount = abs(total_allocated_amount)
|
||||
self.base_total_allocated_amount = abs(base_total_allocated_amount)
|
||||
@@ -831,9 +939,7 @@ class PaymentEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
allocated_amount_in_company_currency = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
|
||||
)
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
gle.update(
|
||||
{
|
||||
@@ -1381,7 +1487,7 @@ def get_orders_to_be_billed(
|
||||
if voucher_type:
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center"):
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
orders = []
|
||||
@@ -1427,9 +1533,15 @@ def get_orders_to_be_billed(
|
||||
|
||||
order_list = []
|
||||
for d in orders:
|
||||
if not (
|
||||
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
|
||||
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and filters.get("outstanding_amt_less_than")
|
||||
and not (
|
||||
flt(filters.get("outstanding_amt_greater_than"))
|
||||
<= flt(d.outstanding_amount)
|
||||
<= flt(filters.get("outstanding_amt_less_than"))
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -1550,17 +1662,7 @@ def get_account_details(account, date, cost_center=None):
|
||||
@frappe.whitelist()
|
||||
def get_company_defaults(company):
|
||||
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
|
||||
ret = frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
for fieldname in fields:
|
||||
if not ret[fieldname]:
|
||||
frappe.throw(
|
||||
_("Please set default {0} in Company {1}").format(
|
||||
frappe.get_meta("Company").get_label(fieldname), company
|
||||
)
|
||||
)
|
||||
|
||||
return ret
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
@@ -1642,11 +1744,21 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_entry(
|
||||
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
|
||||
dt,
|
||||
dn,
|
||||
party_amount=None,
|
||||
bank_account=None,
|
||||
bank_amount=None,
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
100.0 + over_billing_allowance
|
||||
):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
|
||||
if not party_type:
|
||||
@@ -1665,12 +1777,20 @@ def get_payment_entry(
|
||||
# bank or cash
|
||||
bank = get_bank_cash_account(doc, bank_account)
|
||||
|
||||
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
|
||||
if party_type in ["Customer", "Supplier"] and not bank:
|
||||
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
|
||||
if party_bank_account:
|
||||
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
|
||||
bank = get_bank_cash_account(doc, account)
|
||||
|
||||
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||
)
|
||||
|
||||
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc
|
||||
reference_date = getdate(reference_date)
|
||||
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
)
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
@@ -1678,6 +1798,7 @@ def get_payment_entry(
|
||||
pe.company = doc.company
|
||||
pe.cost_center = doc.get("cost_center")
|
||||
pe.posting_date = nowdate()
|
||||
pe.reference_date = reference_date
|
||||
pe.mode_of_payment = doc.get("mode_of_payment")
|
||||
pe.party_type = party_type
|
||||
pe.party = doc.get(scrub(party_type))
|
||||
@@ -1711,14 +1832,19 @@ def get_payment_entry(
|
||||
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
|
||||
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
|
||||
else:
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
|
||||
if doc.doctype in (
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Purchase Order",
|
||||
"Sales Order",
|
||||
) and frappe.get_cached_value(
|
||||
"Payment Terms Template",
|
||||
{"name": doc.payment_terms_template},
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
):
|
||||
|
||||
for reference in get_reference_as_per_payment_terms(
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
pe.append("references", reference)
|
||||
else:
|
||||
@@ -1763,22 +1889,24 @@ def get_payment_entry(
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
pe.set_missing_ref_details()
|
||||
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
|
||||
}
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
|
||||
|
||||
set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
pe.set_difference_amount()
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
return pe
|
||||
|
||||
@@ -1872,37 +2000,53 @@ def set_paid_amount_and_received_amount(
|
||||
paid_amount = received_amount = 0
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = abs(outstanding_amount)
|
||||
elif payment_type == "Receive":
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if payment_type == "Receive":
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
paid_amount = received_amount * doc.get("conversion_rate", 1)
|
||||
received_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
paid_amount = received_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
def apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
):
|
||||
total_discount = 0
|
||||
valid_discounts = []
|
||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
||||
is_multi_currency = party_account_currency != doc.company_currency
|
||||
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
|
||||
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||
else:
|
||||
discount_amount = term.discount
|
||||
|
||||
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
|
||||
# if accounting is done in the same currency, paid_amount = received_amount
|
||||
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
|
||||
discount_amount_in_foreign_currency = discount_amount * conversion_rate
|
||||
|
||||
if doc.doctype == "Sales Invoice":
|
||||
paid_amount -= discount_amount
|
||||
@@ -1911,23 +2055,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
received_amount -= discount_amount
|
||||
paid_amount -= discount_amount_in_foreign_currency
|
||||
|
||||
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
|
||||
total_discount += discount_amount
|
||||
|
||||
if total_discount:
|
||||
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
|
||||
currency = doc.get("currency") if is_multi_currency else doc.company_currency
|
||||
money = frappe.utils.fmt_money(total_discount, currency=currency)
|
||||
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
||||
|
||||
return paid_amount, received_amount, total_discount
|
||||
return paid_amount, received_amount, total_discount, valid_discounts
|
||||
|
||||
|
||||
def set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
):
|
||||
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||
if party_account_currency != doc.company_currency:
|
||||
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
# Avoid considering miniscule losses
|
||||
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
|
||||
|
||||
# Set base discount amount (discount loss/pending rounding loss) in deductions
|
||||
if discount_amount > 0.0:
|
||||
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||
|
||||
# If tax loss booking is enabled, pending loss will be rounding loss.
|
||||
# Otherwise it will be the total discount loss.
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||
"""Split early payment discount into Income Loss & Tax Loss."""
|
||||
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
|
||||
|
||||
if not total_discount_percent:
|
||||
return 0.0
|
||||
|
||||
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
|
||||
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
|
||||
|
||||
# Round off total loss rather than individual losses to reduce rounding error
|
||||
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
|
||||
|
||||
|
||||
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||
"""Get total percentage and amount discount applied as a percentage."""
|
||||
total_discount_percent = (
|
||||
sum(
|
||||
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
# Operate in percentages only as it makes the income & tax split easier
|
||||
total_discount_amount = (
|
||||
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
if total_discount_amount:
|
||||
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
|
||||
total_discount_percent += discount_percentage
|
||||
return total_discount_percent
|
||||
|
||||
return total_discount_percent
|
||||
|
||||
|
||||
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||
"""Add loss on income discount in base currency."""
|
||||
precision = doc.precision("total")
|
||||
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(base_loss_on_income, precision),
|
||||
},
|
||||
)
|
||||
|
||||
return base_loss_on_income # Return loss without rounding
|
||||
|
||||
|
||||
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"""Add loss on tax discount in base currency."""
|
||||
tax_discount_loss = {}
|
||||
base_total_tax_loss = 0
|
||||
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||
total_discount_percentage / 100
|
||||
)
|
||||
|
||||
account = tax.get("account_head")
|
||||
if not tax_discount_loss.get(account):
|
||||
tax_discount_loss[account] = base_tax_loss
|
||||
else:
|
||||
tax_discount_loss[account] += base_tax_loss
|
||||
|
||||
for account, loss in tax_discount_loss.items():
|
||||
base_total_tax_loss += loss
|
||||
if loss == 0.0:
|
||||
continue
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
},
|
||||
)
|
||||
|
||||
return base_total_tax_loss # Return loss without rounding
|
||||
|
||||
|
||||
def get_reference_as_per_payment_terms(
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
references = []
|
||||
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
|
||||
party_account_currency != doc.company_currency
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
payment_term_outstanding = flt(
|
||||
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
||||
)
|
||||
if not is_multi_currency_acc:
|
||||
# If accounting is done in company currency for multi-currency transaction
|
||||
payment_term_outstanding = flt(
|
||||
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||
)
|
||||
|
||||
if payment_term_outstanding:
|
||||
references.append(
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@@ -51,6 +51,38 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 0)
|
||||
|
||||
def test_payment_against_sales_order_usd_to_inr(self):
|
||||
so = make_sales_order(
|
||||
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
|
||||
)
|
||||
so.conversion_rate = 50
|
||||
so.submit()
|
||||
pe = get_payment_entry("Sales Order", so.name)
|
||||
pe.source_exchange_rate = 55
|
||||
pe.received_amount = 5500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
# there should be no difference amount
|
||||
pe.reload()
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
self.assertEqual(pe.deductions, [])
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 100)
|
||||
|
||||
pe.cancel()
|
||||
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 0)
|
||||
|
||||
def test_payment_entry_for_blocked_supplier_invoice(self):
|
||||
supplier = frappe.get_doc("Supplier", "_Test Supplier")
|
||||
supplier.on_hold = 1
|
||||
@@ -256,10 +288,25 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
|
||||
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe.paid_amount, 212.4)
|
||||
self.assertEqual(pe.deductions[0].amount, 23.6)
|
||||
|
||||
pe.submit()
|
||||
si.load_from_db()
|
||||
|
||||
@@ -269,6 +316,190 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
||||
|
||||
def test_payment_entry_against_payment_terms_with_discount_amount(self):
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
|
||||
si.payment_terms_template = "Test Discount Amount Template"
|
||||
create_payment_terms_template_with_discount(
|
||||
name="30 Credit Days with Rs.50 Discount",
|
||||
discount_type="Amount",
|
||||
discount=50,
|
||||
template_name="Test Discount Amount Template",
|
||||
)
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Service Tax",
|
||||
"rate": 18,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Set reference date past discount cut off date
|
||||
pe_1 = get_payment_entry(
|
||||
"Sales Invoice",
|
||||
si.name,
|
||||
bank_account="_Test Cash - _TC",
|
||||
reference_date=frappe.utils.add_days(si.posting_date, 2),
|
||||
)
|
||||
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
|
||||
|
||||
# Test if tax loss is booked on enabling configuration
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
|
||||
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||
self.assertEqual(pe.paid_amount, 186)
|
||||
self.assertEqual(pe.deductions[0].amount, 50.0)
|
||||
|
||||
pe.submit()
|
||||
si.load_from_db()
|
||||
|
||||
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
|
||||
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
|
||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
"book_tax_discount_loss": 1,
|
||||
},
|
||||
)
|
||||
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
|
||||
self,
|
||||
):
|
||||
"""
|
||||
1. Multi-currency SI with single currency accounting (company currency)
|
||||
2. PE with early payment discount
|
||||
3. Test if Paid Amount is calculated in company currency
|
||||
4. Test if deductions are calculated in company currency
|
||||
|
||||
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_save=1,
|
||||
)
|
||||
create_payment_terms_template_with_discount()
|
||||
si.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice",
|
||||
si.name,
|
||||
bank_account="_Test Bank - _TC",
|
||||
)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
# Early payment discount loss on income
|
||||
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
|
||||
self.assertEqual(pe.received_amount, 4500.0)
|
||||
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
self.assertEqual(pe.difference_amount, 0.0)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["Debtors - _TC", 0, 5000, si.name],
|
||||
["_Test Bank - _TC", 4500, 0, None],
|
||||
["Write Off - _TC", 500.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
|
||||
"""
|
||||
1. Multi-currency SI with multi-currency accounting
|
||||
2. PE with early payment discount and also exchange loss
|
||||
3. Test if Paid Amount is calculated in transaction currency
|
||||
4. Test if deductions are calculated in base/company currency
|
||||
5. Test if exchange loss is reflected in difference
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_save=1,
|
||||
)
|
||||
create_payment_terms_template_with_discount()
|
||||
si.payment_terms_template = "Test Discount Template"
|
||||
|
||||
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||
)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
# Early payment discount loss on income
|
||||
self.assertEqual(pe.paid_amount, 90.0)
|
||||
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
|
||||
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.difference_amount, 0.0)
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||
["_Test Bank - _TC", 4200, 0, None],
|
||||
["Write Off - _TC", 500.0, 0, None],
|
||||
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
|
||||
def test_payment_against_purchase_invoice_to_check_status(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
@@ -782,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
||||
create_payment_entry(party_type="Employee", party=employee, save=True)
|
||||
|
||||
def test_duplicate_payment_entry_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
def test_duplicate_payment_entry_partial_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.received_amount = si.total / 2
|
||||
pe.references[0].allocated_amount = si.total / 2
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -839,24 +1094,27 @@ def create_payment_terms_template():
|
||||
).insert()
|
||||
|
||||
|
||||
def create_payment_terms_template_with_discount():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
create_payment_term("30 Credit Days with 10% Discount")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
|
||||
payment_term_template = frappe.get_doc(
|
||||
if not frappe.db.exists("Payment Terms Template", template_name):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "Test Discount Template",
|
||||
"template_name": template_name,
|
||||
"allocate_payment_based_on_payment_terms": 1,
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"payment_term": "30 Credit Days with 10% Discount",
|
||||
"payment_term": name or "30 Credit Days with 10% Discount",
|
||||
"invoice_portion": 100,
|
||||
"credit_days_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 2,
|
||||
"discount": 10,
|
||||
"discount_type": discount_type or "Percentage",
|
||||
"discount": discount or 10,
|
||||
"discount_validity_based_on": "Day(s) after invoice date",
|
||||
"discount_validity": 1,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"creation": "2016-06-15 15:56:30.815503",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"cost_center",
|
||||
@@ -17,9 +18,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@@ -28,37 +27,30 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-12 20:38:08.110674",
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
@@ -66,5 +58,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type('Allocate', null, 'default');
|
||||
}
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
|
||||
if(enabled) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
}
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
company() {
|
||||
@@ -272,4 +298,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on('Payment Reconciliation Allocation', {
|
||||
allocated_amount: function(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
// filter invoice
|
||||
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
|
||||
// filter payment
|
||||
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'calculate_difference_on_allocation_change',
|
||||
args: {
|
||||
payment_entry: payment,
|
||||
invoice: invoice,
|
||||
allocated_amount: row.allocated_amount
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
row.difference_amount = r.message;
|
||||
frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
|
||||
|
||||
@@ -6,10 +6,12 @@ import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
@@ -124,12 +126,29 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
ConstantColumn(voucher_type).as_("voucher_type"),
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
@@ -137,19 +156,10 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account_type == "Payable")
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
# get return invoices
|
||||
doc = qb.DocType(voucher_type)
|
||||
return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
& (IfNull(doc.return_against, "") == "")
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
@@ -201,6 +211,15 @@ class PaymentReconciliation(Document):
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
[x.voucher_no for x in self.return_invoices]
|
||||
if self.party_type in ["Customer", "Supplier"]
|
||||
else []
|
||||
)
|
||||
# Filter out cr/dr notes from outstanding invoices list
|
||||
# Happens when non-standalone cr/dr notes are linked with another invoice through journal entry
|
||||
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
|
||||
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
@@ -233,6 +252,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
return new_difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
@@ -295,9 +323,7 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
self.validate_allocation()
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -321,12 +347,35 @@ class PaymentReconciliation(Document):
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list)
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
|
||||
if dr_or_cr_notes:
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
running_doc = is_any_doc_running(
|
||||
dict(
|
||||
company=self.company,
|
||||
party_type=self.party_type,
|
||||
party=self.party,
|
||||
receivable_payable_account=self.receivable_payable_account,
|
||||
)
|
||||
)
|
||||
|
||||
if running_doc:
|
||||
frappe.throw(
|
||||
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
|
||||
get_link_to_form("Auto Reconcile", running_doc)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.validate_allocation()
|
||||
self.reconcile_allocations()
|
||||
msgprint(_("Successfully Reconciled"))
|
||||
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def make_difference_entry(self, row):
|
||||
|
||||
@@ -497,10 +497,16 @@ def get_amount(ref_doc, payment_account=None):
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
grand_total = pay.amount
|
||||
break
|
||||
elif dt == "POS Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@@ -74,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
self.assertEqual(pr.reference_name, si_usd.name)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
|
||||
def test_payment_entry_against_purchase_invoice(self):
|
||||
si_usd = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=si_usd.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
|
||||
def test_payment_entry(self):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
|
||||
@@ -35,6 +35,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
@@ -169,21 +170,18 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t2.account_currency,
|
||||
t1.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
from `tabGL Entry` t1
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.account in (select name from `tabAccount` where report_type = 'Profit and Loss' and docstatus < 2 and company = %s)
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
dimension_fields=", ".join(dimension_fields),
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
@@ -176,6 +176,23 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
|
||||
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"company": company,
|
||||
"posting_date": "2020-03-15",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": "Test Item 1",
|
||||
"warehouse": warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, repost_doc.save)
|
||||
|
||||
repost_doc.posting_date = add_months(today(), 13)
|
||||
repost_doc.save()
|
||||
|
||||
def make_period_closing_voucher(self, submit=True):
|
||||
surplus_account = create_account()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
@@ -673,18 +674,22 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql(
|
||||
"""select sum(p_item.qty) as qty
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||
where p.name = p_item.parent
|
||||
and ifnull(p.consolidated_invoice, '') = ''
|
||||
and p_item.docstatus = 1
|
||||
and p_item.item_code = %s
|
||||
and p_item.warehouse = %s
|
||||
""",
|
||||
(item_code, warehouse),
|
||||
as_dict=1,
|
||||
)
|
||||
p_inv = frappe.qb.DocType("POS Invoice")
|
||||
p_item = frappe.qb.DocType("POS Invoice Item")
|
||||
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.qty).as_("qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
& (p_inv.is_return == 0)
|
||||
& (p_item.docstatus == 1)
|
||||
& (p_item.item_code == item_code)
|
||||
& (p_item.warehouse == warehouse)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"description": "If rate is zero them item will be treated as \"Free Item\"",
|
||||
"description": "If rate is zero then item will be treated as \"Free Item\"",
|
||||
"fieldname": "free_item_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Free Item Rate"
|
||||
@@ -670,4 +670,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
onload: function(frm) {
|
||||
// set queries
|
||||
frm.set_query("party_type", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
}
|
||||
}
|
||||
});
|
||||
frm.set_query('receivable_payable_account', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('cost_center', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('bank_cash_account', function(doc) {
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
|
||||
let execute_btn = __("Start / Resume")
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
|
||||
args: {
|
||||
docname: frm.doc.name
|
||||
}
|
||||
}).then(r => {
|
||||
if(!r.exc) {
|
||||
frappe.show_alert(__("Job Started"));
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
|
||||
frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
|
||||
args: {
|
||||
"docname": frm.docname,
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let progress = 0;
|
||||
let description = "";
|
||||
|
||||
if (r.message.processed) {
|
||||
progress = (r.message.processed/r.message.total) * 100;
|
||||
description = r.message.processed + "/" + r.message.total + " processed";
|
||||
} else if (r.message.total == 0 && frm.doc.status == "Completed") {
|
||||
progress = 100;
|
||||
}
|
||||
|
||||
|
||||
frm.dashboard.add_progress('Reconciliation Progress', progress, description);
|
||||
}
|
||||
})
|
||||
}
|
||||
if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
|
||||
let execute_btn = __("Pause")
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
|
||||
args: {
|
||||
"docname": frm.docname,
|
||||
}
|
||||
}).then(r => {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert(__("Job Paused"));
|
||||
frm.reload_doc()
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
company(frm) {
|
||||
frm.set_value('party', '');
|
||||
frm.set_value('receivable_payable_account', '');
|
||||
},
|
||||
party_type(frm) {
|
||||
frm.set_value('party', '');
|
||||
},
|
||||
|
||||
party(frm) {
|
||||
frm.set_value('receivable_payable_account', '');
|
||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
}
|
||||
frm.refresh();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:ACC-PPR-{#####}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-30 21:28:39.793927",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"party_type",
|
||||
"column_break_io6c",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"filter_section",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"column_break_kegk",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
"column_break_uj04",
|
||||
"cost_center",
|
||||
"bank_cash_account",
|
||||
"section_break_2n02",
|
||||
"status",
|
||||
"error_log",
|
||||
"section_break_a8yx",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_io6c",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party",
|
||||
"options": "party_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "receivable_payable_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Receivable/Payable Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filter_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Invoice Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Invoice Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kegk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_payment_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Payment Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_payment_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Payment Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uj04",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_cash_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Bank/Cash Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2n02",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.error_log",
|
||||
"fieldname": "error_log",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Error Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Process Payment Reconciliation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_a8yx",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:19:30.912953",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
|
||||
class ProcessPaymentReconciliation(Document):
|
||||
def validate(self):
|
||||
self.validate_receivable_payable_account()
|
||||
self.validate_bank_cash_account()
|
||||
|
||||
def validate_receivable_payable_account(self):
|
||||
if self.receivable_payable_account:
|
||||
if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
|
||||
frappe.throw(
|
||||
_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
|
||||
frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_bank_cash_account(self):
|
||||
if self.bank_cash_account:
|
||||
if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
|
||||
frappe.throw(
|
||||
_("Bank/Cash Account {0} doesn't belong to company {1}").format(
|
||||
frappe.bold(self.bank_cash_account), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.status = ""
|
||||
self.error_log = ""
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("status", "Queued")
|
||||
self.db_set("error_log", None)
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
log = frappe.db.get_value(
|
||||
"Process Payment Reconciliation Log", filters={"process_pr": self.name}
|
||||
)
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reconciled_count(docname: str | None = None) -> float:
|
||||
current_status = {}
|
||||
if docname:
|
||||
reconcile_log = frappe.db.get_value(
|
||||
"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
|
||||
)
|
||||
if reconcile_log:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": reconcile_log},
|
||||
fields=["reconciled_entries", "total_allocations"],
|
||||
as_list=1,
|
||||
)
|
||||
current_status["processed"], current_status["total"] = res[0]
|
||||
|
||||
return current_status
|
||||
|
||||
|
||||
def get_pr_instance(doc: str):
|
||||
process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
fields = [
|
||||
"company",
|
||||
"party_type",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
]
|
||||
d = {}
|
||||
for field in fields:
|
||||
d[field] = process_payment_reconciliation.get(field)
|
||||
pr.update(d)
|
||||
pr.invoice_limit = 1000
|
||||
pr.payment_limit = 1000
|
||||
return pr
|
||||
|
||||
|
||||
def is_job_running(job_name: str) -> bool:
|
||||
jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
|
||||
for x in jobs:
|
||||
if x.job_name == job_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def pause_job_for_doc(docname: str | None = None):
|
||||
if docname:
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def trigger_job_for_doc(docname: str | None = None):
|
||||
"""
|
||||
Trigger background job
|
||||
"""
|
||||
if not docname:
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
frappe.throw(
|
||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||
get_link_to_form("Accounts Settings", "Accounts Settings")
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not is_scheduler_inactive():
|
||||
if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
||||
job_name = f"start_processing_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||
queue="long",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=docname,
|
||||
)
|
||||
|
||||
elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
|
||||
|
||||
# Resume tasks for running doc
|
||||
job_name = f"start_processing_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||
queue="long",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
doc=docname,
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
|
||||
|
||||
|
||||
def trigger_reconciliation_for_queued_docs():
|
||||
"""
|
||||
Will be called from Cron Job
|
||||
Fetch queued docs and start reconciliation process for each one
|
||||
"""
|
||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
frappe.msgprint(
|
||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||
get_link_to_form("Accounts Settings", "Accounts Settings")
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not is_scheduler_inactive():
|
||||
# Get all queued documents
|
||||
all_queued = frappe.db.get_all(
|
||||
"Process Payment Reconciliation",
|
||||
filters={"docstatus": 1, "status": "Queued"},
|
||||
order_by="creation desc",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
docs_to_trigger = []
|
||||
unique_filters = set()
|
||||
queue_size = 5
|
||||
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
for x in fields:
|
||||
filters += tuple(doc.get(x))
|
||||
return filters
|
||||
|
||||
for x in all_queued:
|
||||
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
||||
filters = get_filters_as_tuple(fields, doc)
|
||||
if filters not in unique_filters:
|
||||
unique_filters.add(filters)
|
||||
docs_to_trigger.append(doc.name)
|
||||
if len(docs_to_trigger) == queue_size:
|
||||
break
|
||||
|
||||
# trigger reconcilation process for queue_size unique filters
|
||||
for doc in docs_to_trigger:
|
||||
trigger_job_for_doc(doc)
|
||||
|
||||
else:
|
||||
frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
|
||||
|
||||
|
||||
def reconcile_based_on_filters(doc: None | str = None) -> None:
|
||||
"""
|
||||
Identify current state of document and execute next tasks in background
|
||||
"""
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if not log:
|
||||
log = frappe.new_doc("Process Payment Reconciliation Log")
|
||||
log.process_pr = doc
|
||||
log.status = "Running"
|
||||
log = log.save()
|
||||
|
||||
job_name = f"process_{doc}_fetch_and_allocate"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
else:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": log},
|
||||
fields=["allocated", "reconciled"],
|
||||
as_list=1,
|
||||
)
|
||||
allocated, reconciled = res[0]
|
||||
|
||||
if not allocated:
|
||||
job_name = f"process__{doc}_fetch_and_allocate"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
elif not reconciled:
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
elif reconciled:
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
def get_next_allocation(log: str) -> list:
|
||||
if log:
|
||||
allocations = []
|
||||
next = frappe.db.get_all(
|
||||
"Process Payment Reconciliation Log Allocations",
|
||||
filters={"parent": log, "reconciled": 0},
|
||||
fields=["reference_type", "reference_name"],
|
||||
order_by="idx",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if next:
|
||||
allocations = frappe.db.get_all(
|
||||
"Process Payment Reconciliation Log Allocations",
|
||||
filters={
|
||||
"parent": log,
|
||||
"reconciled": 0,
|
||||
"reference_type": next[0].reference_type,
|
||||
"reference_name": next[0].reference_name,
|
||||
},
|
||||
fields=["*"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
return allocations
|
||||
return []
|
||||
|
||||
|
||||
def fetch_and_allocate(doc: str) -> None:
|
||||
"""
|
||||
Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
|
||||
"""
|
||||
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if log:
|
||||
if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
|
||||
reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
|
||||
|
||||
pr = get_pr_instance(doc)
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
if len(pr.invoices) > 0 and len(pr.payments) > 0:
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
for x in pr.get("allocation"):
|
||||
reconcile_log.append(
|
||||
"allocations",
|
||||
x.as_dict().update(
|
||||
{
|
||||
"parenttype": "Process Payment Reconciliation Log",
|
||||
"parent": reconcile_log.name,
|
||||
"name": None,
|
||||
"reconciled": False,
|
||||
}
|
||||
),
|
||||
)
|
||||
reconcile_log.allocated = True
|
||||
reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
|
||||
reconcile_log.reconciled_entries = 0
|
||||
reconcile_log.save()
|
||||
|
||||
# generate reconcile job name
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
|
||||
|
||||
def reconcile(doc: None | str = None) -> None:
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if log:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": log},
|
||||
fields=["reconciled_entries", "total_allocations"],
|
||||
as_list=1,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
reconciled_entries, total_allocations = res[0]
|
||||
if reconciled_entries != total_allocations:
|
||||
try:
|
||||
# Fetch next allocation
|
||||
allocations = get_next_allocation(log)
|
||||
|
||||
pr = get_pr_instance(doc)
|
||||
|
||||
# pass allocation to PR instance
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
# reconcile
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
||||
|
||||
# If Payment Entry, update details only for newly linked references
|
||||
# This is for performance
|
||||
if allocations[0].reference_type == "Payment Entry":
|
||||
|
||||
references = [(x.invoice_type, x.invoice_number) for x in allocations]
|
||||
pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
|
||||
pe.flags.ignore_validate_update_after_submit = True
|
||||
pe.set_missing_ref_details(update_ref_details_only_for=references)
|
||||
pe.save()
|
||||
|
||||
# Update reconciled flag
|
||||
allocation_names = [x.name for x in allocations]
|
||||
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
|
||||
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
|
||||
|
||||
# Update reconciled count
|
||||
reconciled_count = frappe.db.count(
|
||||
"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
# Update the parent doc about the exception
|
||||
frappe.db.rollback()
|
||||
|
||||
traceback = frappe.get_traceback()
|
||||
if traceback:
|
||||
message = "Traceback: <br>" + traceback
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"error_log",
|
||||
message,
|
||||
)
|
||||
if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"status",
|
||||
"Partially Reconciled",
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"status",
|
||||
"Failed",
|
||||
)
|
||||
finally:
|
||||
if reconciled_entries == total_allocations:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
else:
|
||||
|
||||
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
|
||||
# trigger next batch in job
|
||||
# generate reconcile job name
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if type(for_filter) == str:
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"status": ["in", ["Running", "Paused"]],
|
||||
"company": for_filter.get("company"),
|
||||
"party_type": for_filter.get("party_type"),
|
||||
"party": for_filter.get("party"),
|
||||
"receivable_payable_account": for_filter.get("receivable_payable_account"),
|
||||
},
|
||||
fieldname="name",
|
||||
)
|
||||
else:
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
|
||||
)
|
||||
return running_doc
|
||||
@@ -0,0 +1,15 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "process_pr",
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Reconciliation Logs"),
|
||||
"items": [
|
||||
"Process Payment Reconciliation Log",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
frappe.listview_settings['Process Payment Reconciliation'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
let colors = {
|
||||
'Queued': 'orange',
|
||||
'Paused': 'orange',
|
||||
'Completed': 'green',
|
||||
'Partially Reconciled': 'orange',
|
||||
'Running': 'blue',
|
||||
'Failed': 'red',
|
||||
};
|
||||
let status = doc.status;
|
||||
return [__(status), colors[status], 'status,=,'+status];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestProcessPaymentReconciliation(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Process Payment Reconciliation Log", {
|
||||
refresh(frm) {
|
||||
if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
|
||||
let progress = 0;
|
||||
if (frm.doc.reconciled_entries != 0) {
|
||||
progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
|
||||
} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
|
||||
progress = 100;
|
||||
}
|
||||
frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:PPR-LOG-{##}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-13 15:00:09.149681",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"process_pr",
|
||||
"section_break_fvdw",
|
||||
"status",
|
||||
"tasks_section",
|
||||
"allocated",
|
||||
"reconciled",
|
||||
"column_break_yhin",
|
||||
"total_allocations",
|
||||
"reconciled_entries",
|
||||
"section_break_4ywv",
|
||||
"error_log",
|
||||
"allocations_section",
|
||||
"allocations"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "allocations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allocations",
|
||||
"options": "Process Payment Reconciliation Log Allocations",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "All allocations have been successfully reconciled",
|
||||
"fieldname": "reconciled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reconciled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_allocations",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Allocations",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Invoices and Payments have been Fetched and Allocated",
|
||||
"fieldname": "allocated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allocated",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reconciled_entries",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled Entries",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tasks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tasks"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Allocations"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yhin",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4ywv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.error_log",
|
||||
"fieldname": "error_log",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Reconciliation Error Log",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "process_pr",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Parent Document",
|
||||
"options": "Process Payment Reconciliation",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fvdw",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:36:26.642617",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProcessPaymentReconciliationLog(Document):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
frappe.listview_settings['Process Payment Reconciliation Log'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
var colors = {
|
||||
'Partially Reconciled': 'orange',
|
||||
'Paused': 'orange',
|
||||
'Reconciled': 'green',
|
||||
'Failed': 'red',
|
||||
'Cancelled': 'red',
|
||||
'Running': 'blue',
|
||||
};
|
||||
let status = doc.status;
|
||||
return [__(status), colors[status], "status,=,"+status];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestProcessPaymentReconciliationLog(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-03-13 13:51:27.351463",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_row",
|
||||
"column_break_3",
|
||||
"invoice_type",
|
||||
"invoice_number",
|
||||
"section_break_6",
|
||||
"allocated_amount",
|
||||
"unreconciled_amount",
|
||||
"column_break_8",
|
||||
"amount",
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"reconciled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"options": "reference_type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Invoice Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Number",
|
||||
"options": "invoice_type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "unreconciled_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Unreconciled Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "is_advance",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Is Advance",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Amount",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Difference Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reconciled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-20 21:05:43.121945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log Allocations",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProcessPaymentReconciliationLogAllocations(Document):
|
||||
pass
|
||||
@@ -15,7 +15,12 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
{% else %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party[0] }}</b></h5>
|
||||
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
{% endif %}
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"terms_and_conditions",
|
||||
"section_break_1",
|
||||
"enable_auto_email",
|
||||
"column_break_ocfq",
|
||||
"sender",
|
||||
"section_break_18",
|
||||
"frequency",
|
||||
"filter_duration",
|
||||
@@ -284,10 +286,32 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Terms and Conditions",
|
||||
"options": "Terms and Conditions"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "include_break",
|
||||
"fieldtype": "Check",
|
||||
"label": "Page Break After Each SoA"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_net_values_in_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Net Values in Party Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sender",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ocfq",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-09-06 21:00:45.732505",
|
||||
"modified": "2023-04-26 12:46:43.645455",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -327,7 +327,7 @@ def send_emails(document_name, from_scheduler=False):
|
||||
queue="short",
|
||||
method=frappe.sendmail,
|
||||
recipients=recipients,
|
||||
sender=frappe.session.user,
|
||||
sender=doc.sender or frappe.session.user,
|
||||
cc=cc,
|
||||
subject=subject,
|
||||
message=message,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_email",
|
||||
"fieldtype": "Read Only",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified": "2023-04-26 13:02:41.964499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
|
||||
@@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
@@ -299,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
apply_tds(frm) {
|
||||
var me = this;
|
||||
|
||||
me.frm.set_value("tax_withheld_vouchers", []);
|
||||
if (!me.frm.doc.apply_tds) {
|
||||
me.frm.set_value("tax_withholding_category", '');
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", 1);
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"column_break8",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"total_advance",
|
||||
@@ -118,6 +119,7 @@
|
||||
"paid_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
@@ -1367,6 +1369,7 @@
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "50px",
|
||||
"ignore_user_permissions": 1,
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
@@ -1550,17 +1553,30 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "allocate_advances_automatically",
|
||||
"description": "Advance payments allocated against orders will only be fetched",
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-28 19:18:56.586321",
|
||||
"modified": "2023-04-29 12:57:50.832598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.validate_expense_account()
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.create_remarks()
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
@@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if (
|
||||
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
|
||||
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
|
||||
and not self.is_return
|
||||
and not self.is_internal_supplier
|
||||
):
|
||||
@@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
|
||||
if self.check_asset_cwip_enabled():
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
@@ -975,6 +976,30 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_asset_gl_entry(self, gl_entries):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
@@ -1363,7 +1388,7 @@ class PurchaseInvoice(BuyingController):
|
||||
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
@@ -1373,7 +1398,9 @@ class PurchaseInvoice(BuyingController):
|
||||
"against": self.supplier,
|
||||
"debit_in_account_currency": self.rounding_adjustment,
|
||||
"debit": self.base_rounding_adjustment,
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else (self.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
@@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||
cur_frm.add_custom_button(__('Payment'),
|
||||
this.make_payment_entry, __('Create'));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
if(doc.docstatus==1 && !doc.is_return) {
|
||||
@@ -331,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
|
||||
make_inter_company_invoice() {
|
||||
let me = this;
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
|
||||
frm: me.frm
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"column_break5",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"total_advance",
|
||||
@@ -120,6 +121,7 @@
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"write_off_section",
|
||||
@@ -2126,6 +2128,19 @@
|
||||
"label": "Repost Required",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "allocate_advances_automatically",
|
||||
"description": "Advance payments allocated against orders will only be fetched",
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company default Cost Center for Round off"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2138,7 +2153,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-03-13 11:43:15.883055",
|
||||
"modified": "2023-04-28 14:15:59.901154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if not self.is_return:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
@@ -1179,7 +1179,12 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
@@ -1194,7 +1199,12 @@ class SalesInvoice(SellingController):
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
@@ -1450,7 +1460,7 @@ class SalesInvoice(SellingController):
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Sales Invoice", self.name
|
||||
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
@@ -1462,7 +1472,9 @@ class SalesInvoice(SellingController):
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else (self.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, getdate
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
@@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
@@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
@@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -301,7 +302,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice":
|
||||
if doctype != "Sales Invoice":
|
||||
filters.update(
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
@@ -568,7 +569,12 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
|
||||
{
|
||||
"supplier": ("in", parties),
|
||||
"apply_tds": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
||||
},
|
||||
"sum(tax_withholding_net_total)",
|
||||
)
|
||||
|
||||
@@ -583,10 +589,10 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - deducted_amount
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
@@ -597,9 +603,9 @@ def is_valid_certificate(
|
||||
):
|
||||
valid = False
|
||||
|
||||
if (
|
||||
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
|
||||
) and certificate_limit > deducted_amount:
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
@@ -110,9 +110,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
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)
|
||||
# Threshold calculation should be only on the Second invoice
|
||||
# Second didn't breach, no TDS should be applied
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"creation": "2023-05-23 09:58:17.235916",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 0,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2023-05-23 13:10:56.227127",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a customer for whom this invoice is being prepared.",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 1,
|
||||
"is_table_field": 0,
|
||||
"label": "Customer",
|
||||
"next_step_condition": "eval: doc.customer",
|
||||
"position": "Right",
|
||||
"title": "Select Customer"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Sales Invoice Item",
|
||||
"description": "Select item that you have sold along with quantity and rate.",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"parent_fieldname": "items",
|
||||
"position": "Top",
|
||||
"title": "Select Item"
|
||||
}
|
||||
],
|
||||
"title": "Sales Invoice"
|
||||
}
|
||||
@@ -472,7 +472,9 @@ def update_accounting_dimensions(round_off_gle):
|
||||
round_off_gle[dimension] = dimension_values.get(dimension)
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||
def get_round_off_account_and_cost_center(
|
||||
company, voucher_type, voucher_no, use_company_default=False
|
||||
):
|
||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||
) or [None, None]
|
||||
@@ -480,7 +482,7 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||
meta = frappe.get_meta(voucher_type)
|
||||
|
||||
# Give first preference to parent cost center for round off GLE
|
||||
if meta.has_field("cost_center"):
|
||||
if not use_company_default and meta.has_field("cost_center"):
|
||||
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
|
||||
if parent_cost_center:
|
||||
round_off_cost_center = parent_cost_center
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
@@ -174,6 +176,9 @@ def _get_party_details(
|
||||
party_type, party.name, "tax_withholding_category"
|
||||
)
|
||||
|
||||
if not party_details.get("tax_category") and pos_profile:
|
||||
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
|
||||
|
||||
return party_details
|
||||
|
||||
|
||||
@@ -256,6 +261,8 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
# required to set correct region
|
||||
frappe.flags.company = company
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_address, shipping_address
|
||||
@@ -642,12 +649,12 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead"):
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type == "Lead":
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
args["customer"] = None
|
||||
del args["lead"]
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
@@ -845,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
return company_wise_info
|
||||
|
||||
|
||||
def get_party_shipping_address(doctype, name):
|
||||
def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
|
||||
and/or `is_shipping_address = 1`.
|
||||
@@ -856,37 +863,41 @@ def get_party_shipping_address(doctype, name):
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"SELECT dl.parent "
|
||||
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
|
||||
"where "
|
||||
"dl.link_doctype=%s "
|
||||
"and dl.link_name=%s "
|
||||
"and dl.parenttype='Address' "
|
||||
"and ifnull(ta.disabled, 0) = 0 and"
|
||||
"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
|
||||
"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
|
||||
(doctype, name),
|
||||
shipping_addresses = frappe.get_all(
|
||||
"Address",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
["disabled", "=", 0],
|
||||
],
|
||||
or_filters=[
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
if out:
|
||||
return out[0][0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
):
|
||||
cond = "1=1"
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date)
|
||||
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
|
||||
else:
|
||||
cond = "posting_date <= '{0}'".format(posting_date)
|
||||
|
||||
if company:
|
||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
||||
|
||||
if party:
|
||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" SELECT party, sum({0}) as amount
|
||||
FROM `tabGL Entry`
|
||||
@@ -898,36 +909,36 @@ def get_partywise_advanced_payment_amount(
|
||||
),
|
||||
party_type,
|
||||
)
|
||||
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
def get_default_contact(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns default contact for the given doctype and name.
|
||||
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
|
||||
Returns contact name only if there is a primary contact for given doctype and name.
|
||||
|
||||
Else returns None
|
||||
|
||||
:param doctype: Party Doctype
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"""
|
||||
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
|
||||
FROM `tabDynamic Link` dl
|
||||
INNER JOIN `tabContact` c ON c.name = dl.parent
|
||||
WHERE
|
||||
dl.link_doctype=%s AND
|
||||
dl.link_name=%s AND
|
||||
dl.parenttype = 'Contact'
|
||||
ORDER BY is_primary_contact DESC, is_billing_contact DESC
|
||||
""",
|
||||
(doctype, name),
|
||||
contacts = frappe.get_all(
|
||||
"Contact",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
],
|
||||
or_filters=[
|
||||
["is_primary_contact", "=", 1],
|
||||
["is_billing_contact", "=", 1],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_primary_contact DESC, is_billing_contact DESC",
|
||||
)
|
||||
if out:
|
||||
try:
|
||||
return out[0][0]
|
||||
except Exception:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return contacts[0] if contacts else None
|
||||
|
||||
|
||||
def add_party_account(party_type, party, company, account):
|
||||
|
||||
@@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
# then consider the payment against original sales invoice.
|
||||
if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
@@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1}
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
|
||||
@@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_payment_against_credit_note(self):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
je.append("accounts", credit_entry)
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
@@ -256,7 +317,7 @@ def make_payment(docname):
|
||||
|
||||
|
||||
def make_credit_note(docname):
|
||||
create_sales_invoice(
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
@@ -269,3 +330,5 @@ def make_credit_note(docname):
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
||||
@@ -31,7 +31,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
def get_data(self, args):
|
||||
self.data = []
|
||||
|
||||
self.receivables = ReceivablePayableReport(self.filters).run(args)[1]
|
||||
|
||||
self.get_party_total(args)
|
||||
@@ -42,6 +41,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.filters.report_date,
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=self.filters.get(scrub(self.party_type)),
|
||||
)
|
||||
or {}
|
||||
)
|
||||
@@ -74,6 +74,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
row.gl_balance = gl_balance_map.get(party)
|
||||
row.diff = flt(row.outstanding) - flt(row.gl_balance)
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
row.remaining_balance = flt(row.outstanding) - flt(row.future_amount)
|
||||
|
||||
self.data.append(row)
|
||||
|
||||
def get_party_total(self, args):
|
||||
@@ -106,6 +109,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
}
|
||||
),
|
||||
@@ -151,6 +155,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
self.setup_ageing_columns()
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.party_type == "Customer":
|
||||
self.add_column(
|
||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
||||
|
||||
@@ -25,6 +25,7 @@ def get_data(filters):
|
||||
["posting_date", "<=", filters.get("to_date")],
|
||||
["against_voucher_type", "=", "Asset"],
|
||||
["account", "in", depreciation_accounts],
|
||||
["is_cancelled", "=", 0],
|
||||
]
|
||||
|
||||
if filters.get("asset"):
|
||||
|
||||
@@ -114,28 +114,6 @@ def get_assets(filters):
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabAsset` a, `tabDepreciation Schedule` ds
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
@@ -160,7 +138,7 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
|
||||
@@ -79,7 +80,7 @@ def get_entries(filters):
|
||||
payment_entries = frappe.db.sql(
|
||||
"""SELECT
|
||||
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||
if(paid_from=%(account)s, paid_amount * -1, received_amount)
|
||||
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
@@ -91,4 +92,65 @@ def get_entries(filters):
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
|
||||
# Loan Disbursement
|
||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_disbursement)
|
||||
.select(
|
||||
ConstantColumn("Loan Disbursement").as_("payment_document_type"),
|
||||
loan_disbursement.name.as_("payment_entry"),
|
||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||
loan_disbursement.reference_number.as_("cheque_no"),
|
||||
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||
loan_disbursement.applicant.as_("against"),
|
||||
-loan_disbursement.disbursed_amount.as_("amount"),
|
||||
)
|
||||
.where(loan_disbursement.docstatus == 1)
|
||||
.where(loan_disbursement.disbursement_date >= filters["from_date"])
|
||||
.where(loan_disbursement.disbursement_date <= filters["to_date"])
|
||||
.where(loan_disbursement.disbursement_account == filters["account"])
|
||||
.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
|
||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
|
||||
if filters.get("to_date"):
|
||||
query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
|
||||
|
||||
loan_disbursements = query.run(as_list=1)
|
||||
|
||||
# Loan Repayment
|
||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_repayment)
|
||||
.select(
|
||||
ConstantColumn("Loan Repayment").as_("payment_document_type"),
|
||||
loan_repayment.name.as_("payment_entry"),
|
||||
loan_repayment.posting_date.as_("posting_date"),
|
||||
loan_repayment.reference_number.as_("cheque_no"),
|
||||
loan_repayment.clearance_date.as_("clearance_date"),
|
||||
loan_repayment.applicant.as_("against"),
|
||||
loan_repayment.amount_paid.as_("amount"),
|
||||
)
|
||||
.where(loan_repayment.docstatus == 1)
|
||||
.where(loan_repayment.posting_date >= filters["from_date"])
|
||||
.where(loan_repayment.posting_date <= filters["to_date"])
|
||||
.where(loan_repayment.payment_account == filters["account"])
|
||||
.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
|
||||
.orderby(loan_repayment.name, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(loan_repayment.posting_date >= filters["from_date"])
|
||||
if filters.get("to_date"):
|
||||
query = query.where(loan_repayment.posting_date <= filters["to_date"])
|
||||
|
||||
loan_repayments = query.run(as_list=1)
|
||||
|
||||
return sorted(
|
||||
journal_entries + payment_entries + loan_disbursements + loan_repayments,
|
||||
key=lambda k: k[2] or getdate(nowdate()),
|
||||
)
|
||||
|
||||
@@ -492,11 +492,26 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions.append("cost_center in %(cost_center)s")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
|
||||
@@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"default": "",
|
||||
"fieldtype": "Autocomplete",
|
||||
options: Object.keys(frappe.boot.party_account_types),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
}
|
||||
@@ -177,7 +176,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check"
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_cancelled_entries",
|
||||
|
||||
@@ -244,13 +244,23 @@ def get_conditions(filters):
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("include_default_book_entries"):
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("finance_book in (%(finance_book)s)")
|
||||
conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
if filters.get("finance_book"):
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if not filters.get("show_cancelled_entries"):
|
||||
conditions.append("is_cancelled = 0")
|
||||
|
||||
@@ -24,7 +24,6 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"account_currency": "USD",
|
||||
"inter_company_account": 0,
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
"account_type": "Bank",
|
||||
"doctype": "Account",
|
||||
|
||||
@@ -125,12 +125,14 @@ def get_revenue(data, period_list, include_in_gross=1):
|
||||
|
||||
data_to_be_removed = True
|
||||
while data_to_be_removed:
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list)
|
||||
revenue = adjust_account(revenue, period_list)
|
||||
revenue, data_to_be_removed = remove_parent_with_no_child(revenue)
|
||||
|
||||
adjust_account_totals(revenue, period_list)
|
||||
|
||||
return copy.deepcopy(revenue)
|
||||
|
||||
|
||||
def remove_parent_with_no_child(data, period_list):
|
||||
def remove_parent_with_no_child(data):
|
||||
data_to_be_removed = False
|
||||
for parent in data:
|
||||
if "is_group" in parent and parent.get("is_group") == 1:
|
||||
@@ -147,16 +149,19 @@ def remove_parent_with_no_child(data, period_list):
|
||||
return data, data_to_be_removed
|
||||
|
||||
|
||||
def adjust_account(data, period_list, consolidated=False):
|
||||
leaf_nodes = [item for item in data if item["is_group"] == 0]
|
||||
def adjust_account_totals(data, period_list):
|
||||
totals = {}
|
||||
for node in leaf_nodes:
|
||||
set_total(node, node["total"], data, totals)
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
for d in reversed(data):
|
||||
if d.get("is_group"):
|
||||
for period in period_list:
|
||||
# reset totals for group accounts as totals set by get_data doesn't consider include_in_gross check
|
||||
d[period.key] = sum(
|
||||
item[period.key] for item in data if item.get("parent_account") == d.get("account")
|
||||
)
|
||||
else:
|
||||
set_total(d, d["total"], data, totals)
|
||||
|
||||
d["total"] = totals[d["account"]]
|
||||
|
||||
|
||||
def set_total(node, value, complete_list, totals):
|
||||
@@ -191,6 +196,9 @@ def get_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
@@ -229,6 +237,9 @@ def get_net_profit(
|
||||
|
||||
if profit_loss[key]:
|
||||
has_value = True
|
||||
if not profit_loss.get("total"):
|
||||
profit_loss["total"] = 0
|
||||
profit_loss["total"] += profit_loss[key]
|
||||
|
||||
if has_value:
|
||||
return profit_loss
|
||||
|
||||
@@ -59,7 +59,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
column._options = "";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
@@ -250,7 +251,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "warehouse",
|
||||
"options": "Warehouse",
|
||||
"width": 100,
|
||||
},
|
||||
"qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80},
|
||||
@@ -305,7 +306,8 @@ def get_columns(group_wise_columns, filters):
|
||||
"sales_person": {
|
||||
"label": _("Sales Person"),
|
||||
"fieldname": "sales_person",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Person",
|
||||
"width": 100,
|
||||
},
|
||||
"allocated_amount": {
|
||||
@@ -326,14 +328,14 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "customer",
|
||||
"options": "Customer Group",
|
||||
"width": 100,
|
||||
},
|
||||
"territory": {
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "territory",
|
||||
"options": "Territory",
|
||||
"width": 100,
|
||||
},
|
||||
"monthly": {
|
||||
@@ -735,7 +737,7 @@ class GrossProfitGenerator(object):
|
||||
def load_invoice_items(self):
|
||||
conditions = ""
|
||||
if self.filters.company:
|
||||
conditions += " and company = %(company)s"
|
||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
||||
if self.filters.from_date:
|
||||
conditions += " and posting_date >= %(from_date)s"
|
||||
if self.filters.to_date:
|
||||
@@ -848,30 +850,30 @@ class GrossProfitGenerator(object):
|
||||
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
|
||||
"""
|
||||
|
||||
parents = []
|
||||
grouped = OrderedDict()
|
||||
|
||||
for row in self.si_list:
|
||||
if row.parent not in parents:
|
||||
parents.append(row.parent)
|
||||
# initialize list with a header row for each new parent
|
||||
grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
|
||||
row.update(
|
||||
{"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
|
||||
) # descendant rows will have indent: 1.0 or greater
|
||||
)
|
||||
|
||||
parents_index = 0
|
||||
for index, row in enumerate(self.si_list):
|
||||
if parents_index < len(parents) and row.parent == parents[parents_index]:
|
||||
invoice = self.get_invoice_row(row)
|
||||
self.si_list.insert(index, invoice)
|
||||
parents_index += 1
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
|
||||
else:
|
||||
# skipping the bundle items rows
|
||||
if not row.indent:
|
||||
row.indent = 1.0
|
||||
row.parent_invoice = row.parent
|
||||
row.invoice_or_item = row.item_code
|
||||
self.si_list.clear()
|
||||
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
self.add_bundle_items(row, index)
|
||||
for items in grouped.values():
|
||||
self.si_list.extend(items)
|
||||
|
||||
def get_invoice_row(self, row):
|
||||
# header row format
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": "",
|
||||
@@ -900,13 +902,6 @@ class GrossProfitGenerator(object):
|
||||
}
|
||||
)
|
||||
|
||||
def add_bundle_items(self, product_bundle, index):
|
||||
bundle_items = self.get_bundle_items(product_bundle)
|
||||
|
||||
for i, item in enumerate(bundle_items):
|
||||
bundle_item = self.get_bundle_item_row(product_bundle, item)
|
||||
self.si_list.insert((index + i + 1), bundle_item)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
|
||||
@@ -19,14 +19,19 @@ def execute(filters=None):
|
||||
return _execute(filters)
|
||||
|
||||
|
||||
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
|
||||
def _execute(
|
||||
filters=None,
|
||||
additional_table_columns=None,
|
||||
additional_query_columns=None,
|
||||
additional_conditions=None,
|
||||
):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns(additional_table_columns, filters)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, additional_query_columns)
|
||||
item_list = get_items(filters, additional_query_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
|
||||
for opts in (
|
||||
@@ -341,6 +346,9 @@ def get_conditions(filters):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
@@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
|
||||
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = ", " + ", ".join(additional_query_columns)
|
||||
@@ -391,8 +399,9 @@ def get_items(filters, additional_query_columns):
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
|
||||
@@ -37,6 +37,29 @@ function get_filters() {
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"default": "",
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname":"voucher_no",
|
||||
"label": __("Voucher No"),
|
||||
@@ -49,6 +72,20 @@ function get_filters() {
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname":"include_account_currency",
|
||||
"label": __("Include Account Currency"),
|
||||
"fieldtype": "Check",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname":"group_party",
|
||||
"label": __("Group by Party"),
|
||||
"fieldtype": "Check",
|
||||
"width": 100,
|
||||
},
|
||||
|
||||
|
||||
|
||||
]
|
||||
return filters;
|
||||
|
||||
@@ -17,34 +17,26 @@ class PaymentLedger(object):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
def init_voucher_dict(self):
|
||||
|
||||
if self.voucher_amount:
|
||||
s = set()
|
||||
# build a set of unique vouchers
|
||||
# for each ple, using group_by_key to create a key and assign it to +/- list
|
||||
for ple in self.voucher_amount:
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
s.add(key)
|
||||
group_by_key = None
|
||||
if not self.filters.group_party:
|
||||
group_by_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
else:
|
||||
group_by_key = (ple.party_type, ple.party)
|
||||
|
||||
# for each unique vouchers, initialize +/- list
|
||||
for key in s:
|
||||
self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list())
|
||||
|
||||
# for each ple, using against voucher and amount, assign it to +/- list
|
||||
# group by against voucher
|
||||
for ple in self.voucher_amount:
|
||||
against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
target = None
|
||||
if self.voucher_dict.get(against_key):
|
||||
if ple.amount > 0:
|
||||
target = self.voucher_dict.get(against_key).increase
|
||||
else:
|
||||
target = self.voucher_dict.get(against_key).decrease
|
||||
if ple.amount > 0:
|
||||
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("increase", [])
|
||||
else:
|
||||
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("decrease", [])
|
||||
|
||||
# this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple)
|
||||
# need to somehow include the stray entries as well.
|
||||
if target is not None:
|
||||
entry = frappe._dict(
|
||||
company=ple.company,
|
||||
posting_date=ple.posting_date,
|
||||
account=ple.account,
|
||||
party_type=ple.party_type,
|
||||
party=ple.party,
|
||||
@@ -66,10 +58,10 @@ class PaymentLedger(object):
|
||||
|
||||
for value in self.voucher_dict.values():
|
||||
voucher_data = []
|
||||
if value.increase != []:
|
||||
voucher_data.extend(value.increase)
|
||||
if value.decrease != []:
|
||||
voucher_data.extend(value.decrease)
|
||||
if value.get("increase"):
|
||||
voucher_data.extend(value.get("increase"))
|
||||
if value.get("decrease"):
|
||||
voucher_data.extend(value.get("decrease"))
|
||||
|
||||
if voucher_data:
|
||||
# balance row
|
||||
@@ -117,6 +109,12 @@ class PaymentLedger(object):
|
||||
if self.filters.against_voucher_no:
|
||||
self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no)
|
||||
|
||||
if self.filters.party_type:
|
||||
self.conditions.append(self.ple.party_type == self.filters.party_type)
|
||||
|
||||
if self.filters.party:
|
||||
self.conditions.append(self.ple.party.isin(self.filters.party))
|
||||
|
||||
def get_data(self):
|
||||
ple = self.ple
|
||||
|
||||
@@ -134,7 +132,13 @@ class PaymentLedger(object):
|
||||
def get_columns(self):
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100")
|
||||
dict(
|
||||
label=_("Posting Date"),
|
||||
fieldname="posting_date",
|
||||
fieldtype="Date",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
@@ -160,7 +164,11 @@ class PaymentLedger(object):
|
||||
)
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100"
|
||||
label=_("Voucher No"),
|
||||
fieldname="voucher_no",
|
||||
fieldtype="Dynamic Link",
|
||||
options="voucher_type",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
@@ -176,8 +184,8 @@ class PaymentLedger(object):
|
||||
dict(
|
||||
label=_("Against Voucher No"),
|
||||
fieldname="against_voucher_no",
|
||||
fieldtype="data",
|
||||
options=options,
|
||||
fieldtype="Dynamic Link",
|
||||
options="against_voucher_type",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
@@ -209,7 +217,7 @@ class PaymentLedger(object):
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
|
||||
# initialize dictionary and group using against voucher
|
||||
# initialize dictionary and group using key
|
||||
self.init_voucher_dict()
|
||||
|
||||
# convert dictionary to list and add balance rows
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -66,12 +65,6 @@ def get_result(
|
||||
else:
|
||||
total_amount_credited += entry.credit
|
||||
|
||||
## Check if ldc is applied and show rate as per ldc
|
||||
actual_rate = (tds_deducted / total_amount_credited) * 100
|
||||
|
||||
if flt(actual_rate) < flt(rate):
|
||||
rate = actual_rate
|
||||
|
||||
if tds_deducted:
|
||||
row = {
|
||||
"pan"
|
||||
|
||||
@@ -157,12 +157,25 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if filters.project:
|
||||
additional_conditions += " and project = %(project)s"
|
||||
|
||||
company_fb = frappe.db.get_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if company_fb and cstr(filters.get("finance_book")) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(company_fb)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in ('') OR finance_book IS NULL)"
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
@@ -174,7 +187,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
"finance_book": filters.finance_book,
|
||||
"company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"),
|
||||
"company_fb": company_fb,
|
||||
}
|
||||
|
||||
if accounting_dimensions:
|
||||
|
||||
@@ -420,7 +420,7 @@ def add_cc(args=None):
|
||||
return cc.name
|
||||
|
||||
|
||||
def reconcile_against_document(args): # nosemgrep
|
||||
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
|
||||
"""
|
||||
Cancel PE or JV, Update against document, split if required and resubmit
|
||||
"""
|
||||
@@ -449,13 +449,9 @@ def reconcile_against_document(args): # nosemgrep
|
||||
if voucher_type == "Journal Entry":
|
||||
update_reference_in_journal_entry(entry, doc, do_not_save=True)
|
||||
else:
|
||||
update_reference_in_payment_entry(entry, doc, do_not_save=True)
|
||||
|
||||
if doc.doctype == "Journal Entry":
|
||||
try:
|
||||
doc.validate_total_debit_and_credit()
|
||||
except Exception as validation_exception:
|
||||
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
|
||||
update_reference_in_payment_entry(
|
||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
@@ -592,7 +588,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
journal_entry.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
def update_reference_in_payment_entry(
|
||||
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
|
||||
):
|
||||
reference_details = {
|
||||
"reference_doctype": d.against_voucher_type,
|
||||
"reference_name": d.against_voucher,
|
||||
@@ -638,6 +636,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
|
||||
payment_entry.set_gain_or_loss(account_details=account_details)
|
||||
|
||||
payment_entry.flags.ignore_validate_update_after_submit = True
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
payment_entry.set_amounts()
|
||||
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
|
||||
@@ -1358,10 +1363,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
|
||||
total_stock_value = 0.0
|
||||
for warehouse in related_warehouses:
|
||||
value = get_stock_value_on(warehouse, posting_date)
|
||||
total_stock_value += value
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||
|
||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"label": "Profit and Loss"
|
||||
}
|
||||
],
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]",
|
||||
"content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:41:59.515192",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
@@ -1093,10 +1094,11 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-24 05:41:09.236458",
|
||||
"modified": "2023-05-30 13:23:29.316711",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
|
||||
@@ -469,6 +469,9 @@ frappe.ui.form.on('Asset', {
|
||||
} else {
|
||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||
}
|
||||
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
|
||||
frm.set_value('available_for_use_date', frm.doc.purchase_date);
|
||||
}
|
||||
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
doctype_field = frappe.scrub(doctype)
|
||||
|
||||
@@ -81,6 +81,9 @@
|
||||
"options": "ACC-ASS-.YYYY.-"
|
||||
},
|
||||
{
|
||||
"depends_on": "item_code",
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@@ -527,7 +530,7 @@
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2023-01-25 17:45:48.649543",
|
||||
"modified": "2023-03-30 15:07:41.542374",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -571,4 +574,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
is_first_day_of_the_month,
|
||||
is_last_day_of_the_month,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
@@ -294,17 +295,42 @@ class Asset(AccountsController):
|
||||
if has_pro_rata:
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
has_wdv_or_dd_non_yearly_pro_rata = False
|
||||
if (
|
||||
finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
|
||||
and cint(finance_book.frequency_of_depreciation) != 12
|
||||
):
|
||||
has_wdv_or_dd_non_yearly_pro_rata = self.check_is_pro_rata(
|
||||
finance_book, wdv_or_dd_non_yearly=True
|
||||
)
|
||||
|
||||
skip_row = False
|
||||
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
|
||||
|
||||
depreciation_amount = 0
|
||||
|
||||
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
continue
|
||||
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
||||
if n > 0 and len(self.get("schedules")) > n - 1:
|
||||
prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount
|
||||
else:
|
||||
prev_depreciation_amount = 0
|
||||
|
||||
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
self,
|
||||
value_after_depreciation,
|
||||
finance_book,
|
||||
n,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
if not has_pro_rata or (
|
||||
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
|
||||
):
|
||||
schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
@@ -312,15 +338,14 @@ class Asset(AccountsController):
|
||||
if should_get_last_day:
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
from_date = self.get_from_date_for_disposal(finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, from_date, date_of_disposal
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
date_of_disposal,
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@@ -335,17 +360,41 @@ class Asset(AccountsController):
|
||||
break
|
||||
|
||||
# For first row
|
||||
if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
|
||||
if (
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
self.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
finance_book.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
if not is_first_day_of_the_month(getdate(self.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(self.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
from_date,
|
||||
finance_book.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
# For first depr schedule date will be the start date
|
||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
@@ -359,40 +408,35 @@ class Asset(AccountsController):
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, schedule_date, self.to_date
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
schedule_date,
|
||||
self.to_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(
|
||||
depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book
|
||||
)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
last_schedule_date = schedule_date
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if (
|
||||
finance_book.expected_value_after_useful_life
|
||||
and (
|
||||
(
|
||||
n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life
|
||||
)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life
|
||||
)
|
||||
and (
|
||||
not self.flags.increase_in_asset_value_due_to_repair
|
||||
or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
|
||||
if finance_book.expected_value_after_useful_life and (
|
||||
(
|
||||
n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life
|
||||
)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life
|
||||
):
|
||||
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0:
|
||||
self._add_depreciation_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
@@ -467,16 +511,19 @@ class Asset(AccountsController):
|
||||
|
||||
return start
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
def get_from_date_for_disposal(self, finance_book):
|
||||
if not self.get("schedules"):
|
||||
return self.available_for_use_date
|
||||
return add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
if len(self.finance_books) == 1:
|
||||
return self.schedules[-1].schedule_date
|
||||
|
||||
from_date = ""
|
||||
for schedule in self.get("schedules"):
|
||||
if schedule.finance_book == finance_book:
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
from_date = schedule.schedule_date
|
||||
|
||||
if from_date:
|
||||
@@ -486,28 +533,37 @@ class Asset(AccountsController):
|
||||
return add_days(self.available_for_use_date, -1)
|
||||
|
||||
# if it returns True, depreciation_amount will not be equal for the first and last rows
|
||||
def check_is_pro_rata(self, row):
|
||||
def check_is_pro_rata(self, row, wdv_or_dd_non_yearly=False):
|
||||
has_pro_rata = False
|
||||
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = self.get_modified_available_for_use_date(row)
|
||||
from_date = self.get_modified_available_for_use_date(row, wdv_or_dd_non_yearly)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
|
||||
# if frequency_of_depreciation is 12 months, total_days = 365
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if wdv_or_dd_non_yearly:
|
||||
total_days = get_total_days(row.depreciation_start_date, 12)
|
||||
else:
|
||||
# if frequency_of_depreciation is 12 months, total_days = 365
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
|
||||
if days < total_days:
|
||||
has_pro_rata = True
|
||||
|
||||
return has_pro_rata
|
||||
|
||||
def get_modified_available_for_use_date(self, row):
|
||||
return add_months(
|
||||
self.available_for_use_date,
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
def get_modified_available_for_use_date(self, row, wdv_or_dd_non_yearly=False):
|
||||
if wdv_or_dd_non_yearly:
|
||||
return add_months(
|
||||
self.available_for_use_date,
|
||||
(self.number_of_depreciations_booked * 12),
|
||||
)
|
||||
else:
|
||||
return add_months(
|
||||
self.available_for_use_date,
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
def validate_asset_finance_books(self, row):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
@@ -910,20 +966,51 @@ class Asset(AccountsController):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
if args.get("depreciation_method") == "Double Declining Balance":
|
||||
return 200.0 / args.get("total_number_of_depreciations")
|
||||
return 200.0 / (
|
||||
(
|
||||
flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
|
||||
)
|
||||
/ 12
|
||||
)
|
||||
|
||||
if args.get("depreciation_method") == "Written Down Value":
|
||||
if args.get("rate_of_depreciation") and on_validate:
|
||||
if (
|
||||
args.get("rate_of_depreciation")
|
||||
and on_validate
|
||||
and not self.flags.increase_in_asset_value_due_to_repair
|
||||
):
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
if self.flags.increase_in_asset_value_due_to_repair:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(
|
||||
args.get("value_after_depreciation")
|
||||
)
|
||||
else:
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
|
||||
depreciation_rate = math.pow(
|
||||
value,
|
||||
1.0
|
||||
/ (
|
||||
(
|
||||
flt(args.get("total_number_of_depreciations"), 2)
|
||||
* flt(args.get("frequency_of_depreciation"))
|
||||
)
|
||||
/ 12
|
||||
),
|
||||
)
|
||||
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
|
||||
def get_pro_rata_amt(
|
||||
self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
|
||||
):
|
||||
days = date_diff(to_date, from_date)
|
||||
months = month_diff(to_date, from_date)
|
||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
total_days = get_total_days(to_date, 12)
|
||||
else:
|
||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
||||
|
||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||
|
||||
@@ -1180,27 +1267,74 @@ def get_total_days(date, frequency):
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
def get_depreciation_amount(
|
||||
asset,
|
||||
depreciable_value,
|
||||
row,
|
||||
schedule_idx=0,
|
||||
prev_depreciation_amount=0,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
depreciation_amount = (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
return get_straight_line_or_manual_depr_amount(asset, row)
|
||||
else:
|
||||
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
||||
return get_wdv_or_dd_depr_amount(
|
||||
depreciable_value,
|
||||
row.rate_of_depreciation,
|
||||
row.frequency_of_depreciation,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
|
||||
return depreciation_amount
|
||||
|
||||
def get_straight_line_or_manual_depr_amount(asset, row):
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
|
||||
date_diff(asset.to_date, asset.available_for_use_date) / 365
|
||||
)
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
frequency_of_depreciation,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
):
|
||||
if cint(frequency_of_depreciation) == 12:
|
||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
||||
else:
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
if schedule_idx == 0:
|
||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
||||
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
|
||||
return (
|
||||
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
|
||||
)
|
||||
else:
|
||||
return prev_depreciation_amount
|
||||
else:
|
||||
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
|
||||
return (
|
||||
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
|
||||
)
|
||||
else:
|
||||
return prev_depreciation_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
|
||||
}
|
||||
},
|
||||
onload: function(me) {
|
||||
me.page.add_action_item('Make Asset Movement', function() {
|
||||
me.page.add_action_item(__("Make Asset Movement"), function() {
|
||||
const assets = me.get_checked_items();
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_months, cint, flt, get_last_day, getdate, nowdate, today
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
nowdate,
|
||||
today,
|
||||
)
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
@@ -126,6 +135,7 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
@@ -218,10 +228,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
|
||||
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
||||
|
||||
message = (
|
||||
_("Hi,")
|
||||
+ "<br>"
|
||||
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
|
||||
_("Hello,")
|
||||
+ "<br><br>"
|
||||
+ _("The following assets have failed to automatically post depreciation entries: {0}").format(
|
||||
asset_links
|
||||
)
|
||||
+ "."
|
||||
+ "<br><br>"
|
||||
+ _(
|
||||
"Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
|
||||
)
|
||||
)
|
||||
|
||||
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||
@@ -265,7 +281,7 @@ def scrap_asset(asset_name):
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
for entry in get_gl_entries_on_asset_disposal(asset):
|
||||
for entry in get_gl_entries_on_asset_disposal(asset, date):
|
||||
entry.update({"reference_type": "Asset", "reference_name": asset_name})
|
||||
je.append("accounts", entry)
|
||||
|
||||
@@ -388,8 +404,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -407,7 +426,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -417,7 +436,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -426,7 +445,7 @@ def get_gl_entries_on_asset_regain(
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@@ -438,8 +457,11 @@ def get_gl_entries_on_asset_regain(
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_disposal(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -457,7 +479,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -467,7 +489,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -476,7 +498,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@@ -510,8 +532,12 @@ def get_asset_details(asset, finance_book=None):
|
||||
|
||||
|
||||
def get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||
):
|
||||
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append(
|
||||
asset.get_gl_dict(
|
||||
@@ -520,7 +546,7 @@ def get_profit_gl_entries(
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
)
|
||||
@@ -574,3 +600,9 @@ def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
def is_first_day_of_the_month(date):
|
||||
first_day_of_the_month = get_first_day(date)
|
||||
|
||||
return getdate(first_day_of_the_month) == getdate(date)
|
||||
|
||||
@@ -327,6 +327,79 @@ class TestAsset(AssetSetup):
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_gle_made_by_asset_sale_for_existing_asset(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-04-01",
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_depr_values = [
|
||||
["2023-03-31", 12000, 36000],
|
||||
["2024-03-31", 12000, 48000],
|
||||
["2025-03-31", 12000, 60000],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
|
||||
post_depreciation_entries(date="2023-03-31")
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
|
||||
)
|
||||
asset.load_from_db()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"_Test Fixed Asset - _TC",
|
||||
0.0,
|
||||
60000.0,
|
||||
),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
@@ -649,7 +722,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
|
||||
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in asset.get("schedules")
|
||||
@@ -693,14 +766,14 @@ class TestDepreciationMethods(AssetSetup):
|
||||
number_of_depreciations_booked=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2030-12-31",
|
||||
depreciation_start_date="2031-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=12,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
|
||||
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]]
|
||||
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
@@ -818,12 +891,12 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 647.25, 647.25],
|
||||
["2022-03-31", 1210.71, 1857.96],
|
||||
["2022-04-30", 1053.99, 2911.95],
|
||||
["2022-05-31", 917.55, 3829.5],
|
||||
["2022-06-30", 798.77, 4628.27],
|
||||
["2022-07-15", 371.73, 5000.0],
|
||||
["2022-02-28", 310.89, 310.89],
|
||||
["2022-03-31", 654.45, 965.34],
|
||||
["2022-04-30", 654.45, 1619.79],
|
||||
["2022-05-31", 654.45, 2274.24],
|
||||
["2022-06-30", 654.45, 2928.69],
|
||||
["2022-07-15", 2071.31, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@@ -1453,7 +1526,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
@@ -1466,12 +1539,68 @@ class TestDepreciationBasics(AssetSetup):
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 99900)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_for_depreciable_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
jv.voucher_type = "Depreciation Entry"
|
||||
jv.insert()
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
d.account_type = "Depreciation"
|
||||
jv.voucher_type = "Journal Entry"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv.insert)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
|
||||
@@ -436,6 +436,7 @@ class AssetCapitalization(StockController):
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
@@ -96,7 +96,6 @@ class AssetCategory(Document):
|
||||
frappe.throw(msg, title=_("Missing Account"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_category_account(
|
||||
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
||||
):
|
||||
|
||||
@@ -84,6 +84,8 @@ def calculate_next_due_date(
|
||||
next_due_date = add_years(start_date, 1)
|
||||
if periodicity == "2 Yearly":
|
||||
next_due_date = add_years(start_date, 2)
|
||||
if periodicity == "3 Yearly":
|
||||
next_due_date = add_years(start_date, 3)
|
||||
if periodicity == "Quarterly":
|
||||
next_due_date = add_months(start_date, 3)
|
||||
if end_date and (
|
||||
|
||||
@@ -1,664 +1,156 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "",
|
||||
"beta": 0,
|
||||
"creation": "2017-10-20 07:10:55.903571",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2017-10-20 07:10:55.903571",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"maintenance_task",
|
||||
"maintenance_type",
|
||||
"column_break_2",
|
||||
"maintenance_status",
|
||||
"section_break_2",
|
||||
"start_date",
|
||||
"periodicity",
|
||||
"column_break_4",
|
||||
"end_date",
|
||||
"certificate_required",
|
||||
"section_break_9",
|
||||
"assign_to",
|
||||
"column_break_10",
|
||||
"assign_to_name",
|
||||
"section_break_10",
|
||||
"next_due_date",
|
||||
"column_break_14",
|
||||
"last_completion_date",
|
||||
"section_break_7",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "maintenance_task",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Maintenance Task",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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": "maintenance_task",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Maintenance Task",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "maintenance_type",
|
||||
"fieldtype": "Select",
|
||||
"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": "Maintenance Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Preventive Maintenance\nCalibration",
|
||||
"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
|
||||
},
|
||||
"fieldname": "maintenance_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Maintenance Type",
|
||||
"options": "Preventive Maintenance\nCalibration"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column 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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "maintenance_status",
|
||||
"fieldtype": "Select",
|
||||
"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": "Maintenance Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Planned\nOverdue\nCancelled",
|
||||
"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": "maintenance_status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Maintenance Status",
|
||||
"options": "Planned\nOverdue\nCancelled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_2",
|
||||
"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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Today",
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"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": "Start Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "Today",
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "periodicity",
|
||||
"fieldtype": "Select",
|
||||
"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": "Periodicity",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
|
||||
"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": "periodicity",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Periodicity",
|
||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column 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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"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": "End Date",
|
||||
"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
|
||||
},
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "End Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "certificate_required",
|
||||
"fieldtype": "Check",
|
||||
"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": "Certificate Required",
|
||||
"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": 1,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "certificate_required",
|
||||
"fieldtype": "Check",
|
||||
"label": "Certificate Required",
|
||||
"search_index": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_9",
|
||||
"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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "assign_to",
|
||||
"fieldtype": "Link",
|
||||
"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": "Assign To",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"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
|
||||
},
|
||||
"fieldname": "assign_to",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Assign To",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column 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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "assign_to.full_name",
|
||||
"fieldname": "assign_to_name",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "Assign to Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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
|
||||
},
|
||||
"fieldname": "assign_to_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Assign to Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_10",
|
||||
"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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "next_due_date",
|
||||
"fieldtype": "Date",
|
||||
"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": "Next Due Date",
|
||||
"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
|
||||
},
|
||||
"fieldname": "next_due_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Next Due Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column 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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "last_completion_date",
|
||||
"fieldtype": "Date",
|
||||
"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": "Last Completion Date",
|
||||
"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
|
||||
},
|
||||
"fieldname": "last_completion_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Last Completion Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 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,
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"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": "Description",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-06-18 16:12:04.330021",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Task",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-23 07:03:07.113452",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Task",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -56,8 +56,6 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
self.asset_doc.prepare_depreciation_data()
|
||||
if self.asset_doc.calculate_depreciation:
|
||||
self.update_asset_expected_value_after_useful_life()
|
||||
self.asset_doc.save()
|
||||
|
||||
def before_cancel(self):
|
||||
@@ -81,8 +79,6 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
self.asset_doc.prepare_depreciation_data()
|
||||
if self.asset_doc.calculate_depreciation:
|
||||
self.update_asset_expected_value_after_useful_life()
|
||||
self.asset_doc.save()
|
||||
|
||||
def after_delete(self):
|
||||
@@ -103,26 +99,6 @@ class AssetRepair(AccountsController):
|
||||
title=_("Missing Warehouse"),
|
||||
)
|
||||
|
||||
def update_asset_expected_value_after_useful_life(self):
|
||||
for row in self.asset_doc.get("finance_books"):
|
||||
if row.depreciation_method in ("Written Down Value", "Double Declining Balance"):
|
||||
accumulated_depreciation_after_full_schedule = [
|
||||
d.accumulated_depreciation_amount
|
||||
for d in self.asset_doc.get("schedules")
|
||||
if cint(d.finance_book_id) == row.idx
|
||||
]
|
||||
|
||||
accumulated_depreciation_after_full_schedule = max(
|
||||
accumulated_depreciation_after_full_schedule
|
||||
)
|
||||
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(row.value_after_depreciation) - flt(accumulated_depreciation_after_full_schedule),
|
||||
row.precision("expected_value_after_useful_life"),
|
||||
)
|
||||
|
||||
row.expected_value_after_useful_life = asset_value_after_full_schedule
|
||||
|
||||
def increase_asset_value(self):
|
||||
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
|
||||
frm.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
|
||||
args: {
|
||||
asset: frm.doc.asset,
|
||||
asset_name: frm.doc.asset,
|
||||
finance_book: frm.doc.finance_book
|
||||
},
|
||||
callback: function(r) {
|
||||
|
||||
@@ -116,7 +116,9 @@ class AssetValueAdjustment(Document):
|
||||
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)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
|
||||
total_days
|
||||
)
|
||||
from_date = self.date
|
||||
else:
|
||||
no_of_depreciations = len(
|
||||
|
||||
@@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"default": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book"
|
||||
},
|
||||
{
|
||||
fieldname:"cost_center",
|
||||
label: __("Cost Center"),
|
||||
@@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"is_existing_asset",
|
||||
label: __("Is Existing Asset"),
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
depends_on: "eval: doc.filter_by_finance_book == 1",
|
||||
},
|
||||
{
|
||||
fieldname:"filter_by_finance_book",
|
||||
label: __("Filter by Finance Book"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
{
|
||||
fieldname:"only_existing_assets",
|
||||
label: __("Only existing assets"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
]
|
||||
|
||||
@@ -45,8 +45,8 @@ def get_conditions(filters):
|
||||
filters.year_end_date = getdate(fiscal_year.year_end_date)
|
||||
|
||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||
if filters.get("is_existing_asset"):
|
||||
conditions["is_existing_asset"] = filters.get("is_existing_asset")
|
||||
if filters.get("only_existing_assets"):
|
||||
conditions["is_existing_asset"] = filters.get("only_existing_assets")
|
||||
if filters.get("asset_category"):
|
||||
conditions["asset_category"] = filters.get("asset_category")
|
||||
if filters.get("cost_center"):
|
||||
@@ -102,19 +102,18 @@ def get_data(filters):
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
assets_linked_to_fb = None
|
||||
|
||||
if filters.filter_by_finance_book:
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
for asset in assets_record:
|
||||
if filters.finance_book:
|
||||
if asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
else:
|
||||
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
row = {
|
||||
@@ -172,11 +171,11 @@ def prepare_chart_data(data, filters):
|
||||
"datasets": [
|
||||
{
|
||||
"name": _("Asset Value"),
|
||||
"values": [d.get("asset_value") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
{
|
||||
"name": _("Depreciatied Amount"),
|
||||
"values": [d.get("depreciated_amount") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -310,7 +309,7 @@ def get_columns(filters):
|
||||
|
||||
return [
|
||||
{
|
||||
"label": _("Asset Id"),
|
||||
"label": _("Asset ID"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "asset_id",
|
||||
"options": "Asset",
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
],
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:43:27.634865",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "assets",
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Assets",
|
||||
"links": [
|
||||
{
|
||||
@@ -183,13 +185,15 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-04 12:15:54.839454",
|
||||
"modified": "2023-05-24 14:47:20.243146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Assets",
|
||||
"number_cards": [],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"parent_page": "Accounting",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 4.0,
|
||||
@@ -216,4 +220,4 @@
|
||||
}
|
||||
],
|
||||
"title": "Assets"
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
this.make_purchase_invoice, __('Create'));
|
||||
|
||||
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
if(flt(doc.per_billed) < 100) {
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"party_account_currency",
|
||||
"inter_company_order_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"dashboard"
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -495,6 +495,7 @@
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"oldfieldname": "po_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Order Item",
|
||||
@@ -1100,8 +1101,7 @@
|
||||
{
|
||||
"fieldname": "before_items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Items"
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_col_break",
|
||||
@@ -1184,12 +1184,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "dashboard",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -1265,13 +1259,19 @@
|
||||
"fieldname": "shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-28 18:59:16.322824",
|
||||
"modified": "2023-05-24 11:16:41.195340",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user