mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-14 18:58:49 +00:00
Compare commits
836 Commits
mariadb_li
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec62a652c | ||
|
|
c95b8e8d30 | ||
|
|
b1d91f429c | ||
|
|
4e86a46008 | ||
|
|
4bdb4fb170 | ||
|
|
06a999ebaa | ||
|
|
5663c2a1ca | ||
|
|
13e3db3730 | ||
|
|
bba77529f8 | ||
|
|
625321ba8a | ||
|
|
9ece6ebef8 | ||
|
|
155bdd0251 | ||
|
|
d117411070 | ||
|
|
aaa4f0ae26 | ||
|
|
170fe86f38 | ||
|
|
aedb171dd4 | ||
|
|
1fbde7b8c6 | ||
|
|
0c400f9355 | ||
|
|
147e99a0cc | ||
|
|
b38b2d2283 | ||
|
|
e065794838 | ||
|
|
903194abed | ||
|
|
2bf0ba9802 | ||
|
|
39ec44f169 | ||
|
|
e1cac75f85 | ||
|
|
5e9e95e00b | ||
|
|
58d9113fd6 | ||
|
|
2f4caf755e | ||
|
|
f38abe38d7 | ||
|
|
fe0722c4f1 | ||
|
|
c585903a4a | ||
|
|
48eb488918 | ||
|
|
68e1f9d4b0 | ||
|
|
a24f1d056b | ||
|
|
0b0365d559 | ||
|
|
3401438878 | ||
|
|
00ea513546 | ||
|
|
4bf3a73b50 | ||
|
|
72a38929e5 | ||
|
|
4005e4412d | ||
|
|
470efbeaf5 | ||
|
|
368dbe3bbf | ||
|
|
e5d4b4f0f0 | ||
|
|
a5138f4899 | ||
|
|
4cac80a968 | ||
|
|
f74d8439a1 | ||
|
|
fa182395f6 | ||
|
|
ed84d33b28 | ||
|
|
46b85c7857 | ||
|
|
626be61218 | ||
|
|
9a5348b0e0 | ||
|
|
5ebd7d72fc | ||
|
|
a3bd10f6c6 | ||
|
|
15040a362d | ||
|
|
19729a307f | ||
|
|
42710f9ba1 | ||
|
|
b2e94ed29d | ||
|
|
6908101735 | ||
|
|
d211641ce2 | ||
|
|
c3bca6ed60 | ||
|
|
9423f37e12 | ||
|
|
c93da6cbbc | ||
|
|
709ae67b3f | ||
|
|
efad850ef3 | ||
|
|
622eafdcc0 | ||
|
|
5cddf86c7c | ||
|
|
28931bd49a | ||
|
|
61360fa813 | ||
|
|
a41f6c7fcd | ||
|
|
22aa830f53 | ||
|
|
5aaf1501a2 | ||
|
|
6781c69d33 | ||
|
|
9a1f033fb4 | ||
|
|
259c74eb3a | ||
|
|
b107cf7d03 | ||
|
|
97b0985261 | ||
|
|
055b1c3bdc | ||
|
|
fa0856de8b | ||
|
|
db1d77269a | ||
|
|
44634cde63 | ||
|
|
eabf69ea00 | ||
|
|
7bc508004b | ||
|
|
831dfc8f6d | ||
|
|
7435b28092 | ||
|
|
34584cd8f8 | ||
|
|
677a5e829e | ||
|
|
d1f8105abf | ||
|
|
c54ccc56c7 | ||
|
|
e909fd352a | ||
|
|
916ed3d6aa | ||
|
|
5503d4b05b | ||
|
|
a5f200636a | ||
|
|
673bb99573 | ||
|
|
d64cd86f52 | ||
|
|
3ddd5bb65a | ||
|
|
876598f714 | ||
|
|
ed550bb633 | ||
|
|
6b4004b127 | ||
|
|
96feae60da | ||
|
|
fb1bf29136 | ||
|
|
738c1e0d0a | ||
|
|
19b64d4b0f | ||
|
|
67e57018bc | ||
|
|
fac8013dba | ||
|
|
3c6b2a7c03 | ||
|
|
699d42b26c | ||
|
|
1cdf32d807 | ||
|
|
a0f23339cc | ||
|
|
77478303fe | ||
|
|
3f354b78dc | ||
|
|
c7a2a26f50 | ||
|
|
1fb0d1460a | ||
|
|
acab260762 | ||
|
|
a7ec036d95 | ||
|
|
bb2236ba85 | ||
|
|
66f217c8e6 | ||
|
|
7cd0db219a | ||
|
|
d656e02441 | ||
|
|
67d3ad47d7 | ||
|
|
4ec2e16b98 | ||
|
|
645abe0c77 | ||
|
|
e623b262ab | ||
|
|
5472ff4ac3 | ||
|
|
f377c94b64 | ||
|
|
6b7ceb92fa | ||
|
|
9a15f4fc8d | ||
|
|
fc9fa2a7f8 | ||
|
|
b617b5aa20 | ||
|
|
15006c27cc | ||
|
|
bd3892982f | ||
|
|
53520af2fd | ||
|
|
5ff508de2c | ||
|
|
059c541875 | ||
|
|
d959ca1694 | ||
|
|
fe2d0ea43b | ||
|
|
6c644dd5d2 | ||
|
|
b304c1d079 | ||
|
|
8cf672d878 | ||
|
|
9aa7f87a27 | ||
|
|
692c848154 | ||
|
|
4378be45e4 | ||
|
|
dbb8c34486 | ||
|
|
0933701f55 | ||
|
|
5bd45b5a42 | ||
|
|
48ff8175eb | ||
|
|
8d4562d071 | ||
|
|
334c17f8ab | ||
|
|
bbc772abe7 | ||
|
|
1231ca17c9 | ||
|
|
e563ed0c75 | ||
|
|
7f2a52ff71 | ||
|
|
7fa4ed6139 | ||
|
|
eb22794f14 | ||
|
|
1db135262d | ||
|
|
6320f7290f | ||
|
|
de919568b4 | ||
|
|
8696ba2f5d | ||
|
|
c140596ab3 | ||
|
|
bba72e9b2f | ||
|
|
ad559c3491 | ||
|
|
078b8439d9 | ||
|
|
0b475aa13e | ||
|
|
734880f314 | ||
|
|
cff2629131 | ||
|
|
667213e52b | ||
|
|
f0c9b3852f | ||
|
|
5e82de1b71 | ||
|
|
29ca1a1f40 | ||
|
|
eb5946fa99 | ||
|
|
2fc39859ad | ||
|
|
58322c271b | ||
|
|
319414486a | ||
|
|
711076d02d | ||
|
|
227fadc541 | ||
|
|
4cd0db764f | ||
|
|
fc71001110 | ||
|
|
54d3e5675f | ||
|
|
b9fc3db613 | ||
|
|
817e719cc2 | ||
|
|
7ba61be796 | ||
|
|
4a48b13715 | ||
|
|
cd2bab7c5f | ||
|
|
f8050f4278 | ||
|
|
3bcf1cbdce | ||
|
|
0f68dc4505 | ||
|
|
b7470617e0 | ||
|
|
ac9b0409f5 | ||
|
|
d77d79e011 | ||
|
|
f5e5f7b588 | ||
|
|
a65b200eb7 | ||
|
|
8290731253 | ||
|
|
9429c05693 | ||
|
|
0fc187adc3 | ||
|
|
99956649e3 | ||
|
|
e91d886e76 | ||
|
|
514cfe2c29 | ||
|
|
a9936ae133 | ||
|
|
1e87600119 | ||
|
|
a9aeb8ac54 | ||
|
|
2c64b76392 | ||
|
|
655a241958 | ||
|
|
6bf63f66ec | ||
|
|
8b75993d3a | ||
|
|
e11cadca58 | ||
|
|
77021fff74 | ||
|
|
16e440f9a7 | ||
|
|
3cf765d985 | ||
|
|
42f9d27d79 | ||
|
|
ac2acc535d | ||
|
|
81c8972a66 | ||
|
|
0941b908dd | ||
|
|
f9f4e4b84c | ||
|
|
b8ea6fc708 | ||
|
|
0c7dcec5c2 | ||
|
|
92327729d6 | ||
|
|
5a5804ca87 | ||
|
|
984d744ac2 | ||
|
|
1e992bb263 | ||
|
|
21e7675a44 | ||
|
|
26586a42a4 | ||
|
|
823413a9c4 | ||
|
|
25240967ba | ||
|
|
4ce058fc47 | ||
|
|
b9a08e7920 | ||
|
|
68f23c7a70 | ||
|
|
43dbc1efbb | ||
|
|
472c0c2b27 | ||
|
|
d0e59b580e | ||
|
|
a8b69efa84 | ||
|
|
8b27c2b8ee | ||
|
|
e346e988ca | ||
|
|
a271fd2590 | ||
|
|
7be3eb36d9 | ||
|
|
62269b595a | ||
|
|
7d03e609a6 | ||
|
|
91652921a3 | ||
|
|
cde9d3a9ce | ||
|
|
c655d1db3a | ||
|
|
4fa74d29a9 | ||
|
|
752253704c | ||
|
|
8d35676e5a | ||
|
|
4cc6596d44 | ||
|
|
16d5caa719 | ||
|
|
430a06d056 | ||
|
|
0c62e1a9ae | ||
|
|
d45cd5af2b | ||
|
|
2713055447 | ||
|
|
3431c6c90e | ||
|
|
f0e7eb44f1 | ||
|
|
2b777caa83 | ||
|
|
8a9bf166c6 | ||
|
|
a08c7f37d3 | ||
|
|
3302795e50 | ||
|
|
e867a42181 | ||
|
|
aa3f50ab77 | ||
|
|
fc5946c139 | ||
|
|
2fd4db0891 | ||
|
|
3ee23d9ee8 | ||
|
|
dbaa44688e | ||
|
|
fe4f7b9c2f | ||
|
|
7adab6f5ec | ||
|
|
5b4e28fac6 | ||
|
|
12c91af5bc | ||
|
|
d1f24ca4a5 | ||
|
|
4cf481cca8 | ||
|
|
dd910d7c1a | ||
|
|
1770fe6590 | ||
|
|
a72a1ca517 | ||
|
|
9d2e5391cc | ||
|
|
4312719010 | ||
|
|
9540ffeec0 | ||
|
|
89c2bbed7c | ||
|
|
a7e70b1094 | ||
|
|
760c373eb2 | ||
|
|
89aab0af18 | ||
|
|
345ca405b0 | ||
|
|
7d06622881 | ||
|
|
fa5419dede | ||
|
|
e7a2ff1884 | ||
|
|
6d908f44a5 | ||
|
|
d61977d002 | ||
|
|
29c3ef8280 | ||
|
|
f9c797a402 | ||
|
|
c8940a39b3 | ||
|
|
38471995e7 | ||
|
|
00b3576134 | ||
|
|
94c45d1db3 | ||
|
|
00518069ac | ||
|
|
0665d13fd3 | ||
|
|
505814c07a | ||
|
|
c15d7fe86e | ||
|
|
23308f6d10 | ||
|
|
7c8dd86a35 | ||
|
|
c3111db6e2 | ||
|
|
09f65713ca | ||
|
|
c8410cb5ca | ||
|
|
d7e22de44c | ||
|
|
5918199845 | ||
|
|
8b0b938595 | ||
|
|
2c35299cbb | ||
|
|
07d1663f2c | ||
|
|
a33bcb47b3 | ||
|
|
67ec4fa477 | ||
|
|
c433943c46 | ||
|
|
865956e537 | ||
|
|
7b05a2a097 | ||
|
|
eb6c8d8938 | ||
|
|
28cbd18300 | ||
|
|
b4f831a931 | ||
|
|
a273147b6e | ||
|
|
2cb2e05b19 | ||
|
|
f2d31e3b77 | ||
|
|
a8d17b7590 | ||
|
|
c46b3d4b83 | ||
|
|
dd24cce509 | ||
|
|
b0d9c4f563 | ||
|
|
ccc48f909a | ||
|
|
60b7e22e93 | ||
|
|
f0df41d521 | ||
|
|
9ad7dad86d | ||
|
|
ece7165022 | ||
|
|
e4db7f8d0a | ||
|
|
9a61d2d531 | ||
|
|
265f7ce092 | ||
|
|
db8d368717 | ||
|
|
30c59bddf9 | ||
|
|
bc26c87a63 | ||
|
|
338ee746ec | ||
|
|
3bda9c54ae | ||
|
|
8b6f328665 | ||
|
|
1b674a1051 | ||
|
|
8a10e327ff | ||
|
|
2c80b2baa7 | ||
|
|
d6fb99916e | ||
|
|
9502b163e1 | ||
|
|
269020984b | ||
|
|
e058998689 | ||
|
|
349ad94ff3 | ||
|
|
f5a71c6b88 | ||
|
|
d5edca2022 | ||
|
|
283d69c0bd | ||
|
|
cc2a27315a | ||
|
|
a4628c2024 | ||
|
|
708ba3b229 | ||
|
|
14e17f584a | ||
|
|
871b8473fa | ||
|
|
a60db40fd2 | ||
|
|
86db6a5b06 | ||
|
|
e556616ad1 | ||
|
|
88c2be7e68 | ||
|
|
3274285729 | ||
|
|
14b47e81ce | ||
|
|
7dc2abb516 | ||
|
|
23bc180d98 | ||
|
|
aa7727d50a | ||
|
|
0212be2e58 | ||
|
|
cc26d5da14 | ||
|
|
de153aeb1d | ||
|
|
7f6038208d | ||
|
|
b3cebd87c8 | ||
|
|
7bd24308d3 | ||
|
|
cd9651afc1 | ||
|
|
3aa950c32e | ||
|
|
2729d7521d | ||
|
|
f4722d3b24 | ||
|
|
c30665fda7 | ||
|
|
0c15b65756 | ||
|
|
d25846f383 | ||
|
|
02380c3eab | ||
|
|
655aff7c92 | ||
|
|
1d8f1d66e4 | ||
|
|
3395fc1fde | ||
|
|
415b751bab | ||
|
|
67b95c4abf | ||
|
|
5c3c11cda3 | ||
|
|
af7dc363e1 | ||
|
|
803180d5de | ||
|
|
119904e44f | ||
|
|
2c44e4ec2c | ||
|
|
7358f44cc2 | ||
|
|
c03f1c25cf | ||
|
|
97959dbe75 | ||
|
|
7f3905185c | ||
|
|
316470eee4 | ||
|
|
fc56b1e8aa | ||
|
|
f4e5e0812b | ||
|
|
a384c96617 | ||
|
|
9c5ba2b0b3 | ||
|
|
8837016243 | ||
|
|
2ce297aff8 | ||
|
|
4785f0b31d | ||
|
|
e38dfbfa91 | ||
|
|
f916f29e47 | ||
|
|
3a9b65ebef | ||
|
|
34b0aef5ce | ||
|
|
1481bc80e3 | ||
|
|
6d82e3cc28 | ||
|
|
3a80e116e8 | ||
|
|
6e8589a69a | ||
|
|
e70caedddc | ||
|
|
a0bb8411ef | ||
|
|
5a718d681a | ||
|
|
ba45f7610d | ||
|
|
892dc1862a | ||
|
|
bb43419944 | ||
|
|
0707c9d732 | ||
|
|
edd41fd693 | ||
|
|
e2a25ae3c5 | ||
|
|
732a9b86c6 | ||
|
|
d4ad4a2f6e | ||
|
|
ee47c5eba9 | ||
|
|
70411ec086 | ||
|
|
b0c0a86fcf | ||
|
|
9dee411eb5 | ||
|
|
3d94a7cf2c | ||
|
|
5f24061dd4 | ||
|
|
f619bca2d6 | ||
|
|
e2d63e4c32 | ||
|
|
848d4d3767 | ||
|
|
830b3ba1e5 | ||
|
|
8c736b5bbd | ||
|
|
30b3570987 | ||
|
|
b9b3302b69 | ||
|
|
9c14aa08f8 | ||
|
|
89564bd10b | ||
|
|
c41adc4b9c | ||
|
|
0b08fe2bac | ||
|
|
0a34facb81 | ||
|
|
99f7eb38d3 | ||
|
|
bb129b7883 | ||
|
|
140698d676 | ||
|
|
7970819904 | ||
|
|
f6212f7b51 | ||
|
|
a7a6ca197c | ||
|
|
1deedc766c | ||
|
|
c18d565d3e | ||
|
|
b8015d1032 | ||
|
|
cf70147c0d | ||
|
|
4c08165b69 | ||
|
|
f7ee9ee967 | ||
|
|
f13d98fc7c | ||
|
|
ac47b42c66 | ||
|
|
394b5b5b94 | ||
|
|
8ae48f9baa | ||
|
|
1a1eb00689 | ||
|
|
daac7c589b | ||
|
|
88b9f8d68c | ||
|
|
c86e75c091 | ||
|
|
444225f0ec | ||
|
|
3d5b46bdfc | ||
|
|
15e354f76e | ||
|
|
b1037eaade | ||
|
|
a20d0d8f60 | ||
|
|
bf99f8095d | ||
|
|
7464fdb8e8 | ||
|
|
ea5761ee9c | ||
|
|
c29fb45e10 | ||
|
|
9c4aac03df | ||
|
|
f12b1bbf5d | ||
|
|
da498b0558 | ||
|
|
062b245e3f | ||
|
|
2b2c7bdf09 | ||
|
|
d0504546ec | ||
|
|
da68fa0980 | ||
|
|
8d96acfc98 | ||
|
|
96c59e0435 | ||
|
|
2ec69545ea | ||
|
|
baa612bc72 | ||
|
|
56085fe6a9 | ||
|
|
4c273fcc99 | ||
|
|
8fdda31e45 | ||
|
|
73e34ff9a9 | ||
|
|
d915c2b404 | ||
|
|
468e5e9b2e | ||
|
|
048b87328b | ||
|
|
2c54f49cbc | ||
|
|
fccfcd6b0e | ||
|
|
1170e4fb2c | ||
|
|
20c2af9cd4 | ||
|
|
1d991af821 | ||
|
|
a186b1266d | ||
|
|
dc841fe661 | ||
|
|
ac448988ca | ||
|
|
cc48cfaa5d | ||
|
|
5ed34d6ff9 | ||
|
|
46e6e48495 | ||
|
|
9638151f9d | ||
|
|
998617879c | ||
|
|
984947f333 | ||
|
|
2de2ea9f58 | ||
|
|
657de2cc7e | ||
|
|
27e5344188 | ||
|
|
d163da171f | ||
|
|
d3253d7d06 | ||
|
|
c7b1379a7f | ||
|
|
ed79adebc4 | ||
|
|
f5beda48dc | ||
|
|
d45d20e4db | ||
|
|
f41c6c037b | ||
|
|
313913b329 | ||
|
|
c9c45fe89f | ||
|
|
56ddb16186 | ||
|
|
4f1acc9349 | ||
|
|
ecff9dfdd8 | ||
|
|
73d2878e08 | ||
|
|
feaf39a812 | ||
|
|
1662b7c311 | ||
|
|
dc72e6cf36 | ||
|
|
4edbe77f67 | ||
|
|
fa228da29c | ||
|
|
9aea4ba51a | ||
|
|
b7039cc506 | ||
|
|
b8224693c4 | ||
|
|
1e44e3c1f6 | ||
|
|
a8e2386daa | ||
|
|
7e12332ea5 | ||
|
|
03d6550db3 | ||
|
|
b1311ceb30 | ||
|
|
5089cf2155 | ||
|
|
204de4934a | ||
|
|
8f3ed909c3 | ||
|
|
1728a95111 | ||
|
|
94ec76545c | ||
|
|
4174269091 | ||
|
|
bc2cb1737a | ||
|
|
cf6913891a | ||
|
|
205037fd6b | ||
|
|
7591656491 | ||
|
|
766c5bbe2b | ||
|
|
446264e496 | ||
|
|
1ff47f0780 | ||
|
|
cb02391f37 | ||
|
|
e01ff50833 | ||
|
|
8a97b39028 | ||
|
|
f6e16c1180 | ||
|
|
dd23d4c81b | ||
|
|
dfd115cee5 | ||
|
|
37a964c300 | ||
|
|
b53723acad | ||
|
|
98eb115746 | ||
|
|
c022b80e05 | ||
|
|
a88c62a307 | ||
|
|
98724dff32 | ||
|
|
9dc583ffcb | ||
|
|
971024ab99 | ||
|
|
b6da350c20 | ||
|
|
4f90f50eb2 | ||
|
|
84b9a2aefb | ||
|
|
e70416c78c | ||
|
|
4fb1202c30 | ||
|
|
2245731fc8 | ||
|
|
2b87de1000 | ||
|
|
063c4e9720 | ||
|
|
cc2ca58721 | ||
|
|
7b99275ceb | ||
|
|
478766c600 | ||
|
|
0ae080723c | ||
|
|
f3d6a64156 | ||
|
|
da8f7b29c1 | ||
|
|
8900744fc4 | ||
|
|
71578cb2ef | ||
|
|
8d091f6821 | ||
|
|
9c7c22ed20 | ||
|
|
f9a78e9b45 | ||
|
|
13afd3301f | ||
|
|
a27f3f737f | ||
|
|
bc46045cc7 | ||
|
|
47979871de | ||
|
|
c9675b3f7d | ||
|
|
79f73ccca1 | ||
|
|
74c4ca68e5 | ||
|
|
2a186ab8dd | ||
|
|
6b98323806 | ||
|
|
52ac389661 | ||
|
|
393c1d4bee | ||
|
|
9b8e0eb5c5 | ||
|
|
33f2a23bd8 | ||
|
|
d40538968f | ||
|
|
28ee5fbf2e | ||
|
|
982550b92c | ||
|
|
26f234fdbd | ||
|
|
d6fd613272 | ||
|
|
480e76d98e | ||
|
|
8b06468490 | ||
|
|
ea3d4ced5e | ||
|
|
bb7ddd11f1 | ||
|
|
05e7db2362 | ||
|
|
34e0a939e6 | ||
|
|
0da8ed2daa | ||
|
|
0caa0371dc | ||
|
|
7a266113ed | ||
|
|
e725780c6d | ||
|
|
0fb6b4eaf6 | ||
|
|
403220c69a | ||
|
|
6150106dee | ||
|
|
e073075834 | ||
|
|
fca9843fc2 | ||
|
|
169caaf66f | ||
|
|
dea3e326ba | ||
|
|
26ecd7fd1b | ||
|
|
f877f87b01 | ||
|
|
d33851367b | ||
|
|
4c5d753ade | ||
|
|
bc6f69ad54 | ||
|
|
ee2ea11458 | ||
|
|
e1b2956cdb | ||
|
|
8757800888 | ||
|
|
d99f258d61 | ||
|
|
b8bf4319ac | ||
|
|
7ae642e6fa | ||
|
|
70204b4464 | ||
|
|
e0895be7e9 | ||
|
|
815220a3c6 | ||
|
|
668574e4f0 | ||
|
|
adb9a6bc15 | ||
|
|
597d5aff02 | ||
|
|
896b21e78b | ||
|
|
816b84be02 | ||
|
|
290a9b7804 | ||
|
|
ac7b6c6a3d | ||
|
|
3ccb209bfd | ||
|
|
f2ce84c161 | ||
|
|
52a6856f6c | ||
|
|
92a12d7fea | ||
|
|
a2bb557570 | ||
|
|
a73c555574 | ||
|
|
25838ba9b0 | ||
|
|
e6b9e82b2f | ||
|
|
de8c3ba968 | ||
|
|
de56faf862 | ||
|
|
89233d2b87 | ||
|
|
75a00928b5 | ||
|
|
a3834eef46 | ||
|
|
b38d472d7c | ||
|
|
96bfe7ccb7 | ||
|
|
146f98d026 | ||
|
|
ec578ba231 | ||
|
|
29d94f71f3 | ||
|
|
a878dd3837 | ||
|
|
93d3eb662f | ||
|
|
d8371c41cf | ||
|
|
73c08c1ecd | ||
|
|
ea05f81024 | ||
|
|
e16014e448 | ||
|
|
15b1609d88 | ||
|
|
51751a7a05 | ||
|
|
0ae60b8b61 | ||
|
|
7af9fa36d7 | ||
|
|
3886641887 | ||
|
|
b651d3f622 | ||
|
|
d72825e279 | ||
|
|
6e73fbedb0 | ||
|
|
e9f99e5a3f | ||
|
|
9f44de50eb | ||
|
|
ec1faf02ed | ||
|
|
2397abaee5 | ||
|
|
b11bf8eb79 | ||
|
|
f3460ec840 | ||
|
|
fbe14b79cc | ||
|
|
ebd45878c3 | ||
|
|
91881fad6b | ||
|
|
c2cd4934e7 | ||
|
|
48485c27ec | ||
|
|
6e80d89d13 | ||
|
|
8a2a845a16 | ||
|
|
376dcf50ec | ||
|
|
751f3abd95 | ||
|
|
6e98adecdd | ||
|
|
64ae1ec367 | ||
|
|
b937b18e3d | ||
|
|
fc8ca7d82c | ||
|
|
7efeed54de | ||
|
|
52c0df24e3 | ||
|
|
9d0ebe3427 | ||
|
|
1a90c0d031 | ||
|
|
c5e35cc330 | ||
|
|
da32bb5f51 | ||
|
|
7b7440d44a | ||
|
|
e90c6a33bd | ||
|
|
8cf8f6abad | ||
|
|
097e74979f | ||
|
|
e5920c57aa | ||
|
|
c714b724da | ||
|
|
f0697d8f27 | ||
|
|
27309d6714 | ||
|
|
c0631468db | ||
|
|
d10647a592 | ||
|
|
269ac78a98 | ||
|
|
5c665c562a | ||
|
|
4d784b8fc7 | ||
|
|
dd027f09ac | ||
|
|
2a16353cf6 | ||
|
|
8c2e40e291 | ||
|
|
5f1ca4113d | ||
|
|
846f0350d8 | ||
|
|
95a235e239 | ||
|
|
b7bf2fad84 | ||
|
|
d0537f2ee4 | ||
|
|
495afae178 | ||
|
|
83f279410c | ||
|
|
34f51ae0b2 | ||
|
|
5b619c7832 | ||
|
|
376191b31f | ||
|
|
2c507c891c | ||
|
|
73f6c29559 | ||
|
|
6cac0347ae | ||
|
|
f5de1ea5c8 | ||
|
|
161e336d97 | ||
|
|
d2a4cebe54 | ||
|
|
88255d3d3d | ||
|
|
1cd2266da1 | ||
|
|
288c3ee9c2 | ||
|
|
cae34096c7 | ||
|
|
27c73cf9e9 | ||
|
|
84ea6afd01 | ||
|
|
e342b1f7bd | ||
|
|
9f32021d07 | ||
|
|
8ba66c9833 | ||
|
|
2012045798 | ||
|
|
7ec4d16403 | ||
|
|
8cc6853c34 | ||
|
|
3600f2f91b | ||
|
|
7ed05e7d2d | ||
|
|
8aac6a6b18 | ||
|
|
5fc07842eb | ||
|
|
aa2c56e117 | ||
|
|
97c48ed6d2 | ||
|
|
4e45e69247 | ||
|
|
0e881f2999 | ||
|
|
f4c6bdf204 | ||
|
|
099a5fbad9 | ||
|
|
e60c711fdc | ||
|
|
0a41fe2541 | ||
|
|
901a89ebcd | ||
|
|
2ee463fa33 | ||
|
|
398406082a | ||
|
|
277c1101fc | ||
|
|
ee0dd462b8 | ||
|
|
df0994c0d3 | ||
|
|
fc622631c0 | ||
|
|
46a6290ce9 | ||
|
|
09541c52e1 | ||
|
|
9548f341bf | ||
|
|
b4b473185f | ||
|
|
d46b68230c | ||
|
|
109658731b | ||
|
|
ec07549d5e | ||
|
|
90be3cddf7 | ||
|
|
0a71ca6739 | ||
|
|
2928d39d58 | ||
|
|
407fdab487 | ||
|
|
9e633bddef | ||
|
|
327d067305 | ||
|
|
acb9829159 | ||
|
|
b0535bff34 | ||
|
|
0da90f8092 | ||
|
|
efb8e7c0e4 | ||
|
|
80d6779210 | ||
|
|
32a45cf635 | ||
|
|
45c7bac2d0 | ||
|
|
59ae667cce | ||
|
|
50bf4017d6 | ||
|
|
5f721f01d3 | ||
|
|
f2afd98725 | ||
|
|
2882576479 | ||
|
|
2ff1dcc391 | ||
|
|
dd43594ad6 | ||
|
|
c17ae703c7 | ||
|
|
9da5010265 | ||
|
|
39cd7a29df | ||
|
|
86b37782fe | ||
|
|
1ee8a9f257 | ||
|
|
704223e5d0 | ||
|
|
7ee2418f60 | ||
|
|
14a2f98521 | ||
|
|
7dbc821731 | ||
|
|
0d2a88bafc | ||
|
|
072518ed96 | ||
|
|
863507ea28 | ||
|
|
0b7f73fa8b | ||
|
|
37727448f6 | ||
|
|
74df63a28a | ||
|
|
bb62a01c0d | ||
|
|
7e0e9db4d2 | ||
|
|
48e8e85617 | ||
|
|
8f19f14004 | ||
|
|
aac4ac0fae | ||
|
|
31d12517f0 | ||
|
|
8098229b55 | ||
|
|
8ea9cb1d34 | ||
|
|
6a401bcfbb | ||
|
|
20fd071c4e | ||
|
|
db654d5e59 | ||
|
|
abfff79095 | ||
|
|
8aafd893ed | ||
|
|
21118d5373 | ||
|
|
2c7262b033 | ||
|
|
dc1be35dbb | ||
|
|
ee3f4c21be | ||
|
|
bc002937ad | ||
|
|
2af95d2339 | ||
|
|
d69d5b498d | ||
|
|
cfe04a2aaf | ||
|
|
c6baa34812 | ||
|
|
52177cffcd | ||
|
|
344bcf1448 | ||
|
|
45292700d4 | ||
|
|
c742a1dbe9 | ||
|
|
1cb7d5126c | ||
|
|
e7da4992f3 | ||
|
|
c5e36eb323 | ||
|
|
4dbf4a214d | ||
|
|
ae77c609ff | ||
|
|
195911ce4e | ||
|
|
d2983b977c | ||
|
|
824a86c503 | ||
|
|
a0a8428483 | ||
|
|
87a472c2d7 | ||
|
|
a926c7eafd | ||
|
|
b630ccc8e6 | ||
|
|
0426b37f32 | ||
|
|
e1d9f863c6 | ||
|
|
24cc711a70 | ||
|
|
7c7b392789 | ||
|
|
fab0f4f337 | ||
|
|
aee26c3550 | ||
|
|
1e929e2c6c | ||
|
|
0585bc5aef | ||
|
|
ee4e0c646d | ||
|
|
ea6ff2defe | ||
|
|
ab77ee7f5a | ||
|
|
ce6ace4b8a | ||
|
|
3e4d160626 | ||
|
|
1e37fd8991 | ||
|
|
55e79c4dfd | ||
|
|
fbd8fd7d22 |
12
.coderabbit.yml
Normal file
12
.coderabbit.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
reviews:
|
||||
auto_review:
|
||||
ignore_title_keywords:
|
||||
- "sync translations"
|
||||
- "update POT file"
|
||||
- "style: "
|
||||
review_status: false
|
||||
poem: false
|
||||
collapse_walkthrough: true
|
||||
sequence_diagrams: false
|
||||
changed_files_summary: false
|
||||
high_level_summary: false
|
||||
3
.github/workflows/patch.yml
vendored
3
.github/workflows/patch.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
3
.github/workflows/patch_faux.yml
vendored
3
.github/workflows/patch_faux.yml
vendored
@@ -10,6 +10,9 @@ on:
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
3
.github/workflows/server-tests-mariadb.yml
vendored
3
.github/workflows/server-tests-mariadb.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
3
.github/workflows/server-tests-postgres.yml
vendored
3
.github/workflows/server-tests-postgres.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
types: [opened, labelled, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -32,8 +32,6 @@ repos:
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
|
||||
13
CODEOWNERS
13
CODEOWNERS
@@ -8,17 +8,16 @@ erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/subcontracting @mihir-kandoi
|
||||
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
|
||||
.github/ @ruthra-kumar
|
||||
pyproject.toml @akhilnarang
|
||||
pyproject.toml @ruthra-kumar
|
||||
|
||||
@@ -167,7 +167,7 @@ class Account(NestedSet):
|
||||
if par.root_type:
|
||||
self.root_type = par.root_type
|
||||
|
||||
if self.is_group:
|
||||
if cint(self.is_group):
|
||||
db_value = self.get_doc_before_save()
|
||||
if db_value:
|
||||
if self.report_type != db_value.report_type:
|
||||
@@ -210,7 +210,7 @@ class Account(NestedSet):
|
||||
if doc_before_save and not doc_before_save.parent_account:
|
||||
throw(_("Root cannot be edited."), RootNotEditable)
|
||||
|
||||
if not self.parent_account and not self.is_group:
|
||||
if not self.parent_account and not cint(self.is_group):
|
||||
throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
@@ -259,7 +259,7 @@ class Account(NestedSet):
|
||||
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction cannot be converted to ledger"))
|
||||
elif self.is_group:
|
||||
elif cint(self.is_group):
|
||||
if self.account_type and not self.flags.exclude_account_type_check:
|
||||
throw(_("Cannot covert to Group because Account Type is selected."))
|
||||
elif self.check_if_child_exists():
|
||||
@@ -302,7 +302,9 @@ class Account(NestedSet):
|
||||
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")
|
||||
gl_currency = frappe.db.get_value(
|
||||
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
|
||||
)
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
|
||||
@@ -270,12 +270,14 @@ frappe.treeview_settings["Account"] = {
|
||||
label: __("View Ledger"),
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
if (node.parent_label) {
|
||||
frappe.route_options["account"] = node.label;
|
||||
}
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
|
||||
@@ -18,6 +18,7 @@ def create_charts(
|
||||
accounts = []
|
||||
|
||||
def _import_accounts(children, parent, root_type, root_account=False):
|
||||
nonlocal custom_chart
|
||||
for account_name, child in children.items():
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
@@ -55,7 +56,8 @@ def create_charts(
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_currency": child.get("account_currency")
|
||||
or frappe.get_cached_value("Company", company, "default_currency"),
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
"tax_rate": child.get("tax_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
{
|
||||
"country_code": "au",
|
||||
"name": "Australia - Chart of Accounts with Account Numbers",
|
||||
"tree": {
|
||||
"Assets": {
|
||||
"Current Assets": {
|
||||
"Cash On Hand": {
|
||||
"Cash On Hand": {
|
||||
"account_number": "11010",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"account_number": "110",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash at Bank": {
|
||||
"Every Day Bank Account": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11520"
|
||||
},
|
||||
"Business Term Deposit": {
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
"account_number": "12010",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "12020"
|
||||
},
|
||||
"Sundry Debtors": {
|
||||
"account_number": "12030"
|
||||
},
|
||||
"Debtor Refund": {
|
||||
"account_number": "12040"
|
||||
},
|
||||
"account_number": "120",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock On Hand": {
|
||||
"account_number": "13010",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"WIP - Work In Progress - Manufacturing": {
|
||||
"account_number": "13020"
|
||||
},
|
||||
"account_number": "130",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "14010"
|
||||
},
|
||||
"Provisional Tax Paid": {
|
||||
"account_number": "14020"
|
||||
},
|
||||
"account_number": "140",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "11",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16010",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Plant & Equipment": {
|
||||
"account_number": "16020",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "160",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicle": {
|
||||
"Motor Vehicle": {
|
||||
"account_number": "16110",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Motor Vehicle": {
|
||||
"account_number": "16120",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "161",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Furniture & Equipment": {
|
||||
"account_number": "16210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Office Furniture & Equipment": {
|
||||
"account_number": "16220",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "162",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16310",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Computer Equipment": {
|
||||
"account_number": "16320",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "163",
|
||||
"is_group": 1
|
||||
},
|
||||
"Building": {
|
||||
"Buildings": {
|
||||
"account_number": "16410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Buildings": {
|
||||
"account_number": "16420",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"CWIP - Construction Work In Progress": {
|
||||
"account_number": "16430",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"Accumulated Depreciation - Others": {
|
||||
"account_number": "16440",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "164",
|
||||
"is_group": 1
|
||||
},
|
||||
"Related Party": {
|
||||
"Loan to Party 1": {
|
||||
"account_number": "17010"
|
||||
},
|
||||
"account_number": "170",
|
||||
"is_group": 1
|
||||
},
|
||||
"Investments & Unlisted Entities": {
|
||||
"Investment - Entity 1": {
|
||||
"account_number": "17510"
|
||||
},
|
||||
"account_number": "175",
|
||||
"is_group": 1
|
||||
},
|
||||
"Intagible Assets": {
|
||||
"Goodwill": {
|
||||
"account_number": "18010"
|
||||
},
|
||||
"Opening Balance Temporary ": {
|
||||
"account_number": "18090",
|
||||
"account_type": "Temporary"
|
||||
},
|
||||
"account_number": "180",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "16",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "1",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Liabilities": {
|
||||
"Current Liabilities": {
|
||||
"Trade Payables - Current": {
|
||||
"Trade Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21050",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21060"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21070",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"account_number": "210",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Payables - Current": {
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21510"
|
||||
},
|
||||
"Payroll - Wages Clearing": {
|
||||
"account_number": "21550"
|
||||
},
|
||||
"Payroll - Superannuation Deductions": {
|
||||
"account_number": "21555"
|
||||
},
|
||||
"Payroll - Misc Deductions": {
|
||||
"account_number": "21560"
|
||||
},
|
||||
"Payroll - Withholding Tax Payable": {
|
||||
"account_number": "21565"
|
||||
},
|
||||
"account_number": "215",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST": {
|
||||
"GST Payments to ATO": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Provision for PAYG Tax": {
|
||||
"account_number": "22040"
|
||||
},
|
||||
"account_number": "220",
|
||||
"account_type": "Tax",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Current": {
|
||||
"Credit Card - VISA": {
|
||||
"account_number": "22510"
|
||||
},
|
||||
"account_number": "225",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Overdraft": {
|
||||
"Bank Overdraft Cash at Bank": {
|
||||
"account_number": "23010"
|
||||
},
|
||||
"account_number": "230",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Finance": {
|
||||
"Trade Finance": {
|
||||
"account_number": "23510"
|
||||
},
|
||||
"account_number": "235",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Finance Lease - Current": {
|
||||
"account_number": "24010"
|
||||
},
|
||||
"account_number": "240",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "24510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "24520"
|
||||
},
|
||||
"account_number": "245",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Liabilities": {
|
||||
"Trade & Other Payables - Non Current": {
|
||||
"Loan Account - Party 1": {
|
||||
"account_number": "25010"
|
||||
},
|
||||
"account_number": "250",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Non Current": {
|
||||
"Non Current Liability - Director Loan": {
|
||||
"account_number": "25510"
|
||||
},
|
||||
"account_number": "255",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Loans - Non Current": {
|
||||
"Bank Loan 1 - Non Current": {
|
||||
"account_number": "26010"
|
||||
},
|
||||
"account_number": "260",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities - Non Current": {
|
||||
"Finance Lease - Non Current": {
|
||||
"account_number": "27010"
|
||||
},
|
||||
"account_number": "270",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions - Non Current": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "27510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "27520"
|
||||
},
|
||||
"account_number": "275",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "2",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Equity": {
|
||||
"Owner's/Shareholder's Equity": {
|
||||
"Owner's/Shareholders Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Owner's/Shareholders Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "310",
|
||||
"is_group": 1
|
||||
},
|
||||
"Earnings": {
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "350",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "31",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "3",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Revenue": {
|
||||
"Revenue": {
|
||||
"Sales Revenue": {
|
||||
"Sales Income": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Freight Income": {
|
||||
"account_number": "41020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"account_number": "41030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Service Income": {
|
||||
"account_number": "41040",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "410",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Revenue": {
|
||||
"Commission Received": {
|
||||
"account_number": "42010"
|
||||
},
|
||||
"Discounts Received": {
|
||||
"account_number": "42020"
|
||||
},
|
||||
"Interest received": {
|
||||
"account_number": "42030"
|
||||
},
|
||||
"Profit/Loss on Sales of Assets": {
|
||||
"account_number": "42040"
|
||||
},
|
||||
"Rent Received": {
|
||||
"account_number": "42050"
|
||||
},
|
||||
"Sundry Income": {
|
||||
"account_number": "42060"
|
||||
},
|
||||
"account_number": "420",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "41",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "4",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods Sold": {
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Expenses (sales related)": {
|
||||
"account_number": "51020"
|
||||
},
|
||||
"Discounts Given": {
|
||||
"account_number": "51030"
|
||||
},
|
||||
"Subcontracting Charges": {
|
||||
"account_number": "51040"
|
||||
},
|
||||
"account_number": "510",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other COGS": {
|
||||
"Purchases - Miscellaneous": {
|
||||
"account_number": "52010"
|
||||
},
|
||||
"Duty & Customs Fees": {
|
||||
"account_number": "52020",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "52030",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "52040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Wirte Off": {
|
||||
"account_number": "52050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Valuation Expenses": {
|
||||
"account_number": "52060",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Asset Valuation Expenses": {
|
||||
"account_number": "52070",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "520",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "51",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "5",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Expenses": {
|
||||
"Fixed Expenses": {
|
||||
"Payroll & Related Expenses": {
|
||||
"Salaries & Wages": {
|
||||
"account_number": "61010"
|
||||
},
|
||||
"Superannuation": {
|
||||
"account_number": "61015"
|
||||
},
|
||||
"Staff Amenities - GST Paid": {
|
||||
"account_number": "61020"
|
||||
},
|
||||
"Staff Amenities - GST Free": {
|
||||
"account_number": "61025"
|
||||
},
|
||||
"Staff Recruitment": {
|
||||
"account_number": "61030"
|
||||
},
|
||||
"Staff Training": {
|
||||
"account_number": "61035"
|
||||
},
|
||||
"Fringe Benefits Tax": {
|
||||
"account_number": "61040"
|
||||
},
|
||||
"Payroll Tax": {
|
||||
"account_number": "61045"
|
||||
},
|
||||
"Workers Compensation": {
|
||||
"account_number": "61050"
|
||||
},
|
||||
"Long Service Leave": {
|
||||
"account_number": "61060"
|
||||
},
|
||||
"Mileage Reimbursement": {
|
||||
"account_number": "61070"
|
||||
},
|
||||
"Overtime": {
|
||||
"account_number": "61080"
|
||||
},
|
||||
"Worksafe Insurance": {
|
||||
"account_number": "61090"
|
||||
},
|
||||
"account_number": "610",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation Expenses": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicle": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Building": {
|
||||
"account_number": "62050",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Others": {
|
||||
"account_number": "62510",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "620",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "61",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses - Salaries & Wages": {
|
||||
"account_number": "63010"
|
||||
},
|
||||
"Accrued Expenses - Interest": {
|
||||
"account_number": "63020"
|
||||
},
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"General and Administrative Expenses": {
|
||||
"Low Value Assets less than $300": {
|
||||
"account_number": "64010"
|
||||
},
|
||||
"Office Supplies": {
|
||||
"account_number": "64020"
|
||||
},
|
||||
"Postage & Courier": {
|
||||
"account_number": "64025"
|
||||
},
|
||||
"Printing & Stationery": {
|
||||
"account_number": "64030"
|
||||
},
|
||||
"Registration Fees / Filing Fees": {
|
||||
"account_number": "64040"
|
||||
},
|
||||
"Travel & Accommodation - Local": {
|
||||
"account_number": "64050"
|
||||
},
|
||||
"Travel & Accommodation - Overseas": {
|
||||
"account_number": "64060"
|
||||
},
|
||||
"Relocation Costs": {
|
||||
"account_number": "64070"
|
||||
},
|
||||
"Hire Charges": {
|
||||
"account_number": "64080"
|
||||
},
|
||||
"Repairs & Maintenance": {
|
||||
"account_number": "64210"
|
||||
},
|
||||
"Cleaning Expenses": {
|
||||
"account_number": "64215"
|
||||
},
|
||||
"Uniforms": {
|
||||
"account_number": "64220"
|
||||
},
|
||||
"Security": {
|
||||
"account_number": "64225"
|
||||
},
|
||||
"Subscriptions & Licences": {
|
||||
"account_number": "64510"
|
||||
},
|
||||
"Software Expenses": {
|
||||
"account_number": "64515"
|
||||
},
|
||||
"Marketing Expenses": {
|
||||
"account_number": "64520"
|
||||
},
|
||||
"Advertising Expenses": {
|
||||
"account_number": "64525"
|
||||
},
|
||||
"Website Hosting & Domain Expenses": {
|
||||
"account_number": "64530"
|
||||
},
|
||||
"Computer Repairs / Supplies": {
|
||||
"account_number": "64540"
|
||||
},
|
||||
"Conferences": {
|
||||
"account_number": "64550"
|
||||
},
|
||||
"Consultancy /Contract Services": {
|
||||
"account_number": "64560"
|
||||
},
|
||||
"Training Services": {
|
||||
"account_number": "64570"
|
||||
},
|
||||
"Workshop Supplies": {
|
||||
"account_number": "64580"
|
||||
},
|
||||
"Consumables": {
|
||||
"account_number": "64585"
|
||||
},
|
||||
"Entertainment Expenses - Deductible": {
|
||||
"account_number": "64810"
|
||||
},
|
||||
"Entertainment Expenses - Non Deductible": {
|
||||
"account_number": "64820"
|
||||
},
|
||||
"Amortisation Of Goodwill": {
|
||||
"account_number": "64910"
|
||||
},
|
||||
"General / Miscellaneous Expenses": {
|
||||
"account_number": "64915",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Donations": {
|
||||
"account_number": "64920"
|
||||
},
|
||||
"Client Gifts": {
|
||||
"account_number": "64930"
|
||||
},
|
||||
"Employee Gifts": {
|
||||
"account_number": "64935"
|
||||
},
|
||||
"account_number": "640",
|
||||
"is_group": 1
|
||||
},
|
||||
"Occupancy Expenses": {
|
||||
"Rental Expenses": {
|
||||
"account_number": "65010"
|
||||
},
|
||||
"Property Insurance": {
|
||||
"account_number": "65020"
|
||||
},
|
||||
"Electricity Expenses": {
|
||||
"account_number": "65030"
|
||||
},
|
||||
"Water Rates": {
|
||||
"account_number": "65040"
|
||||
},
|
||||
"Gas Expenses": {
|
||||
"account_number": "65050"
|
||||
},
|
||||
"Property Taxes": {
|
||||
"account_number": "65060"
|
||||
},
|
||||
"Rates": {
|
||||
"account_number": "65070"
|
||||
},
|
||||
"account_number": "650",
|
||||
"is_group": 1
|
||||
},
|
||||
"Communication & Vehicle Expenses": {
|
||||
"Internet Expenses": {
|
||||
"account_number": "66010"
|
||||
},
|
||||
"Mobile Telephone": {
|
||||
"account_number": "66020"
|
||||
},
|
||||
"Telephone Expenses": {
|
||||
"account_number": "66030"
|
||||
},
|
||||
"Motor Vehicle - Fuel Expenses": {
|
||||
"account_number": "66040"
|
||||
},
|
||||
"Motor Vehicle - Parking & Tolls": {
|
||||
"account_number": "66050"
|
||||
},
|
||||
"Motor Vehicle - Registration & Insurance": {
|
||||
"account_number": "66060"
|
||||
},
|
||||
"Motor Vehicle - Service & Repairs": {
|
||||
"account_number": "66070"
|
||||
},
|
||||
"Taxi": {
|
||||
"account_number": "66080"
|
||||
},
|
||||
"account_number": "660",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "64",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Operating Expenses": {
|
||||
"Finance Costs": {
|
||||
"Interest - Bank Loans": {
|
||||
"account_number": "67010"
|
||||
},
|
||||
"Interest - Finance Leases": {
|
||||
"account_number": "67020"
|
||||
},
|
||||
"Interest - Other Loans": {
|
||||
"account_number": "67025"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "67030"
|
||||
},
|
||||
"Bank Charges": {
|
||||
"account_number": "67050"
|
||||
},
|
||||
"Rounding off": {
|
||||
"account_number": "67055",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Audit Fees": {
|
||||
"account_number": "67060"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "67070"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "67080"
|
||||
},
|
||||
"Management Fees": {
|
||||
"account_number": "67090"
|
||||
},
|
||||
"account_number": "670",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Costs": {
|
||||
"Doubtful Debts": {
|
||||
"account_number": "67510"
|
||||
},
|
||||
"Fines": {
|
||||
"account_number": "67520"
|
||||
},
|
||||
"Debt Collection": {
|
||||
"account_number": "67530"
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "67540"
|
||||
},
|
||||
"account_number": "675",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "67",
|
||||
"is_group": 1
|
||||
},
|
||||
"Variable Expenses": {
|
||||
"Variable Expenses": {
|
||||
"Bonus & Commissions Paid": {
|
||||
"account_number": "68010"
|
||||
},
|
||||
"Bonus & Commissions To be Paid": {
|
||||
"account_number": "68020"
|
||||
},
|
||||
"Warranty Claims": {
|
||||
"account_number": "68030"
|
||||
},
|
||||
"account_number": "680",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "68",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "6",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Other Income": {
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "71010"
|
||||
},
|
||||
"account_number": "710",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Income": {
|
||||
"Gain on Asset Disposal": {
|
||||
"account_number": "73010"
|
||||
},
|
||||
"account_number": "730",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "71",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "7",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Other Expenses": {
|
||||
"Other Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"account_number": "81010"
|
||||
},
|
||||
"account_number": "810",
|
||||
"is_group": 1
|
||||
},
|
||||
"Foreign Exchange Gain/Loss": {
|
||||
"Exchange Loss/Gain - Realized": {
|
||||
"account_number": "82010"
|
||||
},
|
||||
"account_number": "820",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Expenses": {
|
||||
"Loss on Asset Disposal": {
|
||||
"account_number": "83010"
|
||||
},
|
||||
"account_number": "830",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "81",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "8",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ def get():
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {},
|
||||
_("Employee Advances"): {"account_type": "Payable"},
|
||||
},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
|
||||
@@ -20,7 +20,7 @@ def get():
|
||||
"account_number": "1100",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_number": "1610"},
|
||||
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
|
||||
@@ -111,17 +111,15 @@ class AccountingDimension(Document):
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
count = 0
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
|
||||
|
||||
for doctype in doclist:
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
"label": doc.label,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounting_dimension",
|
||||
"fieldname",
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
@@ -90,11 +91,17 @@
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.199186",
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@@ -139,8 +146,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
|
||||
ApplicableOnAccount,
|
||||
)
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
|
||||
|
||||
accounting_dimension: DF.Literal
|
||||
accounting_dimension: DF.Literal[None]
|
||||
accounts: DF.Table[ApplicableOnAccount]
|
||||
allow_or_restrict: DF.Literal["Allow", "Restrict"]
|
||||
apply_restriction_on_values: DF.Check
|
||||
company: DF.Link
|
||||
dimensions: DF.Table[AllowedDimension]
|
||||
disabled: DF.Check
|
||||
fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -37,6 +36,10 @@ class AccountingDimensionFilter(Document):
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.fieldname = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
|
||||
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
|
||||
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
def validate_applicable_accounts(self):
|
||||
@@ -71,7 +74,7 @@ def get_dimension_filter_map():
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
p.allow_or_restrict, p.fieldname, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
@@ -86,8 +89,6 @@ def get_dimension_filter_map():
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
|
||||
@@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
@@ -90,6 +91,8 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
@@ -556,7 +559,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -631,6 +634,23 @@
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -639,7 +659,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -49,6 +49,7 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
@@ -59,7 +60,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
@@ -149,8 +150,16 @@ class AccountsSettings(Document):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event"
|
||||
"event",
|
||||
"delinked"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -68,12 +69,20 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
@@ -107,7 +116,8 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.utils import get_advance_payment_doctypes, update_voucher_outstanding
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -19,9 +21,16 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def on_update(self):
|
||||
if (
|
||||
self.against_voucher_type in get_advance_payment_doctypes()
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
@@ -109,6 +109,7 @@ class BankAccount(Document):
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"company": self.company,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
@@ -117,15 +118,6 @@ class BankAccount(Document):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
|
||||
@@ -89,46 +89,64 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
invalid_document = []
|
||||
invalid_cheque_date = []
|
||||
entries_to_update = []
|
||||
|
||||
def validate_entry(d):
|
||||
is_valid = True
|
||||
if not d.payment_document:
|
||||
invalid_document.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
invalid_cheque_date.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
if d.clearance_date or self.include_reconciled_entries:
|
||||
if validate_entry(d) and (d.clearance_date or self.include_reconciled_entries):
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
entries_to_update.append(d)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
if invalid_document or invalid_cheque_date:
|
||||
msg = _("<p>Please correct the following row(s):</p><ul>")
|
||||
if invalid_document:
|
||||
msg += _("<li>Payment document required for row(s): {0}</li>").format(
|
||||
", ".join(invalid_document)
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
if invalid_cheque_date:
|
||||
msg += _("<li>Clearance date must be after cheque date for row(s): {0}</li>").format(
|
||||
", ".join(invalid_cheque_date)
|
||||
)
|
||||
|
||||
if clearance_date_updated:
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
else:
|
||||
msg += "</ul>"
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
msgprint(_("Clearance Date not mentioned"))
|
||||
return
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
|
||||
def get_payment_entries_for_bank_clearance(
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -377,16 +377,17 @@ def auto_reconcile_vouchers(
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transactions,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
for bank_transaction_batch in create_batch(bank_transactions, 1000):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transaction_batch,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
|
||||
@@ -252,7 +252,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
open_url_post(method, {
|
||||
doctype: "Bank Transaction",
|
||||
export_records: "5_records",
|
||||
export_records: "blank_template",
|
||||
export_fields: {
|
||||
"Bank Transaction": [
|
||||
"date",
|
||||
|
||||
@@ -302,7 +302,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
@@ -316,7 +316,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label="Actual Expenses",
|
||||
label=_("Actual Expenses"),
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
@@ -334,7 +334,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label="Material Requests",
|
||||
label=_("Material Requests"),
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
@@ -357,7 +357,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label="Unbilled Orders",
|
||||
label=_("Unbilled Orders"),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
|
||||
@@ -113,6 +113,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -126,7 +130,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -462,9 +462,8 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
@@ -190,6 +191,31 @@ class TestCostCenterAllocation(IntegrationTestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -163,43 +164,66 @@ class Dunning(AccountsController):
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||
if (
|
||||
doc.doctype != "Sales Invoice"
|
||||
or doc.is_return
|
||||
or previous_outstanding_amount == doc.outstanding_amount
|
||||
):
|
||||
return
|
||||
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
to_resolve = doc.outstanding_amount < previous_outstanding_amount
|
||||
state = "Unresolved" if to_resolve else "Resolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
|
||||
if not dunnings:
|
||||
return
|
||||
|
||||
for dunning in dunnings:
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
|
||||
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
|
||||
invoices = set()
|
||||
payment_schedule_ids = set()
|
||||
|
||||
new_status = "Resolved" if resolve else "Unresolved"
|
||||
for dunning in dunnings:
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoices.add(overdue_payment.sales_invoice)
|
||||
if overdue_payment.payment_schedule:
|
||||
payment_schedule_ids.add(overdue_payment.payment_schedule)
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
invoice_outstanding_amounts = dict(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"name": ["in", list(invoices)]},
|
||||
fields=["name", "outstanding_amount"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
ps_outstanding_amounts = (
|
||||
dict(
|
||||
frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"name": ["in", list(payment_schedule_ids)]},
|
||||
fields=["name", "outstanding"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
if payment_schedule_ids
|
||||
else {}
|
||||
)
|
||||
|
||||
for dunning in dunnings:
|
||||
has_outstanding = False
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
|
||||
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
|
||||
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
|
||||
if has_outstanding:
|
||||
break
|
||||
|
||||
new_status = "Resolved" if not has_outstanding else "Unresolved"
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
|
||||
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
@@ -139,6 +139,64 @@ class TestDunning(IntegrationTestCase):
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_resolution_from_credit_note(self):
|
||||
"""
|
||||
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 0
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
credit_note.cancel()
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_not_affected_by_standalone_credit_note(self):
|
||||
"""
|
||||
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 1
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
|
||||
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.accounts.party import (
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
@@ -224,26 +224,23 @@ class GLEntry(Document):
|
||||
def validate_account_details(self, adv_adj):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if ret.is_group == 1:
|
||||
if account.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.docstatus == 2:
|
||||
if account.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.company != self.company:
|
||||
if account.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
@@ -385,7 +382,7 @@ def update_outstanding_amt(
|
||||
)
|
||||
)
|
||||
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
if against_voucher_type in OUTSTANDING_DOCTYPES:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
# Didn't use db_set for optimization purpose
|
||||
@@ -462,4 +459,9 @@ def rename_temporarily_named_docs(doctype):
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
|
||||
for hook_type in ("on_gle_rename", "on_sle_rename"):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -196,6 +196,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
"paid_loan",
|
||||
"inter_company_journal_entry_reference",
|
||||
"column_break98",
|
||||
"bill_no",
|
||||
@@ -310,13 +309,6 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_loan",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid Loan",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
|
||||
"fieldname": "inter_company_journal_entry_reference",
|
||||
@@ -599,7 +591,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-17 15:18:13.322681",
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -71,7 +72,6 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
paid_loan: DF.Data | None
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -151,8 +151,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
self.title = self.get_title()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
@@ -195,8 +195,6 @@ class JournalEntry(AccountsController):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -298,8 +296,6 @@ class JournalEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -309,20 +305,6 @@ class JournalEntry(AccountsController):
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
for voucher_no in list(set(order_list)):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.db.get_value(
|
||||
@@ -672,6 +654,8 @@ class JournalEntry(AccountsController):
|
||||
elif (
|
||||
d.party_type
|
||||
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
|
||||
and d.party_type
|
||||
!= "Employee" # making an excpetion for employee since they can be both payable and receivable
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
|
||||
@@ -1197,49 +1181,65 @@ class JournalEntry(AccountsController):
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
},
|
||||
row,
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
@@ -1796,6 +1796,14 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
|
||||
if existing_reverse:
|
||||
frappe.throw(
|
||||
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
|
||||
get_link_to_form("Journal Entry", existing_reverse)
|
||||
)
|
||||
)
|
||||
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def post_process(source, target):
|
||||
|
||||
@@ -579,6 +579,18 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pay_to_recd_from(self):
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.pay_to_recd_from = "_Test Receiver"
|
||||
jv.save()
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
|
||||
|
||||
jv.pay_to_recd_from = "_Test Receiver 2"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"reference_name",
|
||||
"reference_due_date",
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -262,20 +264,37 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:58.647732",
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ class JournalEntryAccount(Document):
|
||||
account: DF.Link
|
||||
account_currency: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
against_account: DF.Text | None
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
@@ -31,7 +32,6 @@ class JournalEntryAccount(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_balance: DF.Currency
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reference_detail_no: DF.Data | None
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -55,22 +56,30 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
else:
|
||||
|
||||
@@ -273,6 +273,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
contact_person: function (frm) {
|
||||
|
||||
@@ -46,7 +46,9 @@ from erpnext.accounts.party import (
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_outstanding_invoices,
|
||||
get_reconciliation_effect_date,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -197,12 +199,10 @@ class PaymentEntry(AccountsController):
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.update_payment_requests()
|
||||
self.update_payment_schedule()
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
@@ -302,13 +302,11 @@ class PaymentEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def update_payment_requests(self, cancel=False):
|
||||
@@ -639,7 +637,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1099,10 +1097,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
if d.reference_doctype in get_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
|
||||
|
||||
@@ -1384,10 +1379,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1443,23 +1435,27 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if self.transaction_currency == self.party_account_currency
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# advance reference
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_doctype,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
elif self.book_advance_payments_in_separate_party_account:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
@@ -1564,29 +1560,14 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
if invoice.reconcile_effect_on:
|
||||
posting_date = invoice.reconcile_effect_on
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
posting_date = get_reconciliation_effect_date(
|
||||
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(
|
||||
invoice.reference_doctype, invoice.reference_name, date_field
|
||||
)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1604,6 +1585,8 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": invoice.reference_doctype,
|
||||
"against_voucher": invoice.reference_name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
"posting_date": posting_date,
|
||||
}
|
||||
)
|
||||
@@ -1628,6 +1611,8 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
}
|
||||
)
|
||||
gle = self.get_gl_dict(
|
||||
@@ -1776,19 +1761,6 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_lazy_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
self.reference_date = nowdate()
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.assertEqual(pe.paid_to_account_type, "Cash")
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
@@ -84,7 +84,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"exchange_gain_loss",
|
||||
"account",
|
||||
"payment_request",
|
||||
"payment_request_outstanding"
|
||||
"payment_request_outstanding",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -151,20 +153,37 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Reconcile Effect On",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-13 15:56:18.895082",
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class PaymentEntryReference(Document):
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -26,7 +28,6 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_request: DF.Link | None
|
||||
payment_request_outstanding: DF.Float
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
|
||||
@@ -8,4 +8,14 @@ frappe.ui.form.on("Payment Gateway Account", {
|
||||
frm.set_df_property("payment_gateway", "read_only", 1);
|
||||
}
|
||||
},
|
||||
|
||||
setup(frm) {
|
||||
frm.set_query("payment_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"field_order": [
|
||||
"payment_gateway",
|
||||
"payment_channel",
|
||||
"company",
|
||||
"is_default",
|
||||
"column_break_4",
|
||||
"payment_account",
|
||||
@@ -71,11 +72,21 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone\nOther"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-29 18:53:09.836254",
|
||||
"modified": "2025-07-14 16:49:55.210352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
@@ -94,6 +105,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentGatewayAccount(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link
|
||||
currency: DF.ReadOnly | None
|
||||
is_default: DF.Check
|
||||
message: DF.SmallText | None
|
||||
@@ -24,7 +25,8 @@ class PaymentGatewayAccount(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
self.name = self.payment_gateway + " - " + self.currency
|
||||
abbr = frappe.db.get_value("Company", self.company, "abbr")
|
||||
self.name = self.payment_gateway + " - " + self.currency + " - " + abbr
|
||||
|
||||
def validate(self):
|
||||
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
|
||||
@@ -34,13 +36,15 @@ class PaymentGatewayAccount(Document):
|
||||
|
||||
def update_default_payment_gateway(self):
|
||||
if self.is_default:
|
||||
frappe.db.sql(
|
||||
"""update `tabPayment Gateway Account` set is_default = 0
|
||||
where is_default = 1 """
|
||||
frappe.db.set_value(
|
||||
"Payment Gateway Account",
|
||||
{"is_default": 1, "name": ["!=", self.name], "company": self.company},
|
||||
"is_default",
|
||||
0,
|
||||
)
|
||||
|
||||
def set_as_default_if_not_set(self):
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
|
||||
if not frappe.db.exists(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company}
|
||||
):
|
||||
self.is_default = 1
|
||||
|
||||
@@ -197,4 +197,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.gl_entry.gl_entry import (
|
||||
validate_balance_type,
|
||||
validate_frozen_account,
|
||||
)
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, update_voucher_outstanding
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
@@ -51,38 +51,36 @@ class PaymentLedgerEntry(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate_account(self):
|
||||
valid_account = frappe.db.get_list(
|
||||
"Account",
|
||||
"name",
|
||||
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
|
||||
ignore_permissions=True,
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["account_type", "company"], as_dict=True
|
||||
)
|
||||
if not valid_account:
|
||||
|
||||
if account.company != self.company:
|
||||
frappe.throw(_("{0} account is not of company {1}").format(self.account, self.company))
|
||||
|
||||
if account.account_type != self.account_type:
|
||||
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
|
||||
|
||||
def validate_account_details(self):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if ret.is_group == 1:
|
||||
if account.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.docstatus == 2:
|
||||
if account.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.company != self.company:
|
||||
if account.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
@@ -170,7 +168,7 @@ class PaymentLedgerEntry(Document):
|
||||
|
||||
# update outstanding amount
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
self.against_voucher_type in OUTSTANDING_DOCTYPES
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
|
||||
@@ -576,6 +576,7 @@ class PaymentReconciliation(Document):
|
||||
"difference_amount": flt(row.get("difference_amount")),
|
||||
"difference_account": row.get("difference_account"),
|
||||
"difference_posting_date": row.get("gain_loss_posting_date"),
|
||||
"debit_or_credit_note_posting_date": row.get("debit_or_credit_note_posting_date"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
@@ -589,7 +590,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
@@ -765,7 +766,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": voucher_type,
|
||||
"posting_date": today(),
|
||||
"posting_date": inv.get("debit_or_credit_note_posting_date") or today(),
|
||||
"company": company,
|
||||
"multi_currency": 1 if inv.currency != company_currency else 0,
|
||||
"accounts": [
|
||||
@@ -826,7 +827,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.difference_posting_date,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
|
||||
@@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
def test_advance_payment_reconciliation_date_for_older_date(self):
|
||||
old_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"reconciliation_takes_effect_on",
|
||||
"default_advance_paid_account",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||
po = self.create_purchase_order(qty=10, rate=100)
|
||||
|
||||
pay = get_payment_entry(po.doctype, po.name)
|
||||
pay.paid_amount = 1000
|
||||
pay.save().submit()
|
||||
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = 100
|
||||
pr.reconcile()
|
||||
|
||||
pay.reload()
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
|
||||
# test setting of date if not available
|
||||
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
pay.reload()
|
||||
pi1.reload()
|
||||
po.reload()
|
||||
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
pi1.cancel()
|
||||
po.cancel()
|
||||
|
||||
frappe.db.set_value("Company", self.company, old_settings)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
@@ -2147,6 +2208,138 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
|
||||
|
||||
def test_partial_advance_payment_with_closed_fiscal_year(self):
|
||||
"""
|
||||
Test Advance Payment partial reconciliation before period closing and partial after period closing
|
||||
"""
|
||||
default_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"default_advance_paid_account",
|
||||
"reconciliation_takes_effect_on",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
first_fy_start_date = frappe.db.get_value(
|
||||
"Fiscal Year", {"disabled": 0}, [{"MIN": "year_start_date"}]
|
||||
)
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
|
||||
create_fiscal_year(
|
||||
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
# Create advance payment of 1000 (previous FY)
|
||||
pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
# Create purchase invoice of 600 (previous FY)
|
||||
pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True)
|
||||
pi1.posting_date = prev_fy_start_date
|
||||
pi1.set_posting_time = 1
|
||||
pi1.supplier = self.supplier
|
||||
pi1.credit_to = self.creditors
|
||||
pi1.save().submit()
|
||||
|
||||
# Reconcile advance payment
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Verify partial reconciliation
|
||||
pe.reload()
|
||||
pi1.reload()
|
||||
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].allocated_amount, 600)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 400)
|
||||
|
||||
self.assertEqual(pi1.outstanding_amount, 0)
|
||||
self.assertEqual(pi1.status, "Paid")
|
||||
|
||||
# Close accounting period for March (previous FY)
|
||||
pcv = make_period_closing_voucher(
|
||||
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||
)
|
||||
pcv.reload()
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
|
||||
# Change reconciliation setting to "Reconciliation Date"
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
"reconciliation_takes_effect_on",
|
||||
"Reconciliation Date",
|
||||
)
|
||||
|
||||
# Create new purchase invoice for 400 in new fiscal year
|
||||
pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True)
|
||||
pi2.posting_date = today()
|
||||
pi2.set_posting_time = 1
|
||||
pi2.supplier = self.supplier
|
||||
pi2.currency = "INR"
|
||||
pi2.credit_to = self.creditors
|
||||
pi2.save()
|
||||
pi2.submit()
|
||||
|
||||
# Allocate 600 from advance payment to purchase invoice
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pe.reload()
|
||||
pi2.reload()
|
||||
|
||||
# Assert advance payment is fully allocated
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 0)
|
||||
|
||||
# Assert new invoice is fully paid
|
||||
self.assertEqual(pi2.outstanding_amount, 0)
|
||||
self.assertEqual(pi2.status, "Paid")
|
||||
|
||||
# Verify reconciliation dates are correct based on company setting
|
||||
self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date))
|
||||
|
||||
frappe.db.set_value("Company", self.company, default_settings)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"debit_or_credit_note_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
@@ -168,19 +169,25 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_or_credit_note_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Debit / Credit Note Posting Date"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:10.704417",
|
||||
"modified": "2025-08-20 19:12:50.406769",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class PaymentReconciliationAllocation(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
debit_or_credit_note_posting_date: DF.Date | None
|
||||
difference_account: DF.Link | None
|
||||
difference_amount: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
|
||||
@@ -9,6 +9,14 @@ frappe.ui.form.on("Payment Request", {
|
||||
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_gateway_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
|
||||
@@ -464,10 +464,7 @@ class PaymentRequest(Document):
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
|
||||
def update_reference_advance_payment_status(self):
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
if self.reference_doctype in get_advance_payment_doctypes():
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
ref_doc.set_advance_payment_status()
|
||||
|
||||
@@ -537,7 +534,8 @@ def make_payment_request(**args):
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
@@ -784,7 +782,7 @@ def get_gateway_details(args): # nosemgrep
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
"""
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
|
||||
@@ -826,8 +824,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
if not references:
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
|
||||
@@ -34,12 +34,14 @@ payment_method = [
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank - _TC",
|
||||
"currency": "INR",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
@@ -47,6 +49,7 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Other",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
@@ -54,6 +57,7 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Phone",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -67,7 +71,11 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
for method in payment_method:
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account",
|
||||
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
|
||||
{
|
||||
"payment_gateway": method["payment_gateway"],
|
||||
"currency": method["currency"],
|
||||
"company": method["company"],
|
||||
},
|
||||
"name",
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
@@ -103,7 +111,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dt="Sales Order",
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Order")
|
||||
@@ -117,7 +125,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dt="Sales Invoice",
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Invoice")
|
||||
@@ -130,7 +138,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Other - USD",
|
||||
payment_gateway_account="_Test Gateway Other - USD - _TC",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -145,7 +153,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
submit_doc=False,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -163,7 +171,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Phone - USD",
|
||||
payment_gateway_account="_Test Gateway Phone - USD - _TC",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -180,7 +188,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -201,7 +209,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
make_sales_invoice=True,
|
||||
mute_email=True,
|
||||
submit_doc=True,
|
||||
@@ -232,7 +240,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -257,7 +265,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -276,7 +284,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -300,7 +308,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -322,7 +330,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -366,7 +374,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -471,7 +479,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
@@ -813,3 +821,33 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
def test_payment_request_on_unreconcile(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=pi.doctype,
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": pe.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -210,8 +210,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
|
||||
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
|
||||
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -220,14 +222,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"account_currency": frappe.db.get_value(
|
||||
"Account", self.closing_account_head, "account_currency"
|
||||
),
|
||||
"debit_in_account_currency": balance_in_account_currency
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": self.name,
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"label": "User Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "pos_opening_entry.company",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
@@ -103,6 +104,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cashier",
|
||||
"options": "User",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -259,7 +261,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-06 12:00:31.955176",
|
||||
"modified": "2025-06-14 02:38:14.962291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
||||
@@ -209,13 +209,16 @@ class POSClosingEntry(StatusUpdater):
|
||||
def on_submit(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
frappe.publish_realtime(
|
||||
f"poe_{self.pos_opening_entry}_closed",
|
||||
self,
|
||||
f"poe_{self.pos_opening_entry}",
|
||||
message={"operation": "Closed", "doc": self},
|
||||
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||
)
|
||||
|
||||
self.update_sales_invoices_closing_entry()
|
||||
|
||||
def before_cancel(self):
|
||||
self.check_pce_is_cancellable()
|
||||
|
||||
def on_cancel(self):
|
||||
unconsolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
@@ -237,6 +240,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
"Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None
|
||||
)
|
||||
|
||||
def check_pce_is_cancellable(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("Cannot cancel POS Closing Entry"),
|
||||
msg=_(
|
||||
"POS Profile - {0} is currently open. Please close the POS or cancel the existing POS Opening Entry before cancelling this POS Closing Entry."
|
||||
).format(frappe.bold(self.pos_profile)),
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -267,7 +279,7 @@ def get_invoices(start, end, pos_profile, user):
|
||||
|
||||
def get_payments(invoices):
|
||||
if not len(invoices):
|
||||
return
|
||||
return []
|
||||
|
||||
invoices_name = [d.name for d in invoices]
|
||||
|
||||
@@ -301,7 +313,7 @@ def get_payments(invoices):
|
||||
|
||||
def get_taxes(invoices):
|
||||
if not len(invoices):
|
||||
return
|
||||
return []
|
||||
|
||||
invoices_name = [d.name for d in invoices]
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
}
|
||||
|
||||
company() {
|
||||
erpnext.utils.set_letter_head(this.frm);
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
this.frm.set_value("set_warehouse", "");
|
||||
this.frm.set_value("taxes_and_charges", "");
|
||||
@@ -54,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
|
||||
if (this.frm.doc.pos_profile) {
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
this.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onload_post_render(frm) {
|
||||
@@ -66,6 +77,13 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
|
||||
if (doc.docstatus == 1 && !doc.is_return) {
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
|
||||
if (["Partly Paid", "Overdue", "Unpaid"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
this.collect_outstanding_payment.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
@@ -112,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||
this.frm.doc.campaign = r.message.campaign;
|
||||
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||
this.frm.set_default_payment = r.message.set_default_payment;
|
||||
}
|
||||
this.frm.script_manager.trigger("update_stock");
|
||||
this.calculate_taxes_and_totals();
|
||||
@@ -210,6 +229,138 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
async collect_outstanding_payment() {
|
||||
const total_amount = flt(this.frm.doc.rounded_total) | flt(this.frm.doc.grand_total);
|
||||
const paid_amount = flt(this.frm.doc.paid_amount);
|
||||
const outstanding_amount = flt(this.frm.doc.outstanding_amount);
|
||||
const me = this;
|
||||
|
||||
const table_fields = [
|
||||
{
|
||||
fieldname: "mode_of_payment",
|
||||
fieldtype: "Link",
|
||||
in_list_view: 1,
|
||||
label: __("Mode of Payment"),
|
||||
options: "Mode of Payment",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "amount",
|
||||
fieldtype: "Currency",
|
||||
in_list_view: 1,
|
||||
label: __("Amount"),
|
||||
options: this.frm.doc.currency,
|
||||
reqd: 1,
|
||||
onchange: function () {
|
||||
dialog.fields_dict.payments.df.data.some((d) => {
|
||||
if (d.idx == this.doc.idx) {
|
||||
d.amount = this.value === null ? 0 : this.value;
|
||||
dialog.fields_dict.payments.grid.refresh();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
let amount = 0;
|
||||
for (let d of dialog.fields_dict.payments.df.data) {
|
||||
amount += d.amount;
|
||||
}
|
||||
|
||||
let change_amount = total_amount - (paid_amount + amount);
|
||||
|
||||
dialog.fields_dict.outstanding_amount.set_value(
|
||||
outstanding_amount - amount < 0 ? 0 : outstanding_amount - amount
|
||||
);
|
||||
dialog.fields_dict.paid_amount.set_value(paid_amount + amount);
|
||||
dialog.fields_dict.change_amount.set_value(change_amount < 0 ? change_amount * -1 : 0);
|
||||
},
|
||||
},
|
||||
];
|
||||
const payment_method_data = await this.fetch_pos_payment_methods();
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Collect Outstanding Amount"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "payments",
|
||||
fieldtype: "Table",
|
||||
label: __("Payments"),
|
||||
cannot_add_rows: false,
|
||||
in_place_edit: true,
|
||||
reqd: 1,
|
||||
data: payment_method_data,
|
||||
fields: table_fields,
|
||||
},
|
||||
{
|
||||
fieldname: "section_break_1",
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "outstanding_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Outstanding Amount"),
|
||||
read_only: 1,
|
||||
default: outstanding_amount,
|
||||
},
|
||||
{
|
||||
fieldname: "column_break_1",
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldname: "paid_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Paid Amount"),
|
||||
read_only: 1,
|
||||
default: paid_amount,
|
||||
},
|
||||
{
|
||||
fieldname: "change_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Change Amount"),
|
||||
read_only: 1,
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Submit"),
|
||||
primary_action(values) {
|
||||
dialog.hide();
|
||||
me.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "update_payments",
|
||||
args: {
|
||||
payments: values.payments.filter((d) => d.amount != 0),
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert({
|
||||
message: __("Payments updated."),
|
||||
indicator: "green",
|
||||
});
|
||||
me.frm.reload_doc();
|
||||
} else {
|
||||
frappe.show_alert({
|
||||
message: __("Payments could not be updated."),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
async fetch_pos_payment_methods() {
|
||||
const pos_profile = this.frm.doc.pos_profile;
|
||||
if (!pos_profile) return;
|
||||
const pos_profile_doc = await frappe.db.get_doc("POS Profile", pos_profile);
|
||||
const data = [];
|
||||
pos_profile_doc.payments.forEach((pay) => {
|
||||
const { mode_of_payment } = pay;
|
||||
data.push({ mode_of_payment, amount: 0 });
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }));
|
||||
|
||||
@@ -30,24 +30,6 @@
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -61,6 +43,7 @@
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
@@ -91,14 +74,6 @@
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"total_taxes_and_charges",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"section_break_49",
|
||||
"coupon_code",
|
||||
"apply_discount_on",
|
||||
@@ -118,13 +93,7 @@
|
||||
"in_words",
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"payments_tab",
|
||||
"payments_section",
|
||||
"cash_bank_account",
|
||||
"payments",
|
||||
@@ -137,6 +106,10 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
@@ -144,9 +117,41 @@
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"contact_and_address_tab",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"more_info_tab",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
@@ -292,6 +297,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -398,7 +404,6 @@
|
||||
"label": "Customer's Purchase Order Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
@@ -1050,7 +1055,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1130,8 +1134,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_88",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Changes"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_pos",
|
||||
@@ -1218,7 +1224,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "terms",
|
||||
"fieldname": "terms_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1330,7 +1335,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nPartly Paid\nUnpaid\nPartly Paid and Discounted\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1416,6 +1421,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
@@ -1568,12 +1575,39 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-06 15:03:19.957277",
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1618,6 +1652,7 @@
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -149,7 +149,9 @@ class POSInvoice(SalesInvoice):
|
||||
"Consolidated",
|
||||
"Submitted",
|
||||
"Paid",
|
||||
"Partly Paid",
|
||||
"Unpaid",
|
||||
"Partly Paid and Discounted",
|
||||
"Unpaid and Discounted",
|
||||
"Overdue and Discounted",
|
||||
"Overdue",
|
||||
@@ -215,11 +217,15 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
self.update_packing_list()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
validate_coupon_code(self.coupon_code)
|
||||
|
||||
def before_submit(self):
|
||||
self.set_outstanding_amount()
|
||||
|
||||
def on_submit(self):
|
||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||
if not self.is_return and self.loyalty_program:
|
||||
@@ -373,18 +379,6 @@ class POSInvoice(SalesInvoice):
|
||||
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
||||
)
|
||||
|
||||
def validate_pos_opening_entry(self):
|
||||
opening_entries = frappe.get_list(
|
||||
"POS Opening Entry", filters={"pos_profile": self.pos_profile, "status": "Open", "docstatus": 1}
|
||||
)
|
||||
if len(opening_entries) == 0:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(self.pos_profile)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
@@ -417,9 +411,9 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
).format(d.idx, item_code, warehouse, available_stock),
|
||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
@@ -537,6 +531,10 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
)
|
||||
|
||||
def set_outstanding_amount(self):
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
self.outstanding_amount = total - flt(self.paid_amount) if total > flt(self.paid_amount) else 0
|
||||
|
||||
def validate_loyalty_transaction(self):
|
||||
if self.redeem_loyalty_points and (
|
||||
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
|
||||
@@ -558,6 +556,8 @@ class POSInvoice(SalesInvoice):
|
||||
self.status = "Draft"
|
||||
return
|
||||
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
@@ -573,6 +573,14 @@ class POSInvoice(SalesInvoice):
|
||||
self.status = "Overdue and Discounted"
|
||||
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
|
||||
self.status = "Overdue"
|
||||
elif (
|
||||
0 < flt(self.outstanding_amount) < total
|
||||
and self.is_discounted
|
||||
and self.get_discounting_status() == "Disbursed"
|
||||
):
|
||||
self.status = "Partly Paid and Discounted"
|
||||
elif 0 < flt(self.outstanding_amount) < total:
|
||||
self.status = "Partly Paid"
|
||||
elif (
|
||||
flt(self.outstanding_amount) > 0
|
||||
and getdate(self.due_date) >= getdate(nowdate())
|
||||
@@ -709,7 +717,13 @@ class POSInvoice(SalesInvoice):
|
||||
"Account", self.debit_to, "account_currency"
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -724,6 +738,7 @@ class POSInvoice(SalesInvoice):
|
||||
"utm_campaign": profile.get("utm_campaign"),
|
||||
"utm_medium": profile.get("utm_medium"),
|
||||
"allow_print_before_pay": profile.get("allow_print_before_pay"),
|
||||
"set_default_payment": profile.get("set_grand_total_to_default_mop"),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -793,6 +808,48 @@ class POSInvoice(SalesInvoice):
|
||||
if pr:
|
||||
return frappe.get_doc("Payment Request", pr)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_payments(self, payments):
|
||||
if self.status == "Consolidated":
|
||||
frappe.throw(_("Create Payment Entry for Consolidated POS Invoices."))
|
||||
|
||||
paid_amount = flt(self.paid_amount)
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if paid_amount >= total:
|
||||
frappe.throw(title=_("Invoice Paid"), msg=_("This invoice has already been paid."))
|
||||
|
||||
idx = self.payments[-1].idx if self.payments else -1
|
||||
|
||||
for d in payments:
|
||||
idx += 1
|
||||
payment = create_payments_on_invoice(self, idx, frappe._dict(d))
|
||||
paid_amount += flt(payment.amount)
|
||||
payment.submit()
|
||||
|
||||
paid_amount = flt(flt(paid_amount), self.precision("paid_amount"))
|
||||
base_paid_amount = flt(flt(paid_amount * self.conversion_rate), self.precision("base_paid_amount"))
|
||||
outstanding_amount = (
|
||||
flt(flt(total - paid_amount), self.precision("outstanding_amount")) if total > paid_amount else 0
|
||||
)
|
||||
change_amount = (
|
||||
flt(flt(paid_amount - total), self.precision("change_amount")) if paid_amount > total else 0
|
||||
)
|
||||
|
||||
pi = frappe.qb.DocType("POS Invoice")
|
||||
query = (
|
||||
frappe.qb.update(pi)
|
||||
.set(pi.paid_amount, paid_amount)
|
||||
.set(pi.base_paid_amount, base_paid_amount)
|
||||
.set(pi.outstanding_amount, outstanding_amount)
|
||||
.set(pi.change_amount, change_amount)
|
||||
.where(pi.name == self.name)
|
||||
)
|
||||
query.run()
|
||||
self.reload()
|
||||
|
||||
self.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
@@ -818,10 +875,8 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
bundle_bin_qty = 1000000
|
||||
for item in product_bundle.items:
|
||||
item_bin_qty = get_bin_qty(item.item_code, warehouse)
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
max_available_bundles = item_bin_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
@@ -844,13 +899,49 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
"""
|
||||
Calculate total quantity reserved for the given item and warehouse.
|
||||
|
||||
Includes:
|
||||
- Direct sales of the item in submitted POS Invoices
|
||||
- Sales of the item as a component of a Product Bundle
|
||||
|
||||
Excludes consolidated invoices (already merged into Sales Invoices via
|
||||
POS Closing Entry). Used to reflect near real-time availability in the
|
||||
POS UI and to prevent overselling while multiple sessions may be active.
|
||||
"""
|
||||
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
|
||||
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
|
||||
|
||||
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
|
||||
|
||||
return reserved_qty
|
||||
|
||||
|
||||
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
|
||||
"""
|
||||
Get the total reserved quantity for a given item in POS Invoices
|
||||
from a specific child table.
|
||||
|
||||
Args:
|
||||
child_table (str): Name of the child table to query
|
||||
(e.g., "POS Invoice Item", "Packed Item").
|
||||
item_code (str): The Item Code to filter by.
|
||||
warehouse (str): The Warehouse to filter by.
|
||||
|
||||
Returns:
|
||||
float: The total reserved quantity for the item in the given
|
||||
warehouse from submitted, unconsolidated POS Invoices.
|
||||
"""
|
||||
p_inv = frappe.qb.DocType("POS Invoice")
|
||||
p_item = frappe.qb.DocType("POS Invoice Item")
|
||||
p_item = frappe.qb.DocType(child_table)
|
||||
|
||||
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
|
||||
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.stock_qty).as_("stock_qty"))
|
||||
.select(Sum(p_item[qty_column]).as_("stock_qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
@@ -944,3 +1035,19 @@ def get_item_group(pos_profile):
|
||||
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
|
||||
def create_payments_on_invoice(doc, idx, payment_details):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
payment = frappe.new_doc("Sales Invoice Payment")
|
||||
payment.idx = idx
|
||||
payment.mode_of_payment = payment_details.mode_of_payment
|
||||
payment.amount = payment_details.amount
|
||||
payment.base_amount = payment.amount * doc.conversion_rate
|
||||
payment.parent = doc.name
|
||||
payment.parentfield = "payments"
|
||||
payment.parenttype = doc.doctype
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, doc.company).get("account")
|
||||
|
||||
return payment
|
||||
|
||||
@@ -18,11 +18,13 @@ frappe.listview_settings["POS Invoice"] = {
|
||||
Draft: "red",
|
||||
Unpaid: "orange",
|
||||
Paid: "green",
|
||||
"Partly Paid": "yellow",
|
||||
Submitted: "blue",
|
||||
Consolidated: "green",
|
||||
Return: "darkgrey",
|
||||
"Unpaid and Discounted": "orange",
|
||||
"Overdue and Discounted": "red",
|
||||
"Partly Paid and Discounted": "yellow",
|
||||
Overdue: "red",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||
|
||||
@@ -401,6 +401,50 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
|
||||
def test_partly_paid_invoices(self):
|
||||
set_allow_partial_payment(self.pos_profile, 1)
|
||||
|
||||
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 90},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
self.assertEqual(pos_inv.paid_amount, 90)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 10}])
|
||||
self.assertEqual(pos_inv.paid_amount, 100)
|
||||
self.assertEqual(pos_inv.status, "Paid")
|
||||
|
||||
set_allow_partial_payment(self.pos_profile, 0)
|
||||
|
||||
def test_multi_payment_for_partly_paid_invoices(self):
|
||||
set_allow_partial_payment(self.pos_profile, 1)
|
||||
|
||||
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 90},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
self.assertEqual(pos_inv.paid_amount, 90)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
|
||||
self.assertEqual(pos_inv.paid_amount, 95)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
|
||||
self.assertEqual(pos_inv.paid_amount, 100)
|
||||
self.assertEqual(pos_inv.status, "Paid")
|
||||
|
||||
set_allow_partial_payment(self.pos_profile, 0)
|
||||
|
||||
def test_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -1089,8 +1133,7 @@ def create_pos_invoice(**args):
|
||||
return pos_inv
|
||||
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
|
||||
def set_allow_partial_payment(pos_profile, value):
|
||||
pos_profile.reload()
|
||||
pos_profile.allow_partial_payment = value
|
||||
pos_profile.save()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
@@ -113,12 +114,22 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:15.620564",
|
||||
"modified": "2025-07-02 17:08:04.747202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -179,8 +190,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@ class POSInvoiceMergeLog(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consolidated_credit_note: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
customer: DF.Link
|
||||
@@ -339,6 +338,11 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
invoice.pos_profile = ""
|
||||
|
||||
# Unset Commission Section
|
||||
invoice.set("sales_partner", None)
|
||||
invoice.set("commission_rate", 0)
|
||||
invoice.set("total_commission", 0)
|
||||
|
||||
return invoice
|
||||
|
||||
def get_new_sales_invoice(self):
|
||||
@@ -584,6 +588,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.company = closing_entry.get("company") if closing_entry else None
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
|
||||
@@ -491,3 +491,26 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
|
||||
def test_company_in_pos_invoice_merge_log(self):
|
||||
"""
|
||||
Test if the company is fetched from POS Closing Entry
|
||||
"""
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
closing_entry = make_closing_entry_from_opening(opening_entry)
|
||||
closing_entry.insert()
|
||||
closing_entry.submit()
|
||||
|
||||
self.assertTrue(frappe.db.exists("POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}))
|
||||
|
||||
pos_merge_log_company = frappe.db.get_value(
|
||||
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
|
||||
)
|
||||
self.assertEqual(pos_merge_log_company, closing_entry.company)
|
||||
|
||||
@@ -37,6 +37,8 @@ class POSOpeningEntry(StatusUpdater):
|
||||
|
||||
def validate(self):
|
||||
self.validate_pos_profile_and_cashier()
|
||||
self.check_open_pos_exists()
|
||||
self.check_user_already_assigned()
|
||||
self.validate_payment_method_account()
|
||||
self.set_status()
|
||||
|
||||
@@ -49,6 +51,22 @@ class POSOpeningEntry(StatusUpdater):
|
||||
if not cint(frappe.db.get_value("User", self.user, "enabled")):
|
||||
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
|
||||
|
||||
def check_open_pos_exists(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Exists"),
|
||||
msg=_(
|
||||
"{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry."
|
||||
).format(frappe.bold(self.pos_profile)),
|
||||
)
|
||||
|
||||
def check_user_already_assigned(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"user": self.user, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("Cannot Assign Cashier"),
|
||||
msg=_("Cashier is currently assigned to another POS."),
|
||||
)
|
||||
|
||||
def validate_payment_method_account(self):
|
||||
invalid_modes = []
|
||||
for d in self.balance_details:
|
||||
@@ -71,5 +89,25 @@ class POSOpeningEntry(StatusUpdater):
|
||||
def on_submit(self):
|
||||
self.set_status(update=True)
|
||||
|
||||
def before_cancel(self):
|
||||
self.check_poe_is_cancellable()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status(update=True)
|
||||
frappe.publish_realtime(
|
||||
f"poe_{self.name}",
|
||||
message={"operation": "Cancelled"},
|
||||
docname=f"POS Opening Entry/{self.name}",
|
||||
)
|
||||
|
||||
def check_poe_is_cancellable(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import get_invoices
|
||||
|
||||
invoices = get_invoices(
|
||||
self.period_start_date, frappe.utils.get_datetime(), self.pos_profile, self.user
|
||||
)
|
||||
if invoices.get("invoices"):
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Cancellation Error"),
|
||||
msg=_("POS Opening Entry cannot be cancelled as unconsolidated Invoices exists."),
|
||||
)
|
||||
|
||||
@@ -3,14 +3,107 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestPOSOpeningEntry(IntegrationTestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"}))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
|
||||
def setUp(self):
|
||||
# Make stock available for POS Sales
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
|
||||
self.init_user_and_profile = init_user_and_profile
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_pos_opening_entry(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
self.assertNotEqual(opening_entry.docstatus, 0)
|
||||
|
||||
def test_multiple_pos_opening_entries_for_same_pos_profile(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry_1.status, "Open")
|
||||
self.assertEqual(opening_entry_1.user, test_user.name)
|
||||
|
||||
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
|
||||
frappe.set_user(cashier_user.name)
|
||||
|
||||
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
|
||||
opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name)
|
||||
|
||||
self.assertEqual(opening_entry_2.status, "Open")
|
||||
self.assertEqual(opening_entry_2.user, cashier_user.name)
|
||||
|
||||
def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile, cashier_user.name)
|
||||
|
||||
def test_user_assignment_to_multiple_pos_profile(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
|
||||
self.assertEqual(opening_entry_1.user, test_user.name)
|
||||
|
||||
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile2, test_user.name)
|
||||
|
||||
def test_cancel_pos_opening_entry_without_invoices(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
|
||||
|
||||
opening_entry.cancel()
|
||||
self.assertEqual(opening_entry.status, "Cancelled")
|
||||
self.assertNotEqual(opening_entry.docstatus, 1)
|
||||
|
||||
def test_cancel_pos_opening_entry_with_invoice(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
|
||||
|
||||
pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, opening_entry.cancel)
|
||||
|
||||
|
||||
def create_opening_entry(pos_profile, user):
|
||||
def create_opening_entry(pos_profile, user, get_obj=False):
|
||||
entry = frappe.new_doc("POS Opening Entry")
|
||||
entry.pos_profile = pos_profile.name
|
||||
entry.user = user
|
||||
@@ -24,4 +117,7 @@ def create_opening_entry(pos_profile, user):
|
||||
entry.set("balance_details", balance_details)
|
||||
entry.submit()
|
||||
|
||||
if get_obj:
|
||||
return entry
|
||||
|
||||
return entry.as_dict()
|
||||
|
||||
@@ -135,6 +135,7 @@ frappe.ui.form.on("POS Profile", {
|
||||
company: function (frm) {
|
||||
frm.trigger("toggle_display_account_head");
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
toggle_display_account_head: function (frm) {
|
||||
|
||||
@@ -26,13 +26,14 @@
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"action_on_new_invoice",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"set_grand_total_to_default_mop",
|
||||
"action_on_new_invoice",
|
||||
"allow_partial_payment",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -423,6 +424,12 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Action on New Invoice",
|
||||
"options": "Always Ask\nSave Changes and Load New Invoice\nDiscard Changes and Load New Invoice"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Partial Payment"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -451,7 +458,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-23 12:12:32.247652",
|
||||
"modified": "2025-06-24 11:19:19.834905",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -32,6 +32,7 @@ class POSProfile(Document):
|
||||
"Always Ask", "Save Changes and Load New Invoice", "Discard Changes and Load New Invoice"
|
||||
]
|
||||
allow_discount_change: DF.Check
|
||||
allow_partial_payment: DF.Check
|
||||
allow_rate_change: DF.Check
|
||||
applicable_for_users: DF.Table[POSProfileUser]
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Field"
|
||||
"label": "Field",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:16.969895",
|
||||
"modified": "2025-07-29 18:08:40.323579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Search Fields",
|
||||
@@ -35,4 +36,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,56 +41,68 @@ frappe.ui.form.on("Pricing Rule", {
|
||||
<tr><td>
|
||||
<h4>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
{{__('Notes')}}
|
||||
${__("Notes")}
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
{{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
|
||||
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
|
||||
</li>
|
||||
<li>
|
||||
{{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
|
||||
${__(
|
||||
"If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field."
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
|
||||
${__("Discount Percentage can be applied either against a Price List or for all Price List.")}
|
||||
</li>
|
||||
<li>
|
||||
{{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
|
||||
${__(
|
||||
"To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled."
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<h4><i class="fa fa-question-sign"></i>
|
||||
{{__('How Pricing Rule is applied?')}}
|
||||
${__("How Pricing Rule is applied?")}
|
||||
</h4>
|
||||
<ol>
|
||||
<li>
|
||||
{{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
|
||||
${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}
|
||||
</li>
|
||||
<li>
|
||||
{{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
|
||||
${__(
|
||||
"Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc."
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{{__('Pricing Rules are further filtered based on quantity.')}}
|
||||
${__("Pricing Rules are further filtered based on quantity.")}
|
||||
</li>
|
||||
<li>
|
||||
{{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
|
||||
${__(
|
||||
"If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions."
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
|
||||
${__(
|
||||
"Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:"
|
||||
)}
|
||||
<ul>
|
||||
<li>
|
||||
{{__('Item Code > Item Group > Brand')}}
|
||||
${__("Item Code > Item Group > Brand")}
|
||||
</li>
|
||||
<li>
|
||||
{{__('Customer > Customer Group > Territory')}}
|
||||
${__("Customer > Customer Group > Territory")}
|
||||
</li>
|
||||
<li>
|
||||
{{__('Supplier > Supplier Type')}}
|
||||
${__("Supplier > Supplier Type")}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
{{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
|
||||
${__(
|
||||
"If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict."
|
||||
)}
|
||||
</li>
|
||||
</ol>
|
||||
</td></tr>
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "is_cumulative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cumulative"
|
||||
@@ -656,7 +657,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -169,7 +169,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
@@ -702,17 +702,6 @@ def set_transaction_type(args):
|
||||
args.transaction_type = "buying"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_pricing_rule(doctype, docname):
|
||||
doc = frappe.new_doc("Pricing Rule")
|
||||
doc.applicable_for = doctype
|
||||
doc.set(frappe.scrub(doctype), docname)
|
||||
doc.selling = 1 if doctype == "Customer" else 0
|
||||
doc.buying = 1 if doctype == "Supplier" else 0
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -206,6 +206,56 @@ class TestPricingRule(IntegrationTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_unset_group_condition(self):
|
||||
"""
|
||||
If args are not set for group condition, then pricing rule should not be applied.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Item"}],
|
||||
"currency": "USD",
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"discount_percentage": 10,
|
||||
"applicable_for": "Territory",
|
||||
"territory": "All Territories",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "_Test Currency",
|
||||
"doctype": "Sales Order",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
}
|
||||
)
|
||||
|
||||
# without territory in customer
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
territory = customer.territory
|
||||
|
||||
customer.territory = None
|
||||
customer.save()
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 0)
|
||||
|
||||
customer.territory = territory
|
||||
customer.save()
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
|
||||
elif allow_blank:
|
||||
condition = f"ifnull({table}.{field}, '') = ''"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
original_gle = [
|
||||
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
|
||||
[deferred_account, 0.0, 3000, "2023-07-01"],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
|
||||
|
||||
# cancel the process deferred accounting document
|
||||
process_deferred_accounting.cancel()
|
||||
|
||||
# check if gl entries are cancelled
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
change_acc_settings()
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
|
||||
@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("account", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -79,6 +88,12 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
company: function (frm) {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"sales_person",
|
||||
"show_remarks",
|
||||
"based_on_payment_terms",
|
||||
"show_future_payments",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
"collection_name",
|
||||
@@ -78,18 +79,18 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Date",
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@@ -330,7 +331,8 @@
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
"label": "Posting Date",
|
||||
"mandatory_depends_on": "eval:(doc.report == 'Accounts Receivable');"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
@@ -376,7 +378,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -397,10 +399,17 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "show_future_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Future Payments"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified": "2025-08-29 00:20:08.088189",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -72,6 +72,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
sales_partner: DF.Link | None
|
||||
sales_person: DF.Link | None
|
||||
sender: DF.Link | None
|
||||
show_future_payments: DF.Check
|
||||
show_net_values_in_party_account: DF.Check
|
||||
show_remarks: DF.Check
|
||||
start_date: DF.Date | None
|
||||
@@ -82,6 +83,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
self.validate_company_for_table("Cost Center")
|
||||
self.validate_company_for_table("Project")
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
@@ -104,6 +109,43 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
|
||||
if self.company != frappe.get_cached_value("Account", self.account, "company"):
|
||||
frappe.throw(
|
||||
_("Account {0} doesn't belong to Company {1}").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company_for_table(self, doctype):
|
||||
field = frappe.scrub(doctype)
|
||||
if not self.get(field):
|
||||
return
|
||||
|
||||
fieldname = field + "_name"
|
||||
|
||||
values = set(d.get(fieldname) for d in self.get(field))
|
||||
invalid_values = frappe.db.get_all(
|
||||
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
|
||||
)
|
||||
|
||||
if invalid_values:
|
||||
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
|
||||
doctype, frappe.bold(self.company)
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
@@ -225,6 +267,7 @@ def get_ar_filters(doc, entry):
|
||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||
"territory": doc.territory if doc.territory else None,
|
||||
"based_on_payment_terms": doc.based_on_payment_terms,
|
||||
"show_future_payments": doc.show_future_payments,
|
||||
"report_name": "Accounts Receivable",
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
@@ -509,7 +552,7 @@ def send_auto_email():
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"enable_auto_email": 1},
|
||||
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
|
||||
or_filters={"to_date": today(), "posting_date": today()},
|
||||
)
|
||||
for entry in selected:
|
||||
send_emails(entry.name, from_scheduler=True)
|
||||
|
||||
@@ -52,20 +52,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% set balance_row = data.slice(-1).pop() %}
|
||||
{% if(filters.show_future_payments)%}
|
||||
{% set balance_row = data[-1] %}
|
||||
|
||||
{% set ns = namespace(idx=None) %}
|
||||
{% for i in report.columns %}
|
||||
{% if i.fieldname == 'age' %}
|
||||
{% set elem = i %}
|
||||
{% if i.fieldname == "age" and ns.idx is none %}
|
||||
{% set ns.idx = loop.index0 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set start = report.columns.findIndex(elem) %}
|
||||
{% set range1 = report.columns[start].label %}
|
||||
{% set range2 = report.columns[start+1].label %}
|
||||
{% set range3 = report.columns[start+2].label %}
|
||||
{% set range4 = report.columns[start+3].label %}
|
||||
{% set range5 = report.columns[start+4].label %}
|
||||
{% set range6 = report.columns[start+5].label %}
|
||||
|
||||
{% set age = report.columns[ns.idx].label %}
|
||||
{% set range1 = report.columns[ns.idx+1].label %}
|
||||
{% set range2 = report.columns[ns.idx+2].label %}
|
||||
{% set range3 = report.columns[ns.idx+3].label %}
|
||||
{% set range4 = report.columns[ns.idx+4].label %}
|
||||
{% set range5 = report.columns[ns.idx+5].label %}
|
||||
|
||||
{% if(balance_row) %}
|
||||
<table class="table table-bordered table-condensed">
|
||||
@@ -84,12 +86,12 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _(" ") }}</th>
|
||||
<th>{{ _(age) }}</th>
|
||||
<th>{{ _(range1) }}</th>
|
||||
<th>{{ _(range2) }}</th>
|
||||
<th>{{ _(range3) }}</th>
|
||||
<th>{{ _(range4) }}</th>
|
||||
<th>{{ _(range5) }}</th>
|
||||
<th>{{ _(range6) }}</th>
|
||||
<th>{{ _("Total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -97,25 +99,25 @@
|
||||
<tr>
|
||||
<td>{{ _("Total Outstanding") }}</td>
|
||||
<td class="text-right">
|
||||
{{ format_number(balance_row["age"], null, 2) }}
|
||||
{{ frappe.utils.flt(balance_row["age"], 2) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"]), currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>{{ _("Future Payments") }}</td>
|
||||
@@ -126,7 +128,7 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["future_amount"]), currency=balance_row["currency"]) }}
|
||||
</td>
|
||||
<tr class="cvs-footer">
|
||||
<th class="text-left">{{ _("Cheques Required") }}</th>
|
||||
@@ -137,7 +139,7 @@
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="text-right">
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"] - balance_row["future_amount"]), currency=balance_row["currency"]) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import create_batch, getdate
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import DateTimeLikeObject, process_all
|
||||
|
||||
@@ -23,7 +23,23 @@ class ProcessSubscription(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def on_submit(self):
|
||||
process_all(subscription=self.subscription, posting_date=self.posting_date)
|
||||
self.process_all_subscription()
|
||||
|
||||
def process_all_subscription(self):
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
if self.subscription:
|
||||
filters["name"] = self.subscription
|
||||
|
||||
subscriptions = frappe.get_all("Subscription", filters, pluck="name")
|
||||
|
||||
for subscription in create_batch(subscriptions, 500):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
queue="long",
|
||||
subscription=subscription,
|
||||
posting_date=self.posting_date,
|
||||
)
|
||||
|
||||
|
||||
def create_subscription_process(
|
||||
|
||||
@@ -93,12 +93,14 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "mixed_conditions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mixed Conditions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "is_cumulative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cumulative"
|
||||
@@ -278,7 +280,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:22.103686",
|
||||
"modified": "2025-08-20 11:48:23.231081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme",
|
||||
|
||||
@@ -35,7 +35,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
filters: {
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -162,6 +165,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -184,6 +190,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
company: me.frm.doc.company,
|
||||
is_return: 0,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -647,7 +656,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
add_custom_buttons: function (frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100 && frm.doc.update_stock == 0) {
|
||||
frm.add_custom_button(
|
||||
__("Purchase Receipt"),
|
||||
() => {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"col_break_warehouse",
|
||||
"update_stock",
|
||||
"set_warehouse",
|
||||
@@ -63,6 +64,7 @@
|
||||
"column_break_50",
|
||||
"base_total",
|
||||
"base_net_total",
|
||||
"claimed_landed_cost_amount",
|
||||
"column_break_28",
|
||||
"total",
|
||||
"net_total",
|
||||
@@ -321,6 +323,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -1651,6 +1654,22 @@
|
||||
"label": "Select Dispatch Address ",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "claimed_landed_cost_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Claimed Landed Cost Amount (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1658,7 +1677,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-09 16:49:22.175081",
|
||||
"modified": "2025-08-04 19:19:11.380664",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1723,4 +1742,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -104,6 +106,7 @@ class PurchaseInvoice(BuyingController):
|
||||
billing_address_display: DF.TextEditor | None
|
||||
buying_price_list: DF.Link | None
|
||||
cash_bank_account: DF.Link | None
|
||||
claimed_landed_cost_amount: DF.Currency
|
||||
clearance_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
@@ -340,7 +343,12 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
if not self.due_date:
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
|
||||
self.posting_date,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.company,
|
||||
self.bill_date,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
@@ -972,7 +980,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
@@ -1225,6 +1233,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
@@ -1309,8 +1320,37 @@ class PurchaseInvoice(BuyingController):
|
||||
net_amt_precision,
|
||||
)
|
||||
|
||||
# Stock ledger value is not matching with the warehouse amount
|
||||
if (
|
||||
if self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
elif (
|
||||
self.update_stock
|
||||
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||
and warehouse_debit_amount
|
||||
@@ -1338,38 +1378,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
@@ -2072,7 +2080,12 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
@@ -2082,6 +2095,11 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
@@ -2105,7 +2123,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.desk.form.linked_with import get_child_tables_of_doctypes
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
@@ -169,11 +170,15 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
frappe.db.delete(
|
||||
"Advance Payment Ledger Entry",
|
||||
filters={"voucher_type": doc.doctype, "voucher_no": doc.name},
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
@@ -185,7 +190,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
@@ -204,13 +209,29 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
def get_allowed_types_from_settings(child_doc: bool = False):
|
||||
repost_docs = [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
result = repost_docs
|
||||
|
||||
if repost_docs and child_doc:
|
||||
result.extend(get_child_docs(repost_docs))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_child_docs(doc: list) -> list:
|
||||
child_doc = []
|
||||
doc = get_child_tables_of_doctypes(doc)
|
||||
for child_list in doc.values():
|
||||
for child in child_list:
|
||||
if child.get("child_table"):
|
||||
child_doc.append(child["child_table"])
|
||||
return child_doc
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
|
||||
|
||||
|
||||
class RepostAccountingLedgerSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -17,6 +22,24 @@ class RepostAccountingLedgerSettings(Document):
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
allowed_types: DF.Table[RepostAllowedTypes]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
def update_property_for_accounting_dimension(self):
|
||||
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
|
||||
if not doctypes:
|
||||
return
|
||||
doctypes += get_child_docs(doctypes)
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
|
||||
def set_allow_on_submit_for_dimension_fields(doctypes):
|
||||
for dt in doctypes:
|
||||
meta = frappe.get_meta(dt)
|
||||
for dimension in get_accounting_dimensions():
|
||||
df = meta.get_field(dimension)
|
||||
if df and not df.allow_on_submit:
|
||||
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
|
||||
from erpnext.accounts.utils import _delete_adv_pl_entries, _delete_pl_entries, create_payment_ledger_entry
|
||||
|
||||
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
|
||||
@@ -16,6 +16,7 @@ VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal
|
||||
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
|
||||
if voucher_type and voucher_no and gle_map:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||
create_payment_ledger_entry(gle_map, cancel=0)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
me.frm.script_manager.trigger("is_pos");
|
||||
me.frm.refresh_fields();
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
erpnext.queries.setup_warehouse_query(this.frm);
|
||||
}
|
||||
@@ -265,6 +272,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -294,6 +304,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
status: ["!=", "Lost"],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -325,6 +338,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -503,8 +519,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
if (r.message) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
me.frm.set_default_payment = r.message.set_default_payment;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
if (me.frm.doc.taxes_and_charges) {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"update_stock",
|
||||
"column_break_39",
|
||||
"set_warehouse",
|
||||
@@ -373,6 +374,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"hide_days": 1,
|
||||
@@ -1997,6 +1999,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -2218,6 +2221,13 @@
|
||||
"label": "POS Closing Entry",
|
||||
"options": "POS Closing Entry",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -2231,7 +2241,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified": "2025-08-04 19:20:28.732039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -474,6 +475,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -762,6 +764,7 @@ class SalesInvoice(SellingController):
|
||||
"utm_campaign": pos.get("utm_campaign"),
|
||||
"utm_medium": pos.get("utm_medium"),
|
||||
"allow_print_before_pay": pos.get("allow_print_before_pay"),
|
||||
"set_default_payment": pos.get("set_grand_total_to_default_mop", 1),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1100,21 +1103,51 @@ class SalesInvoice(SellingController):
|
||||
if self.invoice_type_in_pos == "POS Invoice" and not self.is_return:
|
||||
frappe.throw(_("Transactions using Sales Invoice in POS are disabled."))
|
||||
|
||||
self.validate_pos_opening_entry()
|
||||
|
||||
def validate_full_payment(self):
|
||||
allow_partial_payment = frappe.db.get_value("POS Profile", self.pos_profile, "allow_partial_payment")
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
if (
|
||||
self.docstatus == 1
|
||||
and not self.is_return
|
||||
and not allow_partial_payment
|
||||
and self.paid_amount < invoice_total
|
||||
):
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
def validate_pos_opening_entry(self):
|
||||
opening_entries = frappe.get_all(
|
||||
"POS Opening Entry",
|
||||
fields=["name", "period_start_date"],
|
||||
filters={"pos_profile": self.pos_profile, "status": "Open"},
|
||||
order_by="period_start_date desc",
|
||||
)
|
||||
if not opening_entries:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(self.pos_profile)
|
||||
),
|
||||
)
|
||||
if len(opening_entries) > 1:
|
||||
frappe.throw(
|
||||
title=_("Multiple POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding."
|
||||
).format(self.pos_profile),
|
||||
)
|
||||
if frappe.utils.get_date_str(opening_entries[0].get("period_start_date")) != frappe.utils.today():
|
||||
frappe.throw(
|
||||
title=_("Outdated POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry."
|
||||
).format(opening_entries[0].get("name")),
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
super().validate_warehouse()
|
||||
@@ -1377,7 +1410,6 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests import IntegrationTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||
from frappe.utils import add_days, cint, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -2483,6 +2483,10 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2537,6 +2541,10 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2544,10 +2552,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
@@ -2587,10 +2591,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
@@ -2833,7 +2833,9 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(si.items[0].rate, rate)
|
||||
self.assertEqual(target_doc.items[0].rate, rate)
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
check_gl_entries(
|
||||
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
|
||||
)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
@@ -3033,6 +3035,28 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
# cases where distributed discount amount is not set
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Item",
|
||||
{"name": ["in", [d.name for d in si.items]]},
|
||||
"distributed_discount_amount",
|
||||
0,
|
||||
)
|
||||
|
||||
si.load_from_db()
|
||||
si.additional_discount_account = additional_discount_account
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
expected_gle = [
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 22.0, 0.0, nowdate()],
|
||||
["Service - _TC", 0.0, 100.0, nowdate()],
|
||||
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
@@ -4525,6 +4549,152 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0)
|
||||
|
||||
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "10"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe.source_exchange_rate = 85
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = si.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.save()
|
||||
ral.submit()
|
||||
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
q = (
|
||||
(
|
||||
frappe.qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.docstatus)
|
||||
.where(
|
||||
(je.voucher_type == "Exchange Gain Or Loss")
|
||||
& (jea.reference_name == si.name)
|
||||
& (jea.reference_type == "Sales Invoice")
|
||||
& (je.is_system_generated == 1)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
self.assertEqual(q[0][0], 1)
|
||||
|
||||
def test_non_batchwise_valuation_for_moving_average(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Non Batchwise Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBATCH-TCNV.####",
|
||||
"valuation_method": "Moving Average",
|
||||
},
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Stock Settings")
|
||||
original_value = cint(doc.do_not_use_batchwise_valuation)
|
||||
|
||||
doc.db_set("do_not_use_batchwise_valuation", 1)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=13.02,
|
||||
valuation_method="Moving Average",
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
se_batch = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
# without use serial and batch fields
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=1,
|
||||
rate=120,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=False,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
si.reload()
|
||||
si_batch = get_batch_from_bundle(si.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(se_batch, si_batch)
|
||||
self.assertEqual(si.items[0].use_serial_batch_fields, 0)
|
||||
|
||||
serial_and_batch_bundle = si.items[0].serial_and_batch_bundle
|
||||
change_in_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(change_in_value, 13.02 * -1)
|
||||
|
||||
# with use serial and batch fields
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=1,
|
||||
rate=120,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=True,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
si.reload()
|
||||
|
||||
self.assertEqual(si.items[0].use_serial_batch_fields, 1)
|
||||
|
||||
serial_and_batch_bundle = si.items[0].serial_and_batch_bundle
|
||||
change_in_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(change_in_value, 13.02 * -1)
|
||||
|
||||
doc.db_set("do_not_use_batchwise_valuation", original_value)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -4561,6 +4731,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
|
||||
)
|
||||
gl_entries = q.run(as_dict=True)
|
||||
|
||||
doc.assertGreater(len(gl_entries), 0)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
@@ -4646,6 +4818,7 @@ def create_sales_invoice(**args):
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -756,18 +756,14 @@ def get_prorata_factor(
|
||||
return diff / plan_days
|
||||
|
||||
|
||||
def process_all(subscription: str | None = None, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
"""
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
if subscription:
|
||||
filters["name"] = subscription
|
||||
|
||||
for subscription in frappe.get_all("Subscription", filters, pluck="name"):
|
||||
for subscription_name in subscription:
|
||||
try:
|
||||
subscription = frappe.get_doc("Subscription", subscription)
|
||||
subscription = frappe.get_doc("Subscription", subscription_name)
|
||||
subscription.process(posting_date)
|
||||
frappe.db.commit()
|
||||
except frappe.ValidationError:
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount"
|
||||
@@ -83,10 +83,11 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:52.531436",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -126,8 +127,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
@@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
@@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
pe.cancel()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
@@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
self.assertEqual(so2.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
@@ -463,8 +465,77 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.allocations = [x for x in unreconcile.allocations if x.reference_name == si.name]
|
||||
unreconcile.save().submit()
|
||||
|
||||
# after unreconcilaition advance paid will be reduced
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_unreconcile_advance_from_journal_entry(self):
|
||||
po = create_purchase_order(
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
item=self.item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
transaction_date=today(),
|
||||
do_not_submit=False,
|
||||
)
|
||||
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": po.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Creditors - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": po.supplier,
|
||||
"debit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": po.doctype,
|
||||
"reference_name": po.name,
|
||||
},
|
||||
{"account": "Cash - _TC", "credit_in_account_currency": 100},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": je.doctype,
|
||||
"voucher_no": je.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 1)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEqual([po.name], allocations)
|
||||
unreconcile.save().submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
|
||||
@@ -44,31 +44,12 @@ class UnreconcilePayment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_allocations_from_payment(self):
|
||||
allocated_references = []
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
allocated_references = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(
|
||||
(ple.docstatus == 1)
|
||||
& (ple.voucher_type == self.voucher_type)
|
||||
& (ple.voucher_no == self.voucher_no)
|
||||
& (ple.voucher_no != ple.against_voucher_no)
|
||||
)
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
return get_linked_payments_for_doc(
|
||||
company=self.company,
|
||||
doctype=self.voucher_type,
|
||||
docname=self.voucher_no,
|
||||
)
|
||||
|
||||
return allocated_references
|
||||
|
||||
def add_references(self):
|
||||
allocations = self.get_allocations_from_payment()
|
||||
|
||||
@@ -81,29 +62,43 @@ class UnreconcilePayment(Document):
|
||||
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
|
||||
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
|
||||
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
|
||||
|
||||
# update outstanding amounts
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
alloc.reference_doctype,
|
||||
alloc.reference_name,
|
||||
alloc.account,
|
||||
alloc.party_type,
|
||||
alloc.party,
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_payable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_receivable_doctypes"
|
||||
):
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def doc_has_references(doctype: str | None = None, docname: str | None = None):
|
||||
count = 0
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return frappe.db.count(
|
||||
count = frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
|
||||
)
|
||||
else:
|
||||
return frappe.db.count(
|
||||
count = frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
|
||||
)
|
||||
count += frappe.db.count(
|
||||
"Advance Payment Ledger Entry",
|
||||
filters={
|
||||
"delinked": 0,
|
||||
"voucher_no": docname,
|
||||
"voucher_type": doctype,
|
||||
"event": ["=", "Submit"],
|
||||
},
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -125,9 +120,12 @@ def get_linked_payments_for_doc(
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.company,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.voucher_type.as_("reference_doctype"),
|
||||
ple.voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
@@ -149,19 +147,52 @@ def get_linked_payments_for_doc(
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
)
|
||||
|
||||
res = query.run(as_dict=True)
|
||||
|
||||
res += get_linked_advances(company, _dn)
|
||||
|
||||
return res
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def get_linked_advances(company, docname):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
criteria = [
|
||||
(adv.company == company),
|
||||
(adv.delinked == 0),
|
||||
(adv.voucher_no == docname),
|
||||
(adv.event == "Submit"),
|
||||
]
|
||||
|
||||
return (
|
||||
qb.from_(adv)
|
||||
.select(
|
||||
adv.company,
|
||||
adv.against_voucher_type.as_("reference_doctype"),
|
||||
adv.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(adv.amount)).as_("allocated_amount"),
|
||||
adv.currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.groupby(adv.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user