mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-15 03:08:47 +00:00
Compare commits
515 Commits
develop
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464b7c8507 | ||
|
|
2c5bdefd13 | ||
|
|
a4ee85e89c | ||
|
|
92c2c7bf82 | ||
|
|
54bed64356 | ||
|
|
82170dfd07 | ||
|
|
595b5e7dae | ||
|
|
cfad7e17f1 | ||
|
|
00ebeec94e | ||
|
|
b56c444f18 | ||
|
|
9d2ccd7246 | ||
|
|
d06e611c72 | ||
|
|
3355c60c79 | ||
|
|
dbe2a87a84 | ||
|
|
f78317a79a | ||
|
|
0c4b9ea9ca | ||
|
|
715dbc0093 | ||
|
|
bccca6f58e | ||
|
|
72a0d82147 | ||
|
|
77da3306b5 | ||
|
|
636414c0e4 | ||
|
|
c5bc92d50e | ||
|
|
c669387b82 | ||
|
|
a499b7e046 | ||
|
|
17062e0154 | ||
|
|
2d864bb599 | ||
|
|
f6a1ea804a | ||
|
|
d7b61b945f | ||
|
|
b6a2284003 | ||
|
|
131e279a0c | ||
|
|
cc7de7e66e | ||
|
|
a5c83dd11e | ||
|
|
f5fa1ba02b | ||
|
|
81845992a6 | ||
|
|
893683a512 | ||
|
|
59e2dbb435 | ||
|
|
35e9ca64ce | ||
|
|
b15db05ef8 | ||
|
|
66d1b7c837 | ||
|
|
10be8f19e2 | ||
|
|
2ccb8c839d | ||
|
|
8649543ae0 | ||
|
|
bed2c83272 | ||
|
|
5461945d00 | ||
|
|
dd4e1867f5 | ||
|
|
6f812ccaf5 | ||
|
|
6c4bba3992 | ||
|
|
eda479a917 | ||
|
|
2200b9aa67 | ||
|
|
af15050acc | ||
|
|
531bdbc727 | ||
|
|
53e400cca1 | ||
|
|
6ae1b18616 | ||
|
|
a8f21dbc07 | ||
|
|
1dd471fb18 | ||
|
|
a039c176c8 | ||
|
|
cf141045ba | ||
|
|
62ea18f1cc | ||
|
|
302ae382f1 | ||
|
|
98ff54a871 | ||
|
|
41b089ab51 | ||
|
|
46acd328a9 | ||
|
|
1820c35880 | ||
|
|
80c98cdcf4 | ||
|
|
4a8363e7da | ||
|
|
dcbc1e1303 | ||
|
|
ecfd193002 | ||
|
|
f4b0e646b4 | ||
|
|
605c0db976 | ||
|
|
ca8f324b51 | ||
|
|
7677b2f573 | ||
|
|
f08964683a | ||
|
|
d2eabcbf74 | ||
|
|
7d8bbac5fd | ||
|
|
ac90975f43 | ||
|
|
28592d0180 | ||
|
|
de5e8a6e6b | ||
|
|
e46e8741b4 | ||
|
|
4a4e9956e2 | ||
|
|
8ec83f2080 | ||
|
|
46b5884420 | ||
|
|
3548073a07 | ||
|
|
e2183ebde9 | ||
|
|
37f740caa6 | ||
|
|
485c1b025a | ||
|
|
880cc50ae4 | ||
|
|
3c33a19634 | ||
|
|
38679d6d14 | ||
|
|
3744c32950 | ||
|
|
543e0131b5 | ||
|
|
eb5ffcdc88 | ||
|
|
f98e53692e | ||
|
|
e778eabcb0 | ||
|
|
1d444e53eb | ||
|
|
3bf9aff67e | ||
|
|
b844afe0ec | ||
|
|
0d831aad41 | ||
|
|
00caebe90c | ||
|
|
0ee97ffbbf | ||
|
|
4a6c428848 | ||
|
|
3e3daa50b1 | ||
|
|
ac7a25fff9 | ||
|
|
0d02bbb01a | ||
|
|
d01ea27f2f | ||
|
|
345d25bdf1 | ||
|
|
e8eaae4120 | ||
|
|
d081a26608 | ||
|
|
9b32c84462 | ||
|
|
22c9b26a24 | ||
|
|
1c3fe000ba | ||
|
|
ffd9b248f6 | ||
|
|
b8256e5f31 | ||
|
|
dffb6ac4cf | ||
|
|
e753df8ff0 | ||
|
|
560fd2e0d1 | ||
|
|
327719a0fd | ||
|
|
801a26ae67 | ||
|
|
8c4a9040b7 | ||
|
|
53e512ceaf | ||
|
|
e9212c6a32 | ||
|
|
29d33b3139 | ||
|
|
7e0dc2ff86 | ||
|
|
898d2e3c9a | ||
|
|
4c562b2903 | ||
|
|
04d6273713 | ||
|
|
b6afe7f4da | ||
|
|
cf7c127dc6 | ||
|
|
59f6012c57 | ||
|
|
e2c12043ae | ||
|
|
3e0a7f2400 | ||
|
|
abef9109b0 | ||
|
|
9766361c07 | ||
|
|
6bcd311214 | ||
|
|
dd45bb5664 | ||
|
|
23a26b540b | ||
|
|
9886b46cb4 | ||
|
|
32d5cedafc | ||
|
|
ece8d00415 | ||
|
|
c171b9a184 | ||
|
|
d2387a3af8 | ||
|
|
79e0e07446 | ||
|
|
4caca08b90 | ||
|
|
738cb6847e | ||
|
|
7a78e9705c | ||
|
|
a1ec68cd1e | ||
|
|
4847a76cb8 | ||
|
|
f56c6f93a1 | ||
|
|
c0c6cc58ed | ||
|
|
8c372faf50 | ||
|
|
ae4cf27f4f | ||
|
|
305483e074 | ||
|
|
86dd2e786c | ||
|
|
11222653ce | ||
|
|
d06a46ae85 | ||
|
|
8c536df5f2 | ||
|
|
4dec567421 | ||
|
|
1d24abf5dd | ||
|
|
62616ad9e1 | ||
|
|
6bffdbce56 | ||
|
|
6a9a28b4ae | ||
|
|
66d0ab6380 | ||
|
|
5913d5f14e | ||
|
|
4a7ffce320 | ||
|
|
2994ba1b41 | ||
|
|
55eb631116 | ||
|
|
77693b12a4 | ||
|
|
8caf609f8d | ||
|
|
921584c769 | ||
|
|
7df18af799 | ||
|
|
64d82a811f | ||
|
|
9d14c0b60e | ||
|
|
0a285523a8 | ||
|
|
ce695ebdd0 | ||
|
|
9f31910226 | ||
|
|
f8405e4ca4 | ||
|
|
96dfecf0d5 | ||
|
|
b5b8032ce0 | ||
|
|
5298e26a11 | ||
|
|
c5ff534d58 | ||
|
|
1dc44691db | ||
|
|
91043de352 | ||
|
|
b99ca486d7 | ||
|
|
b1b75eca3d | ||
|
|
33305550b7 | ||
|
|
e86ab97b7e | ||
|
|
6ff8820732 | ||
|
|
64a7956a4a | ||
|
|
1f78f45aee | ||
|
|
ab59f73064 | ||
|
|
4a7e2742ec | ||
|
|
55d5d6535b | ||
|
|
b8ac04fb54 | ||
|
|
45e4c04830 | ||
|
|
d80c8d14b0 | ||
|
|
66e47f5651 | ||
|
|
a3e8af19a6 | ||
|
|
9b498a8da8 | ||
|
|
25389be340 | ||
|
|
de244e0af7 | ||
|
|
b132e3f22a | ||
|
|
670fd79e38 | ||
|
|
e09406d085 | ||
|
|
3bc348d6f0 | ||
|
|
3f0032d793 | ||
|
|
163b848455 | ||
|
|
8ce51b2f80 | ||
|
|
422b37332e | ||
|
|
4c14e74a12 | ||
|
|
28e8c40bfc | ||
|
|
660fc8f76a | ||
|
|
22456a5857 | ||
|
|
76e0eb00a5 | ||
|
|
9dcaf38142 | ||
|
|
6048add4c0 | ||
|
|
0552b48328 | ||
|
|
193b29d5fc | ||
|
|
220a528d7f | ||
|
|
e087a8b179 | ||
|
|
d7067f6b7a | ||
|
|
e53ccd0745 | ||
|
|
e264d8e2d6 | ||
|
|
8b2559ab0c | ||
|
|
f425f89a26 | ||
|
|
e3bf84c572 | ||
|
|
e2b88218ec | ||
|
|
e755a4ad98 | ||
|
|
d2ea428030 | ||
|
|
496956f08f | ||
|
|
a3190dd556 | ||
|
|
95c4b8de06 | ||
|
|
4963261dc8 | ||
|
|
81e65757ee | ||
|
|
78e581154b | ||
|
|
2a3642b55a | ||
|
|
beaa76ca16 | ||
|
|
9997185071 | ||
|
|
681c0b5917 | ||
|
|
4a0e04ee20 | ||
|
|
55129e697d | ||
|
|
f9a8fc1f2d | ||
|
|
091ff81ae5 | ||
|
|
024e7b01ac | ||
|
|
a0156b61b8 | ||
|
|
7e5eab261c | ||
|
|
63782e6355 | ||
|
|
c80b554cd7 | ||
|
|
7db88b210e | ||
|
|
42d873f1d9 | ||
|
|
19c1dcc3dd | ||
|
|
23e027b6be | ||
|
|
d91cfa76e6 | ||
|
|
a29df7be67 | ||
|
|
dbe5846908 | ||
|
|
83fcb5d2d8 | ||
|
|
b8ab55fee8 | ||
|
|
4d03f4ebaa | ||
|
|
a2d302b3fa | ||
|
|
b5321d42a3 | ||
|
|
af167f91fe | ||
|
|
e74389f01c | ||
|
|
e7ace8e620 | ||
|
|
e23e9b5d66 | ||
|
|
136b2cfba5 | ||
|
|
3b3738577d | ||
|
|
1e646bd0ed | ||
|
|
02e6c49130 | ||
|
|
e9fa725030 | ||
|
|
b03494bb67 | ||
|
|
f7b915dfe6 | ||
|
|
0a5aac9ce7 | ||
|
|
e3c62070d1 | ||
|
|
b365444027 | ||
|
|
0d1c30f3f0 | ||
|
|
d0b553dca3 | ||
|
|
e4cdd971c8 | ||
|
|
78c4f01733 | ||
|
|
eac4978278 | ||
|
|
c619be989b | ||
|
|
64921fc1b5 | ||
|
|
6d8d502bbf | ||
|
|
5595602f24 | ||
|
|
7042f2b8fb | ||
|
|
f462639aa0 | ||
|
|
14ba0f1cae | ||
|
|
3944dfde31 | ||
|
|
16cc2b0a25 | ||
|
|
3394e1a126 | ||
|
|
66086010fc | ||
|
|
195a2f3e74 | ||
|
|
7e7b16b23e | ||
|
|
f4beb41df2 | ||
|
|
4c4aa9bbdf | ||
|
|
687a80d74c | ||
|
|
f01e0576b9 | ||
|
|
83a0d957ef | ||
|
|
554aeb94fd | ||
|
|
05e30dc011 | ||
|
|
40a3dabd30 | ||
|
|
0f9bf08685 | ||
|
|
b84fd46841 | ||
|
|
6209d633c2 | ||
|
|
095fe65bef | ||
|
|
b285548a46 | ||
|
|
0b7684eccd | ||
|
|
574460c009 | ||
|
|
27fe754a7d | ||
|
|
531fe59a24 | ||
|
|
0a56647a61 | ||
|
|
c2f666b7a3 | ||
|
|
c1bbe1104e | ||
|
|
2c86327c7e | ||
|
|
31385a1f91 | ||
|
|
4f1695616a | ||
|
|
9ed3801d06 | ||
|
|
61ad67ec29 | ||
|
|
735b9da6b1 | ||
|
|
fe7a797156 | ||
|
|
f951dd180a | ||
|
|
8e871796d4 | ||
|
|
c88ee50c34 | ||
|
|
e49f6f4f09 | ||
|
|
72942e6b8c | ||
|
|
fdcf037f1b | ||
|
|
9e0c606b95 | ||
|
|
6b9f2ddf83 | ||
|
|
c1cc1dbd27 | ||
|
|
de584e2e8d | ||
|
|
75758610dd | ||
|
|
1927adbd2e | ||
|
|
7342b2551b | ||
|
|
6115f8fb9a | ||
|
|
53b73757ed | ||
|
|
dee3357da2 | ||
|
|
b1b1f25bb1 | ||
|
|
46e6096fe3 | ||
|
|
f486071cf6 | ||
|
|
b541fbbd60 | ||
|
|
41e6687b35 | ||
|
|
55ce40de37 | ||
|
|
6f21ab5d9a | ||
|
|
7c81672b36 | ||
|
|
d0faff09cb | ||
|
|
cff09b71cc | ||
|
|
b180e3b78c | ||
|
|
6ba2795043 | ||
|
|
1b9a93f90e | ||
|
|
1f1428f00a | ||
|
|
606ac2a91a | ||
|
|
9a175757ac | ||
|
|
22a8d483e1 | ||
|
|
7abaaed957 | ||
|
|
8946f12677 | ||
|
|
e9f3f0f445 | ||
|
|
ba38bc3eaf | ||
|
|
b4572978f9 | ||
|
|
bf53133f94 | ||
|
|
23c902c317 | ||
|
|
13e4849c43 | ||
|
|
f97b850077 | ||
|
|
b3e12f9acb | ||
|
|
fb3fb8ca5e | ||
|
|
e2232340dc | ||
|
|
881562fc37 | ||
|
|
dcd6279d47 | ||
|
|
041a7c5a57 | ||
|
|
27ffef41a7 | ||
|
|
9038f19fb6 | ||
|
|
264855e5e1 | ||
|
|
7120fbd14b | ||
|
|
b1716bfeef | ||
|
|
37a237dbb7 | ||
|
|
be9112b6fc | ||
|
|
2f240f3553 | ||
|
|
c7c7a55a58 | ||
|
|
30e6b5daac | ||
|
|
0b5cc039b6 | ||
|
|
6fa60d2f1a | ||
|
|
91199ea9c9 | ||
|
|
e23ba0e852 | ||
|
|
7c2bbe0d82 | ||
|
|
0a2234a814 | ||
|
|
504c84e28a | ||
|
|
bb2bada1fd | ||
|
|
ca85ee33f5 | ||
|
|
21c1189e24 | ||
|
|
7e7885b304 | ||
|
|
30238e3063 | ||
|
|
35b3045b72 | ||
|
|
cf130ff865 | ||
|
|
176096bc5b | ||
|
|
e854eafc0b | ||
|
|
72cdddbeda | ||
|
|
c9d7c6cd42 | ||
|
|
8393c3c32d | ||
|
|
5752d2e0a1 | ||
|
|
f25558b4d7 | ||
|
|
fef6df709d | ||
|
|
3ac431bd50 | ||
|
|
5d5d208a49 | ||
|
|
9535f3d583 | ||
|
|
673635e2c3 | ||
|
|
d067e37ab6 | ||
|
|
37e241ba15 | ||
|
|
fb669eb6f4 | ||
|
|
232225d753 | ||
|
|
80cbd851d1 | ||
|
|
5474ac298d | ||
|
|
7a9b10a05e | ||
|
|
621243c1d3 | ||
|
|
efa5173964 | ||
|
|
7b3f74609a | ||
|
|
775f6d07b1 | ||
|
|
e80ed14456 | ||
|
|
fe59ace285 | ||
|
|
2131c7aadb | ||
|
|
b7284c7717 | ||
|
|
6b9107c05c | ||
|
|
1ed8857d31 | ||
|
|
a195690bc8 | ||
|
|
0c546c9e5a | ||
|
|
11d9fd3dee | ||
|
|
3bdaab149b | ||
|
|
ad4ac4e53c | ||
|
|
9cc9fa59be | ||
|
|
ab2aedd9a2 | ||
|
|
686911546f | ||
|
|
4f3078ab1a | ||
|
|
a950adab79 | ||
|
|
ae6b3af013 | ||
|
|
1da8ed202b | ||
|
|
a3d860eabf | ||
|
|
adc9dc82ca | ||
|
|
79e04ea1fe | ||
|
|
0981b894dd | ||
|
|
380564a677 | ||
|
|
75deb180fb | ||
|
|
839315752b | ||
|
|
2b4a547e23 | ||
|
|
3c533d04f5 | ||
|
|
1c214eec98 | ||
|
|
aeb2b60450 | ||
|
|
1b3f5e1c96 | ||
|
|
c830bf6fc7 | ||
|
|
e2b95da24d | ||
|
|
89b44c41a2 | ||
|
|
181141b56a | ||
|
|
5742a5d86a | ||
|
|
68c8dfb24c | ||
|
|
7f54de7926 | ||
|
|
cb2d4550af | ||
|
|
c22d7e16d1 | ||
|
|
9d5a0e56a0 | ||
|
|
fcea7603a8 | ||
|
|
42ebb7446a | ||
|
|
49760e4542 | ||
|
|
6e1f4d84b6 | ||
|
|
ab482caac9 | ||
|
|
d8506fb2c0 | ||
|
|
d3dfed909e | ||
|
|
ac31c5ca19 | ||
|
|
ccab91b9ed | ||
|
|
541a8b135a | ||
|
|
c0a30a5302 | ||
|
|
accce1fe59 | ||
|
|
f04221417e | ||
|
|
c4c2d35565 | ||
|
|
f1e41f4a4f | ||
|
|
d9326d80de | ||
|
|
5c93bf5798 | ||
|
|
f62ad83d6f | ||
|
|
876e2d4e6e | ||
|
|
4977e06c50 | ||
|
|
e64ae9a8a9 | ||
|
|
6ddf4eee15 | ||
|
|
d27a09cb9f | ||
|
|
86d5939d91 | ||
|
|
768c131073 | ||
|
|
8f77223057 | ||
|
|
8ba470160d | ||
|
|
1a6264d831 | ||
|
|
19a90c0980 | ||
|
|
d57fc49896 | ||
|
|
fe0431a6d0 | ||
|
|
bfd6375508 | ||
|
|
6dade11d8f | ||
|
|
ce421bb1d4 | ||
|
|
84a749e3d0 | ||
|
|
65a1c7086b | ||
|
|
a04da71182 | ||
|
|
cbfc13728b | ||
|
|
9b88275312 | ||
|
|
332673f260 | ||
|
|
e49add20b7 | ||
|
|
c3b0633eda | ||
|
|
a660ed061b | ||
|
|
0b1c0c36b5 | ||
|
|
d316ef2306 | ||
|
|
af3a7903b3 | ||
|
|
a66e114a71 | ||
|
|
631b9d3bb0 | ||
|
|
eb03781718 | ||
|
|
eb7cebac91 | ||
|
|
3420e21d45 | ||
|
|
211832104c | ||
|
|
4b85d51257 | ||
|
|
0363b01ab7 | ||
|
|
fc517f7fa2 | ||
|
|
18451b69e6 | ||
|
|
50ce61ae02 | ||
|
|
294fb27dc8 | ||
|
|
c3fdb191b9 | ||
|
|
93db2ebd6f | ||
|
|
2925f9a04e | ||
|
|
c7bf103c0c | ||
|
|
6dead8fd85 |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -60,7 +60,7 @@ body:
|
||||
description: Share exact version number of Frappe and ERPNext you are using.
|
||||
placeholder: |
|
||||
Frappe version -
|
||||
ERPNext Verion -
|
||||
ERPNext version -
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15"]
|
||||
version: ["14", "15", "16"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
5
.github/workflows/patch.yml
vendored
5
.github/workflows/patch.yml
vendored
@@ -113,8 +113,8 @@ jobs:
|
||||
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
|
||||
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
wget https://erpnext.com/files/v13-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
|
||||
wget https://frappe.io/files/erpnext-v14.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
@@ -142,7 +142,6 @@ jobs:
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 14 3.11
|
||||
update_to_version 15 3.13
|
||||
|
||||
echo "Updating to latest version"
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
- version-16
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.svg"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"branches": ["version-13"],
|
||||
"branches": ["version-16"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
@@ -21,4 +21,4 @@
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappe.io/erpnext">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0-dev"
|
||||
__version__ = "16.3.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
50
erpnext/accounts/accounts_dashboard/payments/payments.json
Normal file
50
erpnext/accounts/accounts_dashboard/payments/payments.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"cards": [
|
||||
{
|
||||
"card": "Total Outgoing Bills"
|
||||
},
|
||||
{
|
||||
"card": "Total Incoming Bills"
|
||||
},
|
||||
{
|
||||
"card": "Total Incoming Payment"
|
||||
},
|
||||
{
|
||||
"card": "Total Outgoing Payment"
|
||||
}
|
||||
],
|
||||
"charts": [
|
||||
{
|
||||
"chart": "Incoming Bills (Purchase Invoice)",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Outgoing Bills (Sales Invoice)",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Accounts Receivable Ageing",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Accounts Payable Ageing",
|
||||
"width": "Half"
|
||||
},
|
||||
{
|
||||
"chart": "Bank Balance",
|
||||
"width": "Full"
|
||||
}
|
||||
],
|
||||
"creation": "2026-01-26 21:25:12.793893",
|
||||
"dashboard_name": "Payments",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2026-01-26 21:25:12.793893",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payments",
|
||||
"owner": "Administrator"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,17 @@
|
||||
},
|
||||
"account_number": "1151.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.002",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1152.000"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"Kas": {
|
||||
@@ -97,17 +108,6 @@
|
||||
},
|
||||
"account_number": "1130.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1151.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"account_number": "1100.000"
|
||||
|
||||
},
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -640,7 +640,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
frappe.provide("erpnext.integrations");
|
||||
|
||||
frappe.ui.form.on("Bank", {
|
||||
onload: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
|
||||
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
|
||||
});
|
||||
});
|
||||
|
||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
||||
"bank_transaction_field",
|
||||
"options",
|
||||
options
|
||||
);
|
||||
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
|
||||
|
||||
if (grid) {
|
||||
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
|
||||
)
|
||||
);
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
|
||||
@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
is_company_account: function (frm) {
|
||||
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -98,6 +99,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -252,7 +254,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -51,25 +51,29 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_account()
|
||||
self.validate_is_company_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
def validate_is_company_account(self):
|
||||
if self.is_company_account:
|
||||
if not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
if not self.account:
|
||||
frappe.throw(_("Company Account is mandatory"))
|
||||
|
||||
self.validate_account()
|
||||
|
||||
def validate_account(self):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
|
||||
@@ -139,6 +139,8 @@ class BankTransaction(Document):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
self.delink_payment_entry(payment_entry)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, date_diff, flt, getdate
|
||||
from pypika.terms import LiteralValue
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -541,7 +541,7 @@ class FinancialQueryBuilder:
|
||||
.where(acb_table.period_closing_voucher == closing_voucher)
|
||||
)
|
||||
|
||||
query = self._apply_standard_filters(query, acb_table)
|
||||
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
|
||||
results = self._execute_with_permissions(query, "Account Closing Balance")
|
||||
|
||||
for row in results:
|
||||
@@ -636,12 +636,15 @@ class FinancialQueryBuilder:
|
||||
return self._execute_with_permissions(query, "GL Entry")
|
||||
|
||||
def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
|
||||
for row in gl_data:
|
||||
account = row["account"]
|
||||
gl_dict = {row["account"]: row for row in gl_data}
|
||||
accounts = set(balances_data.keys()) | set(gl_dict.keys())
|
||||
|
||||
for account in accounts:
|
||||
if account not in balances_data:
|
||||
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
||||
|
||||
account_data: AccountData = balances_data[account]
|
||||
gl_movement = gl_dict.get(account, {})
|
||||
|
||||
if account_data.has_periods():
|
||||
first_period = account_data.get_period(self.periods[0]["key"])
|
||||
@@ -651,20 +654,13 @@ class FinancialQueryBuilder:
|
||||
|
||||
for period in self.periods:
|
||||
period_key = period["key"]
|
||||
movement = row.get(period_key, 0.0)
|
||||
movement = gl_movement.get(period_key, 0.0)
|
||||
closing_balance = current_balance + movement
|
||||
|
||||
account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
|
||||
|
||||
current_balance = closing_balance
|
||||
|
||||
# Accounts with no movements
|
||||
for account_data in balances_data.values():
|
||||
for period in self.periods:
|
||||
period_key = period["key"]
|
||||
if period_key not in account_data.period_values:
|
||||
account_data.add_period(PeriodValue(period_key, 0.0, 0.0, 0.0))
|
||||
|
||||
def _handle_balance_accumulation(self, balances_data):
|
||||
for account_data in balances_data.values():
|
||||
account_data: AccountData
|
||||
@@ -683,12 +679,12 @@ class FinancialQueryBuilder:
|
||||
else:
|
||||
account_data.unaccumulate_values()
|
||||
|
||||
def _apply_standard_filters(self, query, table):
|
||||
def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
|
||||
if self.filters.get("ignore_closing_entries"):
|
||||
if hasattr(table, "is_period_closing_voucher_entry"):
|
||||
query = query.where(table.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
if doctype == "GL Entry":
|
||||
query = query.where(table.voucher_type != "Period Closing Voucher")
|
||||
else:
|
||||
query = query.where(table.is_period_closing_voucher_entry == 0)
|
||||
|
||||
if self.filters.get("project"):
|
||||
projects = self.filters.get("project")
|
||||
@@ -736,7 +732,7 @@ class FinancialQueryBuilder:
|
||||
user_conditions = build_match_conditions(doctype)
|
||||
|
||||
if user_conditions:
|
||||
query = query.where(LiteralValue(user_conditions))
|
||||
query = query.where(Bracket(LiteralValue(user_conditions)))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
|
||||
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
|
||||
FinancialReportTemplateTestCase,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
@@ -1672,3 +1673,360 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
|
||||
mock_row_invalid = self._create_mock_report_row(invalid_formula)
|
||||
condition = parser.build_condition(mock_row_invalid, account_table)
|
||||
self.assertIsNone(condition)
|
||||
|
||||
|
||||
class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
||||
def test_fetch_balances_with_journal_entries(self):
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
bank_account = "_Test Bank - _TC"
|
||||
|
||||
# Create journal entries in different periods
|
||||
# October: Transfer 1000 from Bank to Cash
|
||||
jv_oct = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=bank_account,
|
||||
amount=1000,
|
||||
posting_date="2024-10-15",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
# November: Transfer 500 from Bank to Cash
|
||||
jv_nov = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=bank_account,
|
||||
amount=500,
|
||||
posting_date="2024-11-20",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
# December: No transactions (test zero movement period)
|
||||
|
||||
try:
|
||||
# Set up filters and periods for Q4 2024
|
||||
filters = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-10-01",
|
||||
"period_end_date": "2024-12-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Monthly",
|
||||
}
|
||||
|
||||
periods = [
|
||||
{"key": "2024_oct", "from_date": "2024-10-01", "to_date": "2024-10-31"},
|
||||
{"key": "2024_nov", "from_date": "2024-11-01", "to_date": "2024-11-30"},
|
||||
{"key": "2024_dec", "from_date": "2024-12-01", "to_date": "2024-12-31"},
|
||||
]
|
||||
|
||||
query_builder = FinancialQueryBuilder(filters, periods)
|
||||
|
||||
# Create account objects as expected by fetch_account_balances
|
||||
accounts = [
|
||||
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
|
||||
]
|
||||
|
||||
# Fetch balances using the full workflow
|
||||
balances_data = query_builder.fetch_account_balances(accounts)
|
||||
|
||||
# Verify Cash account balances
|
||||
cash_data = balances_data.get(cash_account)
|
||||
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||
|
||||
# October: movement = +1000 (debit)
|
||||
oct_cash = cash_data.get_period("2024_oct")
|
||||
self.assertIsNotNone(oct_cash, "October period should exist for cash")
|
||||
self.assertEqual(oct_cash.movement, 1000.0, "October cash movement should be 1000")
|
||||
|
||||
# November: movement = +500
|
||||
nov_cash = cash_data.get_period("2024_nov")
|
||||
self.assertIsNotNone(nov_cash, "November period should exist for cash")
|
||||
self.assertEqual(nov_cash.movement, 500.0, "November cash movement should be 500")
|
||||
self.assertEqual(
|
||||
nov_cash.opening, oct_cash.closing, "November opening should equal October closing"
|
||||
)
|
||||
|
||||
# December: movement = 0 (no transactions)
|
||||
dec_cash = cash_data.get_period("2024_dec")
|
||||
self.assertIsNotNone(dec_cash, "December period should exist for cash")
|
||||
self.assertEqual(dec_cash.movement, 0.0, "December cash movement should be 0")
|
||||
self.assertEqual(
|
||||
dec_cash.closing,
|
||||
nov_cash.closing,
|
||||
"December closing should equal November closing when no movement",
|
||||
)
|
||||
|
||||
# Verify Bank account balances (opposite direction)
|
||||
bank_data = balances_data.get(bank_account)
|
||||
self.assertIsNotNone(bank_data, "Bank account should exist in results")
|
||||
|
||||
oct_bank = bank_data.get_period("2024_oct")
|
||||
self.assertEqual(oct_bank.movement, -1000.0, "October bank movement should be -1000")
|
||||
|
||||
nov_bank = bank_data.get_period("2024_nov")
|
||||
self.assertEqual(nov_bank.movement, -500.0, "November bank movement should be -500")
|
||||
|
||||
finally:
|
||||
# Clean up: cancel journal entries
|
||||
jv_nov.cancel()
|
||||
jv_oct.cancel()
|
||||
|
||||
def test_opening_balance_from_previous_period_closing(self):
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
sales_account = "Sales - _TC"
|
||||
posting_date_2023 = "2023-06-15"
|
||||
|
||||
# Create journal entry in prior period (2023)
|
||||
# Cash Dr 5000, Sales Cr 5000
|
||||
jv_2023 = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=sales_account,
|
||||
amount=5000,
|
||||
posting_date=posting_date_2023,
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
pcv = None
|
||||
jv_2024 = None
|
||||
original_pcv_setting = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv"
|
||||
)
|
||||
|
||||
try:
|
||||
# Create Period Closing Voucher for 2023
|
||||
# This will create Account Closing Balance entries
|
||||
closing_account = frappe.db.get_value(
|
||||
"Account",
|
||||
{
|
||||
"company": company,
|
||||
"root_type": "Liability",
|
||||
"is_group": 0,
|
||||
"account_type": ["not in", ["Payable", "Receivable"]],
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
fy_2023 = get_fiscal_year(posting_date_2023, company=company)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": "2023-12-31",
|
||||
"period_start_date": fy_2023[1],
|
||||
"period_end_date": fy_2023[2],
|
||||
"company": company,
|
||||
"fiscal_year": fy_2023[0],
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"closing_account_head": closing_account,
|
||||
"remarks": "Test Period Closing",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
pcv.submit()
|
||||
pcv.reload()
|
||||
|
||||
# Now create a small transaction in 2024 to ensure the account appears
|
||||
jv_2024 = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=sales_account,
|
||||
amount=100,
|
||||
posting_date="2024-01-15",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
# Set up filters for Q1 2024 (after the period closing)
|
||||
filters = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2024-03-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Monthly",
|
||||
"ignore_closing_entries": True, # Don't include PCV entries in movements
|
||||
}
|
||||
|
||||
periods = [
|
||||
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
|
||||
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
|
||||
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
|
||||
]
|
||||
|
||||
query_builder = FinancialQueryBuilder(filters, periods)
|
||||
|
||||
accounts = [
|
||||
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||
]
|
||||
|
||||
balances_data = query_builder.fetch_account_balances(accounts)
|
||||
|
||||
# Verify Cash account has opening balance from 2023 transactions
|
||||
cash_data = balances_data.get(cash_account)
|
||||
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||
|
||||
jan_cash = cash_data.get_period("2024_jan")
|
||||
self.assertIsNotNone(jan_cash, "January period should exist")
|
||||
|
||||
# Opening balance should be from prior period
|
||||
# Cash had 5000 debit in 2023, so opening in 2024 should be >= 5000
|
||||
# (may be higher if there were other test transactions)
|
||||
self.assertEqual(
|
||||
jan_cash.opening,
|
||||
5000.0,
|
||||
"January opening should equal to balance from 2023 (5000)",
|
||||
)
|
||||
|
||||
# Verify running balance logic
|
||||
# Movement in January is 100 (from jv_2024)
|
||||
self.assertEqual(jan_cash.movement, 100.0, "January movement should be 100")
|
||||
self.assertEqual(
|
||||
jan_cash.closing, jan_cash.opening + jan_cash.movement, "Closing = Opening + Movement"
|
||||
)
|
||||
|
||||
# February and March should have no movement but carry the balance
|
||||
feb_cash = cash_data.get_period("2024_feb")
|
||||
self.assertEqual(feb_cash.opening, jan_cash.closing, "Feb opening = Jan closing")
|
||||
self.assertEqual(feb_cash.movement, 0.0, "February should have no movement")
|
||||
self.assertEqual(feb_cash.closing, feb_cash.opening, "Feb closing = opening when no movement")
|
||||
|
||||
mar_cash = cash_data.get_period("2024_mar")
|
||||
self.assertEqual(mar_cash.opening, feb_cash.closing, "Mar opening = Feb closing")
|
||||
self.assertEqual(mar_cash.movement, 0.0, "March should have no movement")
|
||||
self.assertEqual(mar_cash.closing, mar_cash.opening, "Mar closing = opening when no movement")
|
||||
|
||||
# Set up filters for Q2 2024
|
||||
filters_q2 = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-04-01",
|
||||
"period_end_date": "2024-06-30",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Monthly",
|
||||
"ignore_closing_entries": True,
|
||||
}
|
||||
|
||||
periods_q2 = [
|
||||
{"key": "2024_apr", "from_date": "2024-04-01", "to_date": "2024-04-30"},
|
||||
{"key": "2024_may", "from_date": "2024-05-01", "to_date": "2024-05-31"},
|
||||
{"key": "2024_jun", "from_date": "2024-06-01", "to_date": "2024-06-30"},
|
||||
]
|
||||
|
||||
query_builder_q2 = FinancialQueryBuilder(filters_q2, periods_q2)
|
||||
|
||||
balances_data_q2 = query_builder_q2.fetch_account_balances(accounts)
|
||||
|
||||
# Verify Cash account in Q2
|
||||
cash_data_q2 = balances_data_q2.get(cash_account)
|
||||
self.assertIsNotNone(cash_data_q2, "Cash account should exist in Q2 results")
|
||||
|
||||
apr_cash = cash_data_q2.get_period("2024_apr")
|
||||
self.assertIsNotNone(apr_cash, "April period should exist")
|
||||
|
||||
# Opening balance in April should equal closing in March
|
||||
self.assertEqual(
|
||||
apr_cash.opening,
|
||||
mar_cash.closing,
|
||||
"April opening should equal March closing balance",
|
||||
)
|
||||
|
||||
self.assertEqual(apr_cash.closing, apr_cash.opening, "April closing = opening when no movement")
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
|
||||
)
|
||||
|
||||
if jv_2024:
|
||||
jv_2024.cancel()
|
||||
|
||||
if pcv:
|
||||
pcv.reload()
|
||||
if pcv.docstatus == 1:
|
||||
pcv.cancel()
|
||||
|
||||
jv_2023.cancel()
|
||||
|
||||
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
bank_account = "_Test Bank - _TC"
|
||||
|
||||
# Create journal entries WITHOUT any prior Period Closing Voucher
|
||||
# This ensures the account exists in gl_dict but NOT in balances_data
|
||||
jv = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=bank_account,
|
||||
amount=2500,
|
||||
posting_date="2024-07-15",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Set up filters - use a period with no prior PCV
|
||||
filters = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-07-01",
|
||||
"period_end_date": "2024-09-30",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Monthly",
|
||||
}
|
||||
|
||||
periods = [
|
||||
{"key": "2024_jul", "from_date": "2024-07-01", "to_date": "2024-07-31"},
|
||||
{"key": "2024_aug", "from_date": "2024-08-01", "to_date": "2024-08-31"},
|
||||
{"key": "2024_sep", "from_date": "2024-09-01", "to_date": "2024-09-30"},
|
||||
]
|
||||
|
||||
query_builder = FinancialQueryBuilder(filters, periods)
|
||||
|
||||
# Use accounts that have GL entries but may not have Account Closing Balance
|
||||
accounts = [
|
||||
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
|
||||
]
|
||||
|
||||
balances_data = query_builder.fetch_account_balances(accounts)
|
||||
|
||||
# Verify accounts are present in results even without prior closing balance
|
||||
cash_data = balances_data.get(cash_account)
|
||||
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||
|
||||
bank_data = balances_data.get(bank_account)
|
||||
self.assertIsNotNone(bank_data, "Bank account should exist in results")
|
||||
|
||||
# Verify July has the movement from journal entry
|
||||
jul_cash = cash_data.get_period("2024_jul")
|
||||
self.assertIsNotNone(jul_cash, "July period should exist for cash")
|
||||
self.assertEqual(jul_cash.movement, 2500.0, "July cash movement should be 2500")
|
||||
|
||||
jul_bank = bank_data.get_period("2024_jul")
|
||||
self.assertIsNotNone(jul_bank, "July period should exist for bank")
|
||||
self.assertEqual(jul_bank.movement, -2500.0, "July bank movement should be -2500")
|
||||
|
||||
# Verify subsequent periods exist with zero movement
|
||||
aug_cash = cash_data.get_period("2024_aug")
|
||||
self.assertIsNotNone(aug_cash, "August period should exist for cash")
|
||||
self.assertEqual(aug_cash.movement, 0.0, "August cash movement should be 0")
|
||||
self.assertEqual(aug_cash.opening, jul_cash.closing, "August opening = July closing")
|
||||
|
||||
sep_cash = cash_data.get_period("2024_sep")
|
||||
self.assertIsNotNone(sep_cash, "September period should exist for cash")
|
||||
self.assertEqual(sep_cash.movement, 0.0, "September cash movement should be 0")
|
||||
self.assertEqual(sep_cash.opening, aug_cash.closing, "September opening = August closing")
|
||||
|
||||
finally:
|
||||
jv.cancel()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe import _, cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
@@ -33,24 +33,6 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
if not self.is_new():
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""",
|
||||
(self.name),
|
||||
)
|
||||
|
||||
if year_start_end_dates:
|
||||
if (
|
||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
@@ -66,28 +48,20 @@ class FiscalYear(Document):
|
||||
frappe.exceptions.InvalidDates,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(year_start_date)s between year_start_date and year_end_date)
|
||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
||||
) and name!=%(name)s""",
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
name = self.name or self.year
|
||||
|
||||
existing_fiscal_years = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where(
|
||||
(fy.year_start_date <= self.year_end_date)
|
||||
& (fy.year_end_date >= self.year_start_date)
|
||||
& (fy.name != name)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
@@ -110,37 +84,30 @@ class FiscalYear(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(existing.name),
|
||||
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_duplicate_fiscal_year(doc):
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
||||
(doc.name),
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_create_fiscal_year():
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
# Skipped auto-creating Short Year, as it has very rare use case.
|
||||
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
|
||||
follow_up_date = add_days(getdate(), days=3)
|
||||
fiscal_year = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
for d in fiscal_year:
|
||||
try:
|
||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||
|
||||
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
|
||||
new_fy = frappe.new_doc("Fiscal Year")
|
||||
new_fy.disabled = cint(current_fy.disabled)
|
||||
|
||||
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
||||
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
||||
@@ -148,6 +115,10 @@ def auto_create_fiscal_year():
|
||||
start_year = cstr(new_fy.year_start_date.year)
|
||||
end_year = cstr(new_fy.year_end_date.year)
|
||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||
|
||||
for row in current_fy.companies:
|
||||
new_fy.append("companies", {"company": row.company})
|
||||
|
||||
new_fy.auto_created = 1
|
||||
|
||||
new_fy.insert(ignore_permissions=True)
|
||||
|
||||
@@ -15,20 +15,22 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:44.659251",
|
||||
"modified": "2026-02-20 23:02:26.193606",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
company: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -277,7 +277,21 @@ frappe.ui.form.on("Journal Entry", {
|
||||
var update_jv_details = function (doc, r) {
|
||||
$.each(r, function (i, d) {
|
||||
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
|
||||
frappe.model.set_value(row.doctype, row.name, "account", d.account);
|
||||
const {
|
||||
idx,
|
||||
name,
|
||||
owner,
|
||||
parent,
|
||||
parenttype,
|
||||
parentfield,
|
||||
creation,
|
||||
modified,
|
||||
modified_by,
|
||||
doctype,
|
||||
docstatus,
|
||||
...fields
|
||||
} = d;
|
||||
frappe.model.set_value(row.doctype, row.name, fields);
|
||||
});
|
||||
refresh_field("accounts");
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"company",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
@@ -17,7 +18,6 @@
|
||||
"reversal_of",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"apply_tds",
|
||||
@@ -638,7 +638,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2026-02-03 14:40:39.944524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -179,11 +179,14 @@ class JournalEntry(AccountsController):
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||
queue_submission(self, "_submit")
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
@@ -554,12 +557,27 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
@@ -1673,6 +1691,10 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
# Ensure exchange_rate is always numeric to avoid calculation errors
|
||||
if isinstance(exchange_rate, str):
|
||||
exchange_rate = flt(exchange_rate) or 1
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -198,7 +198,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
|
||||
"fieldname": "reference_due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
@@ -294,7 +294,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
|
||||
"Fees",
|
||||
"Full and Final Statement",
|
||||
"Payment Entry",
|
||||
"Bank Transaction",
|
||||
]
|
||||
user_remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
onload: function (frm) {
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
if (frm.is_new()) {
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
@@ -37,6 +38,31 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("party_type", "accounts", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
|
||||
return {
|
||||
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
|
||||
filters: {
|
||||
account: row.account,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
voucher_type: function (frm) {
|
||||
var add_accounts = function (doc, r) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -42,7 +43,29 @@ class JournalEntryTemplate(Document):
|
||||
]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def validate(self):
|
||||
self.validate_party()
|
||||
|
||||
def validate_party(self):
|
||||
"""
|
||||
Loop over all accounts and see if party and party type is set correctly
|
||||
"""
|
||||
for account in self.accounts:
|
||||
if account.party_type:
|
||||
account_type = frappe.get_cached_value("Account", account.account, "account_type")
|
||||
if account_type not in ["Receivable", "Payable"]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Check row {0} for account {1}: Party Type is only allowed for Receivable or Payable accounts"
|
||||
).format(account.idx, account.account)
|
||||
)
|
||||
|
||||
if account.party and not account.party_type:
|
||||
frappe.throw(
|
||||
_("Check row {0} for account {1}: Party is only allowed if Party Type is set").format(
|
||||
account.idx, account.account
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account"
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -15,18 +21,55 @@
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party",
|
||||
"options": "party_type"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:58.986448",
|
||||
"modified": "2026-01-09 13:16:27.615083",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Template Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,13 @@ class JournalEntryTemplateAccount(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
account: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
frm.refresh_fields();
|
||||
|
||||
const party_currency =
|
||||
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
|
||||
|
||||
var reference_grid = frm.fields_dict["references"].grid;
|
||||
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
|
||||
reference_grid.update_docfield_property(fieldname, "options", party_currency);
|
||||
});
|
||||
|
||||
reference_grid.refresh();
|
||||
},
|
||||
|
||||
show_general_ledger: function (frm) {
|
||||
@@ -1104,7 +1114,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount: flt(paid_amount),
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
|
||||
@@ -132,6 +132,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
|
||||
@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
|
||||
amount_in_account_currency: DF.Currency
|
||||
company: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
project: DF.Link | None
|
||||
delinked: DF.Check
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
|
||||
@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
|
||||
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
|
||||
@@ -535,7 +535,7 @@ class PaymentRequest(Document):
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
@@ -546,6 +546,9 @@ def make_payment_request(**args):
|
||||
if args.dn and not isinstance(args.dn, str):
|
||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||
|
||||
frappe.has_permission("Payment Request", "create", throw=True)
|
||||
frappe.has_permission(args.dt, "read", args.dn, throw=True)
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -819,7 +822,7 @@ def get_print_format_list(ref_doctype):
|
||||
return {"print_format": print_format_list}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def resend_payment_email(docname):
|
||||
return frappe.get_doc("Payment Request", docname).send_email()
|
||||
|
||||
|
||||
@@ -1610,13 +1610,14 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified": "2026-01-29 21:20:51.376875",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -898,6 +898,53 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 5)
|
||||
|
||||
def test_pos_batch_reservation_with_return_qty(self):
|
||||
"""
|
||||
Test POS Invoice reserved qty for batch without bundle with return invoices.
|
||||
"""
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_batch_item_with_batch,
|
||||
)
|
||||
|
||||
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="_Batch Item Reserve Return",
|
||||
qty=30,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
# POS Invoice for the batch without bundle
|
||||
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 4500},
|
||||
)
|
||||
pos_inv.items[0].batch_no = batch_no
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
# POS Invoice return
|
||||
pos_return = make_sales_return(pos_inv.name)
|
||||
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
|
||||
for batch in batches:
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 30)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
BatchNegativeStockError,
|
||||
|
||||
@@ -32,8 +32,15 @@
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
<<<<<<< HEAD
|
||||
"set_grand_total_to_default_mop",
|
||||
"allow_partial_payment",
|
||||
=======
|
||||
"column_break_egpi",
|
||||
"allow_warehouse_change",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
>>>>>>> 1c5e36017c (feat(pos): allow warehouse change configuration (#52437))
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -430,6 +437,110 @@
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Partial Payment"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tvls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xygw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "campaign_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Campaign"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_configurations_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS Configurations"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list_and_currency_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Price List & Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bptt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "write_off_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Write Off"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ukpz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pkca",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "income_and_expense_account",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Income and Expense"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_byzk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_cjpp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_item_selector_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Item Selector"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rpny",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_stcl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_item_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Item Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hwfg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_egpi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_warehouse_change",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow User to Edit Warehouse"
|
||||
>>>>>>> 1c5e36017c (feat(pos): allow warehouse change configuration (#52437))
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -458,7 +569,11 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2025-06-24 11:19:19.834905",
|
||||
=======
|
||||
"modified": "2026-02-05 04:36:40.757162",
|
||||
>>>>>>> 1c5e36017c (feat(pos): allow warehouse change configuration (#52437))
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -34,6 +34,7 @@ class POSProfile(Document):
|
||||
allow_discount_change: DF.Check
|
||||
allow_partial_payment: DF.Check
|
||||
allow_rate_change: DF.Check
|
||||
allow_warehouse_change: DF.Check
|
||||
applicable_for_users: DF.Table[POSProfileUser]
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
auto_add_item_to_cart: DF.Check
|
||||
|
||||
@@ -99,8 +99,7 @@ def get_customers_list(pos_profile=None):
|
||||
|
||||
return (
|
||||
frappe.db.sql(
|
||||
f""" select name, customer_name, customer_group,
|
||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
||||
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
|
||||
and {cond}""",
|
||||
tuple(customer_groups),
|
||||
as_dict=1,
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Apply On",
|
||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
||||
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -657,7 +657,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified": "2026-02-17 12:24:07.553505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -714,9 +714,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
apply_discount_on_rate: DF.Check
|
||||
apply_multiple_pricing_rules: DF.Check
|
||||
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_recursion_over: DF.Float
|
||||
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||
brands: DF.Table[PricingRuleBrand]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -28,14 +28,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:17.857046",
|
||||
"modified": "2026-02-17 12:17:13.073587",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Brand",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -28,14 +28,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:18.221095",
|
||||
"modified": "2026-02-17 12:16:57.778471",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Item Group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||
# reconcile
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
|
||||
|
||||
# If Payment Entry, update details only for newly linked references
|
||||
# This is for performance
|
||||
@@ -504,6 +505,37 @@ def reconcile(doc: None | str = None) -> None:
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
def check_multi_currency(pr_doc):
|
||||
GL = frappe.qb.DocType("GL Entry")
|
||||
Account = frappe.qb.DocType("Account")
|
||||
|
||||
def get_account_currency(voucher_type, voucher_no):
|
||||
currency = (
|
||||
frappe.qb.from_(GL)
|
||||
.join(Account)
|
||||
.on(GL.account == Account.name)
|
||||
.select(Account.account_currency)
|
||||
.where(
|
||||
(GL.voucher_type == voucher_type)
|
||||
& (GL.voucher_no == voucher_no)
|
||||
& (Account.account_type.isin(["Payable", "Receivable"]))
|
||||
)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
return currency[0].account_currency if currency else None
|
||||
|
||||
for allocation in pr_doc.allocation:
|
||||
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
|
||||
|
||||
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
|
||||
|
||||
if reference_currency != invoice_currency:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"column_break_6",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -606,6 +606,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
@@ -1625,7 +1626,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -1667,7 +1669,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:41:38.237728",
|
||||
"modified": "2026-02-05 20:45:16.964500",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
update_billed_amount_based_on_po,
|
||||
@@ -1745,10 +1745,6 @@ class PurchaseInvoice(BuyingController):
|
||||
project_doc.db_update()
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
if getdate(self.bill_date) > getdate(self.posting_date):
|
||||
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
|
||||
|
||||
if self.bill_no:
|
||||
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
|
||||
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
|
||||
@@ -2005,9 +2001,17 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
for row in target_parent.get("items"):
|
||||
if row.get("qty") == 0:
|
||||
target_parent.remove(row)
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
def remove_items_with_zero_qty(target_parent):
|
||||
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||
|
||||
def set_missing_values(source_parent, target_parent):
|
||||
target_parent.run_method("set_missing_values")
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source_parent, target_parent)
|
||||
target_parent.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||
@@ -2059,7 +2063,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"reset_value": not (args and args.get("merge_taxes")),
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_parent_process,
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"allow_zero_valuation_rate",
|
||||
"section_break_22",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -97,7 +98,6 @@
|
||||
"service_start_date",
|
||||
"service_end_date",
|
||||
"reference",
|
||||
"allow_zero_valuation_rate",
|
||||
"item_tax_rate",
|
||||
"bom",
|
||||
"include_exploded_items",
|
||||
@@ -420,6 +420,7 @@
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "warehouse_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Warehouse"
|
||||
@@ -447,7 +448,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
@@ -459,14 +459,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "rejected_serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Rejected Serial No",
|
||||
@@ -577,6 +575,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
@@ -800,7 +799,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
|
||||
"depends_on": "eval:parent.is_internal_supplier",
|
||||
"fieldname": "from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@@ -896,7 +895,7 @@
|
||||
"label": "Consider for Tax Withholding"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -906,7 +905,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "rejected_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@@ -922,7 +921,7 @@
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
@@ -992,7 +991,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-13 14:10:02.379392",
|
||||
"modified": "2026-02-15 21:07:49.455930",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
"Packing Slip",
|
||||
];
|
||||
|
||||
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
@@ -115,18 +116,21 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
if (cint(doc.update_stock) != 1) {
|
||||
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
|
||||
var from_delivery_note = false;
|
||||
from_delivery_note = this.frm.doc.items.some(function (item) {
|
||||
return item.delivery_note ? true : false;
|
||||
});
|
||||
|
||||
if (!from_delivery_note && !is_delivered_by_supplier) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery"),
|
||||
this.frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
if (!is_delivered_by_supplier) {
|
||||
const should_create_delivery_note = doc.items.some(
|
||||
(item) =>
|
||||
item.qty - item.delivered_qty > 0 &&
|
||||
!item.scio_detail &&
|
||||
!item.dn_detail &&
|
||||
!item.delivered_by_supplier
|
||||
);
|
||||
if (should_create_delivery_note) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
this.frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"column_break1",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -703,6 +703,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -778,8 +779,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
||||
"depends_on": "eval:!doc.is_return",
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
|
||||
"fieldname": "time_sheet_list",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
@@ -793,7 +793,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"no_copy": 1,
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -2092,7 +2091,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
|
||||
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
|
||||
"fieldname": "section_break_104",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@@ -2252,7 +2251,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -2306,7 +2306,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-10-09 14:48:59.472826",
|
||||
"modified": "2026-02-06 20:43:44.732805",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import split_asset
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
@@ -352,10 +353,22 @@ class SalesInvoice(SellingController):
|
||||
self.is_opening = "No"
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
|
||||
if self.is_return and not self.return_against and self.timesheets:
|
||||
frappe.throw(_("Direct return is not allowed for Timesheet."))
|
||||
|
||||
if not self.is_return:
|
||||
self.validate_time_sheets_are_submitted()
|
||||
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||
if self.is_return:
|
||||
self.timesheets = []
|
||||
|
||||
if self.is_return and self.return_against:
|
||||
for row in self.timesheets:
|
||||
if row.billing_hours:
|
||||
row.billing_hours = -abs(row.billing_hours)
|
||||
if row.billing_amount:
|
||||
row.billing_amount = -abs(row.billing_amount)
|
||||
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
self.update_timesheet_billing_for_project()
|
||||
@@ -468,6 +481,8 @@ class SalesInvoice(SellingController):
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.split_asset_based_on_sale_qty()
|
||||
|
||||
self.process_asset_depreciation()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -484,7 +499,7 @@ class SalesInvoice(SellingController):
|
||||
if cint(self.is_pos) != 1 and not self.is_return:
|
||||
self.update_against_document_in_jv()
|
||||
|
||||
self.update_time_sheet(self.name)
|
||||
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
|
||||
|
||||
if frappe.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -564,7 +579,7 @@ class SalesInvoice(SellingController):
|
||||
self.check_if_consolidated_invoice()
|
||||
|
||||
super().before_cancel()
|
||||
self.update_time_sheet(None)
|
||||
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
|
||||
|
||||
def on_cancel(self):
|
||||
check_if_return_invoice_linked_with_payment_entry(self)
|
||||
@@ -804,8 +819,20 @@ class SalesInvoice(SellingController):
|
||||
for data in timesheet.time_logs:
|
||||
if (
|
||||
(self.project and args.timesheet_detail == data.name)
|
||||
or (not self.project and not data.sales_invoice)
|
||||
or (not sales_invoice and data.sales_invoice == self.name)
|
||||
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
|
||||
or (
|
||||
not sales_invoice
|
||||
and data.sales_invoice == self.name
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
or (
|
||||
self.is_return
|
||||
and self.return_against
|
||||
and data.sales_invoice
|
||||
and data.sales_invoice == self.return_against
|
||||
and not sales_invoice
|
||||
and args.timesheet_detail == data.name
|
||||
)
|
||||
):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
@@ -845,11 +872,26 @@ class SalesInvoice(SellingController):
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||
|
||||
def validate_time_sheets_are_submitted(self):
|
||||
# Note: This validation is skipped for return invoices
|
||||
# to allow returns to reference already-billed timesheet details
|
||||
for data in self.timesheets:
|
||||
# Handle invoice duplication
|
||||
if data.time_sheet and data.timesheet_detail:
|
||||
if sales_invoice := frappe.db.get_value(
|
||||
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
|
||||
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
|
||||
)
|
||||
)
|
||||
|
||||
if data.time_sheet:
|
||||
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
|
||||
if status not in ["Submitted", "Payslip"]:
|
||||
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
|
||||
if status not in ["Submitted", "Payslip", "Partially Billed"]:
|
||||
frappe.throw(
|
||||
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
|
||||
)
|
||||
|
||||
def set_pos_fields(self, for_validate=False):
|
||||
"""Set retail related fields from POS Profiles"""
|
||||
@@ -1283,7 +1325,12 @@ class SalesInvoice(SellingController):
|
||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||
|
||||
def update_timesheet_billing_for_project(self):
|
||||
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
|
||||
if (
|
||||
not self.is_return
|
||||
and not self.timesheets
|
||||
and self.project
|
||||
and self.is_auto_fetch_timesheet_enabled()
|
||||
):
|
||||
self.add_timesheet_data()
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
@@ -1358,6 +1405,51 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||
|
||||
def split_asset_based_on_sale_qty(self):
|
||||
asset_qty_map = self.get_asset_qty()
|
||||
for asset, qty in asset_qty_map.items():
|
||||
if qty["actual_qty"] < qty["sale_qty"]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
|
||||
).format(asset, qty["actual_qty"])
|
||||
)
|
||||
|
||||
remaining_qty = qty["actual_qty"] - qty["sale_qty"]
|
||||
if remaining_qty > 0:
|
||||
split_asset(asset, remaining_qty)
|
||||
|
||||
def get_asset_qty(self):
|
||||
asset_qty_map = {}
|
||||
|
||||
assets = {row.asset for row in self.items if row.is_fixed_asset and row.asset}
|
||||
if not assets or self.is_return:
|
||||
return asset_qty_map
|
||||
|
||||
asset_actual_qty = dict(
|
||||
frappe.db.get_all(
|
||||
"Asset",
|
||||
{"name": ["in", list(assets)]},
|
||||
["name", "asset_quantity"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
for row in self.items:
|
||||
if row.is_fixed_asset and row.asset:
|
||||
actual_qty = asset_actual_qty.get(row.asset)
|
||||
if row.asset in asset_qty_map.keys():
|
||||
asset_qty_map[row.asset]["sale_qty"] += flt(row.qty)
|
||||
else:
|
||||
asset_qty_map.setdefault(
|
||||
row.asset,
|
||||
{
|
||||
"sale_qty": flt(row.qty),
|
||||
"actual_qty": flt(actual_qty),
|
||||
},
|
||||
)
|
||||
|
||||
return asset_qty_map
|
||||
|
||||
def process_asset_depreciation(self):
|
||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||
self.depreciate_asset_on_sale()
|
||||
@@ -2378,7 +2470,10 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"cost_center": "cost_center",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1
|
||||
and not doc.scio_detail
|
||||
and not doc.dn_detail
|
||||
and doc.qty - doc.delivered_qty > 0,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
|
||||
@@ -2951,6 +2951,60 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
def test_item_tax_template_change_with_grand_total_discount(self):
|
||||
"""
|
||||
Test that when item tax template changes due to discount on Grand Total,
|
||||
the tax calculations are consistent.
|
||||
"""
|
||||
item = create_item("Test Item With Multiple Tax Templates")
|
||||
|
||||
item.set("taxes", [])
|
||||
item.append(
|
||||
"taxes",
|
||||
{
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
"minimum_net_rate": 0,
|
||||
"maximum_net_rate": 500,
|
||||
},
|
||||
)
|
||||
|
||||
item.append(
|
||||
"taxes",
|
||||
{
|
||||
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
|
||||
"minimum_net_rate": 501,
|
||||
"maximum_net_rate": 1000,
|
||||
},
|
||||
)
|
||||
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"rate": 0,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.discount_amount = 300
|
||||
si.save()
|
||||
|
||||
# Verify template changed to 10%
|
||||
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
|
||||
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
|
||||
|
||||
si.submit()
|
||||
|
||||
@IntegrationTestCase.change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
discount_account = create_account(
|
||||
@@ -4691,6 +4745,66 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
doc.db_set("do_not_use_batchwise_valuation", original_value)
|
||||
|
||||
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
|
||||
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
|
||||
item_code = "_Test Item for Expiry Batch Zero Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"has_expiry_date": 1,
|
||||
"shelf_life_in_days": 2,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBATCH-EBZV.####",
|
||||
},
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
# fetch batch no from bundle
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
si = create_sales_invoice(
|
||||
posting_date=add_days(nowdate(), 3),
|
||||
item=item_code,
|
||||
qty=-10,
|
||||
rate=100,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=1,
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
si.items[0].batch_no = batch_no
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
si.reload()
|
||||
# check zero incoming rate in voucher
|
||||
self.assertEqual(si.items[0].incoming_rate, 0.0)
|
||||
|
||||
# chekc zero incoming rate in stock ledger
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"grant_commission",
|
||||
"allow_zero_valuation_rate",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -88,7 +89,6 @@
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
@@ -580,6 +580,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "warehouse_and_reference",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stock Details"
|
||||
@@ -595,7 +596,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
|
||||
"depends_on": "eval: parent.is_internal_customer",
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
@@ -613,7 +614,6 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
@@ -626,6 +626,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
@@ -633,7 +634,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
@@ -906,7 +906,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -916,7 +916,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
@@ -1009,7 +1009,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-04 11:08:25.583561",
|
||||
"modified": "2026-02-15 21:08:57.341638",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@@ -85,14 +85,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.427565",
|
||||
"modified": "2026-02-16 20:46:34.592604",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Timesheet Detail",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -117,15 +116,16 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.562795",
|
||||
"modified": "2025-12-23 13:54:17.677187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +415,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
@@ -506,7 +505,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 200,
|
||||
"description": "Test Gross Tax",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
@@ -541,10 +539,10 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 400,
|
||||
"description": "Test Gross Tax",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
si.reload()
|
||||
si.submit()
|
||||
invoices.append(si)
|
||||
# For amount before threshold (first 8000 + VAT): TCS entry with amount zero
|
||||
@@ -594,7 +592,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "VAT added to test TDS calculation on gross amount",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
@@ -1024,7 +1021,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 1000,
|
||||
"description": "VAT added to test TDS calculation on gross amount",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
@@ -1162,7 +1158,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -708,6 +708,10 @@ class TaxWithholdingController:
|
||||
existing_taxes = {row.account_head: row for row in self.doc.taxes if row.is_tax_withholding_account}
|
||||
precision = self.doc.precision("tax_amount", "taxes")
|
||||
conversion_rate = self.get_conversion_rate()
|
||||
add_deduct_tax = "Deduct"
|
||||
|
||||
if self.party_type == "Customer":
|
||||
add_deduct_tax = "Add"
|
||||
|
||||
for account_head, base_amount in account_amount_map.items():
|
||||
tax_amount = flt(base_amount / conversion_rate, precision)
|
||||
@@ -724,6 +728,7 @@ class TaxWithholdingController:
|
||||
tax_row = self._create_tax_row(account_head, tax_amount)
|
||||
for_update = False
|
||||
|
||||
tax_row.add_deduct_tax = add_deduct_tax
|
||||
# Set item-wise tax breakup for this tax row
|
||||
self._set_item_wise_tax_for_tds(
|
||||
tax_row, account_head, category_withholding_map, for_update=for_update
|
||||
@@ -743,7 +748,6 @@ class TaxWithholdingController:
|
||||
"account_head": account_head,
|
||||
"description": account_head,
|
||||
"cost_center": cost_center,
|
||||
"add_deduct_tax": "Deduct",
|
||||
"tax_amount": tax_amount,
|
||||
"dont_recompute_tax": 1,
|
||||
},
|
||||
@@ -807,12 +811,14 @@ class TaxWithholdingController:
|
||||
else:
|
||||
item_tax_amount = 0
|
||||
|
||||
multiplier = -1 if tax_row.add_deduct_tax == "Deduct" else 1
|
||||
|
||||
self.doc._item_wise_tax_details.append(
|
||||
frappe._dict(
|
||||
item=item,
|
||||
tax=tax_row,
|
||||
rate=category.tax_rate,
|
||||
amount=item_tax_amount * -1, # Negative because it's a deduction
|
||||
amount=item_tax_amount * multiplier,
|
||||
taxable_amount=item_base_taxable,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -43,16 +43,18 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:55.008837",
|
||||
"modified": "2025-11-14 16:17:25.584675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Transaction Deletion Record Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
<h3>{{ _("Fiscal Year") }}</h3>
|
||||
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
|
||||
|
||||
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p>
|
||||
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
|
||||
|
||||
<p>{{ _("Fiscal Year Details") }}</p>
|
||||
|
||||
<table style="margin-bottom: 1rem; width: 70%">
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
|
||||
<td>{{ doc.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
|
||||
</tr>
|
||||
{% if doc.companies|length > 0 %}
|
||||
<tr>
|
||||
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
|
||||
{% if doc.companies|length < 2 %}
|
||||
{{ _("Company") }}
|
||||
{% else %}
|
||||
{{ _("Companies") }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ doc.companies[0].company }}</td>
|
||||
</tr>
|
||||
{% for idx in range(1, doc.companies|length) %}
|
||||
<tr>
|
||||
<td>{{ doc.companies[idx].company }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if doc.disabled %}
|
||||
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.auto_created",
|
||||
"condition": "doc.auto_created == 1",
|
||||
"condition_type": "Python",
|
||||
"creation": "2018-04-25 14:19:05.440361",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
@@ -11,8 +12,10 @@
|
||||
"event": "New",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
|
||||
"message_type": "HTML",
|
||||
"modified": "2023-11-17 08:54:51.532104",
|
||||
"minutes_offset": 0,
|
||||
"modified": "2026-02-21 12:14:54.736795",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Notification for new fiscal year",
|
||||
@@ -27,5 +30,5 @@
|
||||
],
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "Notification for new fiscal year {{ doc.name }}"
|
||||
"subject": "{{ _(\"New Fiscal Year {0} - Review Required\").format(doc.name) }}"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<table>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -877,11 +877,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
entry_date = row.posting_date
|
||||
|
||||
row.range0 = 0.0
|
||||
|
||||
self.get_ageing_data(entry_date, row)
|
||||
|
||||
# ageing buckets should not have amounts if due date is not reached
|
||||
if getdate(entry_date) > getdate(self.age_as_on):
|
||||
row.range0 = row.outstanding
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
row.total_due = 0
|
||||
return
|
||||
|
||||
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
|
||||
|
||||
@@ -1281,6 +1285,8 @@ class ReceivablePayableReport:
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
self.add_column(label=_("<0"), fieldname="range0", fieldtype="Currency")
|
||||
self.ageing_column_labels.append(_("<0"))
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
label = f"{prev_range_value}-{curr_range_value}"
|
||||
self.add_column(label=label, fieldname="range" + str(idx + 1))
|
||||
@@ -1296,7 +1302,9 @@ class ReceivablePayableReport:
|
||||
for row in self.data:
|
||||
row = frappe._dict(row)
|
||||
if not cint(row.bold):
|
||||
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
|
||||
values = [flt(row.get("range0", 0), precision)] + [
|
||||
flt(row.get(f"range{i}", 0), precision) for i in self.range_numbers
|
||||
]
|
||||
rows.append({"values": values})
|
||||
|
||||
self.chart = {
|
||||
|
||||
@@ -232,11 +232,11 @@ def get_report_summary(
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
|
||||
asset_data, liability_data, equity_data = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for p in columns[4:]:
|
||||
if asset:
|
||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||
if liability:
|
||||
|
||||
@@ -18,6 +18,8 @@ def execute(filters=None):
|
||||
dimensions = filters.get("budget_against_filter")
|
||||
else:
|
||||
dimensions = get_budget_dimensions(filters)
|
||||
if not dimensions:
|
||||
return columns, [], None, None
|
||||
|
||||
budget_records = get_budget_records(filters, dimensions)
|
||||
budget_map = build_budget_map(budget_records, filters)
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from pypika.terms import LiteralValue
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions(party_type)
|
||||
|
||||
if match_conditions:
|
||||
query = query.where(LiteralValue(match_conditions))
|
||||
if match_conditions := build_match_conditions(party_type):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
party_details = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
from pypika.terms import Bracket, ExistsCriterion, LiteralValue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -564,18 +564,15 @@ def get_accounting_entries(
|
||||
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
if group_by_account:
|
||||
query = query.groupby("account")
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
if group_by_account:
|
||||
query += " GROUP BY `account`"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
|
||||
@@ -324,10 +324,8 @@ def get_conditions(filters):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions("GL Entry")
|
||||
|
||||
if match_conditions:
|
||||
conditions.append(match_conditions)
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
conditions.append(f"({match_conditions})")
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
|
||||
@@ -219,13 +219,18 @@ def get_net_profit(
|
||||
|
||||
has_value = False
|
||||
|
||||
gross_income_roots = [row for row in (gross_income or []) if not flt(row.get("indent"))]
|
||||
non_gross_income_roots = [row for row in (non_gross_income or []) if not flt(row.get("indent"))]
|
||||
gross_expense_roots = [row for row in (gross_expense or []) if not flt(row.get("indent"))]
|
||||
non_gross_expense_roots = [row for row in (non_gross_expense or []) if not flt(row.get("indent"))]
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
|
||||
non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
|
||||
|
||||
gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
|
||||
non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
|
||||
gross_income_for_period = sum(flt(row.get(key, 0)) for row in gross_income_roots)
|
||||
non_gross_income_for_period = sum(flt(row.get(key, 0)) for row in non_gross_income_roots)
|
||||
gross_expense_for_period = sum(flt(row.get(key, 0)) for row in gross_expense_roots)
|
||||
non_gross_expense_for_period = sum(flt(row.get(key, 0)) for row in non_gross_expense_roots)
|
||||
|
||||
total_income = gross_income_for_period + non_gross_income_for_period
|
||||
total_expense = gross_expense_for_period + non_gross_expense_for_period
|
||||
|
||||
@@ -5,15 +5,16 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder import Case, Order
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cint, flt, formatdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
@@ -176,7 +177,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
column_names = get_column_names()
|
||||
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
columns[0]["fieldname"] = "sales_invoice"
|
||||
columns[0]["options"] = "Item"
|
||||
columns[0]["width"] = 300
|
||||
# removing Item Code and Item Name columns
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
total_gross_profit = flt(
|
||||
total_base_amount + abs(total_buying_amount)
|
||||
if total_buying_amount < 0
|
||||
else total_base_amount - total_buying_amount,
|
||||
)
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_%": flt(
|
||||
(total_gross_profit / total_base_amount) * 100.0,
|
||||
(total_gross_profit / abs(total_base_amount)) * 100.0,
|
||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||
)
|
||||
if total_base_amount
|
||||
@@ -248,9 +255,13 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
total_gross_profit = flt(
|
||||
total_base_amount + abs(total_buying_amount)
|
||||
if total_buying_amount < 0
|
||||
else total_base_amount - total_buying_amount,
|
||||
)
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
gross_profit_percent = (total_gross_profit / abs(total_base_amount) * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
row.gross_profit = flt(
|
||||
row.base_amount + abs(row.buying_amount)
|
||||
if row.buying_amount < 0
|
||||
else row.base_amount - row.buying_amount,
|
||||
self.currency_precision,
|
||||
)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
(row.gross_profit / abs(row.base_amount)) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
@@ -673,9 +689,14 @@ class GrossProfitGenerator:
|
||||
return new_row
|
||||
|
||||
def set_average_gross_profit(self, new_row):
|
||||
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
|
||||
new_row.gross_profit = flt(
|
||||
new_row.base_amount + abs(new_row.buying_amount)
|
||||
if new_row.buying_amount < 0
|
||||
else new_row.base_amount - new_row.buying_amount,
|
||||
self.currency_precision,
|
||||
)
|
||||
new_row.gross_profit_percent = (
|
||||
flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision)
|
||||
flt(((new_row.gross_profit / abs(new_row.base_amount)) * 100.0), self.currency_precision)
|
||||
if new_row.base_amount
|
||||
else 0
|
||||
)
|
||||
@@ -851,129 +872,173 @@ class GrossProfitGenerator:
|
||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||
|
||||
def load_invoice_items(self):
|
||||
conditions = ""
|
||||
if self.filters.company:
|
||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
||||
if self.filters.from_date:
|
||||
conditions += " and posting_date >= %(from_date)s"
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
self.si_list = []
|
||||
|
||||
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||
base_query = self.prepare_invoice_query()
|
||||
|
||||
if self.filters.include_returned_invoices:
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
invoice_query = base_query.where(
|
||||
(SalesInvoice.is_return == 0)
|
||||
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
|
||||
)
|
||||
else:
|
||||
conditions += " and is_return = 0"
|
||||
invoice_query = base_query.where(SalesInvoice.is_return == 0)
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
|
||||
self.si_list += invoice_query.run(as_dict=True)
|
||||
self.prepare_vouchers_to_ignore()
|
||||
|
||||
if self.filters.sales_person:
|
||||
conditions += """
|
||||
and exists(select 1
|
||||
from `tabSales Team` st
|
||||
where st.parent = `tabSales Invoice`.name
|
||||
and st.sales_person = %(sales_person)s)
|
||||
"""
|
||||
ret_invoice_query = base_query.where(
|
||||
(SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
|
||||
)
|
||||
if self.vouchers_to_ignore:
|
||||
ret_invoice_query = ret_invoice_query.where(
|
||||
SalesInvoice.return_against.notin(self.vouchers_to_ignore)
|
||||
)
|
||||
|
||||
self.si_list += ret_invoice_query.run(as_dict=True)
|
||||
|
||||
def prepare_invoice_query(self):
|
||||
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
SalesTeam = frappe.qb.DocType("Sales Team")
|
||||
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.join(SalesInvoiceItem)
|
||||
.on(SalesInvoiceItem.parent == SalesInvoice.name)
|
||||
.join(Item)
|
||||
.on(Item.name == SalesInvoiceItem.item_code)
|
||||
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
|
||||
)
|
||||
|
||||
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item)
|
||||
|
||||
query = query.select(
|
||||
SalesInvoiceItem.parenttype,
|
||||
SalesInvoiceItem.parent,
|
||||
SalesInvoice.posting_date,
|
||||
SalesInvoice.posting_time,
|
||||
SalesInvoice.project,
|
||||
SalesInvoice.update_stock,
|
||||
SalesInvoice.customer,
|
||||
SalesInvoice.customer_group,
|
||||
SalesInvoice.customer_name,
|
||||
SalesInvoice.territory,
|
||||
SalesInvoiceItem.item_code,
|
||||
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
|
||||
SalesInvoiceItem.item_name,
|
||||
SalesInvoiceItem.description,
|
||||
SalesInvoiceItem.warehouse,
|
||||
SalesInvoiceItem.item_group,
|
||||
SalesInvoiceItem.brand,
|
||||
SalesInvoiceItem.so_detail,
|
||||
SalesInvoiceItem.sales_order,
|
||||
SalesInvoiceItem.dn_detail,
|
||||
SalesInvoiceItem.delivery_note,
|
||||
SalesInvoiceItem.stock_qty.as_("qty"),
|
||||
SalesInvoiceItem.base_net_rate,
|
||||
SalesInvoiceItem.base_net_amount,
|
||||
SalesInvoiceItem.name.as_("item_row"),
|
||||
SalesInvoice.is_return,
|
||||
SalesInvoiceItem.cost_center,
|
||||
SalesInvoiceItem.serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = """, sales.sales_person,
|
||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
||||
sales.incentives
|
||||
"""
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
else:
|
||||
sales_person_cols = ""
|
||||
sales_team_table = ""
|
||||
query = query.select(
|
||||
SalesTeam.sales_person,
|
||||
(SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
|
||||
"allocated_amount"
|
||||
),
|
||||
SalesTeam.incentives,
|
||||
)
|
||||
|
||||
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
|
||||
|
||||
if self.filters.group_by == "Payment Term":
|
||||
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||
'{}',
|
||||
coalesce(schedule.payment_term, '{}')) as payment_term,
|
||||
schedule.invoice_portion,
|
||||
schedule.payment_amount """.format(_("Sales Return"), _("No Terms"))
|
||||
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||
`tabSales Invoice`.is_return = 0 """
|
||||
else:
|
||||
payment_term_cols = ""
|
||||
payment_term_table = ""
|
||||
query = query.select(
|
||||
Case()
|
||||
.when(SalesInvoice.is_return == 1, _("Sales Return"))
|
||||
.else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
|
||||
.as_("payment_term"),
|
||||
PaymentSchedule.invoice_portion,
|
||||
PaymentSchedule.payment_amount,
|
||||
)
|
||||
|
||||
if self.filters.get("sales_invoice"):
|
||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||
query = query.left_join(PaymentSchedule).on(
|
||||
(PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
|
||||
)
|
||||
|
||||
if self.filters.get("item_code"):
|
||||
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
|
||||
query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
|
||||
SalesInvoice.posting_time, order=Order.desc
|
||||
)
|
||||
|
||||
if self.filters.get("cost_center"):
|
||||
return query
|
||||
|
||||
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item):
|
||||
if self.filters.company:
|
||||
query = query.where(SalesInvoice.company == self.filters.company)
|
||||
|
||||
if self.filters.from_date:
|
||||
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
|
||||
|
||||
if self.filters.to_date:
|
||||
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
|
||||
|
||||
if self.filters.item_group:
|
||||
query = query.where(get_item_group_condition(self.filters.item_group, Item))
|
||||
|
||||
if self.filters.sales_person:
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(SalesTeam)
|
||||
.select(1)
|
||||
.where(
|
||||
(SalesTeam.parent == SalesInvoice.name)
|
||||
& (SalesTeam.sales_person == self.filters.sales_person)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.sales_invoice:
|
||||
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
|
||||
|
||||
if self.filters.item_code:
|
||||
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
|
||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s"
|
||||
query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
|
||||
|
||||
if self.filters.get("project"):
|
||||
if self.filters.project:
|
||||
self.filters.project = frappe.parse_json(self.filters.get("project"))
|
||||
conditions += " and `tabSales Invoice Item`.project in %(project)s"
|
||||
query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if self.filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
else:
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
for dim in get_accounting_dimensions(as_list=False) or []:
|
||||
if self.filters.get(dim.fieldname):
|
||||
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
|
||||
self.filters[dim.fieldname] = get_dimension_with_children(
|
||||
dim.document_type, self.filters.get(dim.fieldname)
|
||||
)
|
||||
query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
if self.filters.warehouse:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
SalesInvoiceItem.warehouse.isin(
|
||||
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
|
||||
)
|
||||
)
|
||||
if warehouse_details:
|
||||
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
|
||||
|
||||
self.si_list = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
|
||||
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
|
||||
{sales_person_cols}
|
||||
{payment_term_cols}
|
||||
from
|
||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
|
||||
{sales_team_table}
|
||||
{payment_term_table}
|
||||
where
|
||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||
order by
|
||||
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format(
|
||||
conditions=conditions,
|
||||
sales_person_cols=sales_person_cols,
|
||||
sales_team_table=sales_team_table,
|
||||
payment_term_cols=payment_term_cols,
|
||||
payment_term_table=payment_term_table,
|
||||
match_cond=get_match_cond("Sales Invoice"),
|
||||
),
|
||||
self.filters,
|
||||
as_dict=1,
|
||||
)
|
||||
return query
|
||||
|
||||
def prepare_vouchers_to_ignore(self):
|
||||
self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
|
||||
|
||||
def get_delivery_notes(self):
|
||||
self.delivery_notes = frappe._dict({})
|
||||
|
||||
@@ -470,7 +470,7 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit_%": -100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
|
||||
@@ -649,21 +649,24 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
sales_inv_date = month_start_date
|
||||
return_inv_date = add_days(month_end_date, 1)
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.posting_date = return_inv_date
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
||||
company=self.company, from_date=month_start_date, to_date=month_start_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
@@ -675,7 +678,7 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
filters.update({"to_date": return_inv_date})
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
@@ -684,3 +687,63 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
# apply filters only on returned period
|
||||
filters.update({"from_date": return_inv_date, "to_date": return_inv_date})
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, -100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||
|
||||
def test_sales_person_wise_gross_profit(self):
|
||||
sales_person = make_sales_person("_Test Sales Person")
|
||||
|
||||
posting_date = get_first_day(nowdate())
|
||||
qty = 10
|
||||
rate = 100
|
||||
|
||||
sinv = self.create_sales_invoice(qty=qty, rate=rate, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = posting_date
|
||||
sinv.append(
|
||||
"sales_team",
|
||||
{
|
||||
"sales_person": sales_person.name,
|
||||
"allocated_percentage": 100,
|
||||
"allocated_amount": 1000.0,
|
||||
"commission_rate": 5,
|
||||
"incentives": 5,
|
||||
},
|
||||
)
|
||||
sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=posting_date, to_date=posting_date, group_by="Sales Person"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total[5], 1000.0)
|
||||
self.assertEqual(total[6], 0.0)
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
|
||||
def make_sales_person(sales_person_name="_Test Sales Person"):
|
||||
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
|
||||
sales_person_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Person",
|
||||
"is_group": 0,
|
||||
"parent_sales_person": "Sales Team",
|
||||
"sales_person_name": sales_person_name,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
else:
|
||||
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
|
||||
|
||||
return sales_person_doc
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
@@ -361,15 +362,12 @@ def get_items(filters, additional_table_columns):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
query = apply_order_by_conditions(doctype, query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
@@ -105,7 +106,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
{
|
||||
"total_tax": total_tax,
|
||||
"total_other_charges": total_other_charges,
|
||||
"total": d.base_net_amount + total_tax,
|
||||
"total": d.base_net_amount + total_tax + total_other_charges,
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
@@ -390,20 +391,21 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
||||
|
||||
|
||||
def apply_order_by_conditions(doctype, query, filters):
|
||||
invoice = f"`tab{doctype}`"
|
||||
invoice_item = f"`tab{doctype} Item`"
|
||||
invoice = frappe.qb.DocType(doctype)
|
||||
invoice_item = frappe.qb.DocType(f"{doctype} Item")
|
||||
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
|
||||
query = query.orderby(invoice.posting_date, order=Order.desc)
|
||||
query = query.orderby(invoice_item.item_group, order=Order.desc)
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {invoice_item}.parent desc"
|
||||
query = query.orderby(invoice_item.parent, order=Order.desc)
|
||||
elif filters.get("group_by") == "Item":
|
||||
query += f" order by {invoice_item}.item_code"
|
||||
query = query.orderby(invoice_item.item_code)
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query += f" order by {invoice_item}.item_group"
|
||||
query = query.orderby(invoice_item.item_group)
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
query = query.orderby(filter_field, order=Order.desc)
|
||||
|
||||
return query
|
||||
|
||||
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
query = apply_order_by_conditions(doctype, query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
@@ -163,11 +163,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for p in columns[4:]:
|
||||
if income:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
if expense:
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -33,11 +34,19 @@ def execute(filters=None):
|
||||
|
||||
def get_accounts_data(based_on, company):
|
||||
if based_on == "Cost Center":
|
||||
return frappe.db.sql(
|
||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
||||
from `tabCost Center` where company=%s order by name""",
|
||||
company,
|
||||
as_dict=True,
|
||||
cc = qb.DocType("Cost Center")
|
||||
return (
|
||||
qb.from_(cc)
|
||||
.select(
|
||||
cc.name,
|
||||
cc.parent_cost_center.as_("parent_account"),
|
||||
cc.cost_center_name.as_("account_name"),
|
||||
cc.lft,
|
||||
cc.rgt,
|
||||
)
|
||||
.where(cc.company.eq(company))
|
||||
.orderby(cc.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
elif based_on == "Project":
|
||||
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
||||
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
|
||||
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
additional_conditions = []
|
||||
gl = qb.DocType("GL Entry")
|
||||
acc = qb.DocType("Account")
|
||||
|
||||
conditions = []
|
||||
conditions.append(gl.company.eq(company))
|
||||
conditions.append(gl[based_on].notnull())
|
||||
conditions.append(gl.is_cancelled.eq(0))
|
||||
|
||||
if from_date and to_date:
|
||||
conditions.append(gl.posting_date.between(from_date, to_date))
|
||||
elif from_date and not to_date:
|
||||
conditions.append(gl.posting_date.gte(from_date))
|
||||
elif not from_date and to_date:
|
||||
conditions.append(gl.posting_date.lte(to_date))
|
||||
|
||||
if ignore_closing_entries:
|
||||
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
|
||||
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append("and posting_date >= %(from_date)s")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select posting_date, {based_on} as based_on, debit, credit,
|
||||
is_opening, (select root_type from `tabAccount` where name = account) as type
|
||||
from `tabGL Entry` where company=%(company)s
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
{"company": company, "from_date": from_date, "to_date": to_date},
|
||||
as_dict=True,
|
||||
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.posting_date,
|
||||
gl[based_on].as_("based_on"),
|
||||
gl.debit,
|
||||
gl.credit,
|
||||
gl.is_opening,
|
||||
root_subquery.as_("type"),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(gl[based_on], gl.posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika import Order
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.utils import (
|
||||
@@ -422,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
if match_conditions := build_match_conditions("Purchase Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _, msgprint
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika import Order
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.utils import (
|
||||
@@ -458,15 +458,13 @@ def get_invoices(filters, additional_query_columns):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
if match_conditions := build_match_conditions("Sales Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -154,17 +154,11 @@ def get_columns(filters):
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
@@ -172,10 +166,16 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"label": _("Grand Total (Company Currency)"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Transaction Currency)"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
"label": _("Reference Date"),
|
||||
|
||||
@@ -106,7 +106,7 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
|
||||
@@ -11,6 +11,7 @@ import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import determine_consecutive_week_number
|
||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table
|
||||
from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
@@ -25,6 +26,7 @@ from frappe.utils import (
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
now_datetime,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils.caching import site_cache
|
||||
@@ -66,6 +68,7 @@ def get_fiscal_year(
|
||||
as_dict=False,
|
||||
boolean=None,
|
||||
raise_on_missing=True,
|
||||
truncate=False,
|
||||
):
|
||||
if isinstance(raise_on_missing, str):
|
||||
raise_on_missing = loads(raise_on_missing)
|
||||
@@ -79,7 +82,14 @@ def get_fiscal_year(
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing
|
||||
)
|
||||
return False if not fiscal_years else fiscal_years[0]
|
||||
|
||||
if fiscal_years:
|
||||
fiscal_year = fiscal_years[0]
|
||||
if truncate:
|
||||
return ("-".join(y[-2:] for y in fiscal_year[0].split("-")), fiscal_year[1], fiscal_year[2])
|
||||
return fiscal_year
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_fiscal_years(
|
||||
@@ -547,6 +557,7 @@ def reconcile_against_document(
|
||||
doc.make_advance_gl_entries(entry=row)
|
||||
else:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||
@@ -1500,14 +1511,14 @@ def get_autoname_with_number(number_value, doc_title, company):
|
||||
|
||||
|
||||
def parse_naming_series_variable(doc, variable):
|
||||
if variable == "FY":
|
||||
if variable in ["FY", "TFY"]:
|
||||
if doc:
|
||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||
company = doc.get("company")
|
||||
else:
|
||||
date = getdate()
|
||||
company = None
|
||||
return get_fiscal_year(date=date, company=company)[0]
|
||||
return get_fiscal_year(date=date, company=company, truncate=variable == "TFY")[0]
|
||||
|
||||
elif variable == "ABBR":
|
||||
if doc:
|
||||
@@ -1517,6 +1528,18 @@ def parse_naming_series_variable(doc, variable):
|
||||
|
||||
return frappe.db.get_value("Company", company, "abbr") if company else ""
|
||||
|
||||
else:
|
||||
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
|
||||
date = (
|
||||
(
|
||||
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
|
||||
or now_datetime()
|
||||
)
|
||||
if frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents")
|
||||
else now_datetime()
|
||||
)
|
||||
return date.strftime(data[variable]) if variable in data else determine_consecutive_week_number(date)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_coa(doctype, parent, is_root=None, chart=None):
|
||||
@@ -1946,6 +1969,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
account=gle.account,
|
||||
party_type=gle.party_type,
|
||||
party=gle.party,
|
||||
project=gle.project,
|
||||
cost_center=gle.cost_center,
|
||||
finance_book=gle.finance_book,
|
||||
due_date=gle.due_date,
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "accounting",
|
||||
"idx": 3,
|
||||
"idx": 4,
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
"label": "Accounting",
|
||||
"label": "Invoicing",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
@@ -587,10 +587,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-24 13:20:34.857205",
|
||||
"modified": "2026-01-23 11:05:47.246213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
"name": "Invoicing",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Outgoing Bills",
|
||||
@@ -617,6 +617,6 @@
|
||||
"roles": [],
|
||||
"sequence_id": 2.0,
|
||||
"shortcuts": [],
|
||||
"title": "Accounting",
|
||||
"title": "Invoicing",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -111,15 +111,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.add_custom_button(
|
||||
__("Sell Asset"),
|
||||
function () {
|
||||
frm.trigger("make_sales_invoice");
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Repair Asset"),
|
||||
function () {
|
||||
frm.trigger("create_asset_repair");
|
||||
frm.trigger("sell_asset");
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
@@ -155,6 +147,14 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Repair Asset"),
|
||||
function () {
|
||||
frm.trigger("create_asset_repair");
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
}
|
||||
|
||||
if (!frm.doc.calculate_depreciation) {
|
||||
@@ -513,28 +513,14 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
is_composite_asset: function (frm) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
} else {
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
if (frm.doc.docstatus == 0) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
} else {
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
}
|
||||
frm.trigger("toggle_reference_doc");
|
||||
}
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
make_sales_invoice: function (frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
item_code: frm.doc.item_code,
|
||||
company: frm.doc.company,
|
||||
serial_no: frm.doc.serial_no,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
create_asset_maintenance: function (frm) {
|
||||
@@ -585,6 +571,69 @@ frappe.ui.form.on("Asset", {
|
||||
});
|
||||
},
|
||||
|
||||
sell_asset: function (frm) {
|
||||
const make_sales_invoice = (sell_qty) => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
item_code: frm.doc.item_code,
|
||||
company: frm.doc.company,
|
||||
serial_no: frm.doc.serial_no,
|
||||
sell_qty: sell_qty,
|
||||
},
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Sell Asset"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sell_qty",
|
||||
fieldtype: "Int",
|
||||
label: __("Sell Qty"),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
dialog.set_primary_action(__("Sell"), function () {
|
||||
const dialog_data = dialog.get_values();
|
||||
const sell_qty = cint(dialog_data.sell_qty);
|
||||
const asset_qty = cint(frm.doc.asset_quantity);
|
||||
|
||||
if (sell_qty <= 0) {
|
||||
frappe.throw(__("Sell quantity must be greater than zero"));
|
||||
}
|
||||
|
||||
if (sell_qty > asset_qty) {
|
||||
frappe.throw(__("Sell quantity cannot exceed the asset quantity"));
|
||||
}
|
||||
|
||||
if (sell_qty < asset_qty) {
|
||||
frappe.confirm(
|
||||
__(
|
||||
"The sell quantity is less than the total asset quantity. The remaining quantity will be split into a new asset. This action cannot be undone. <br><br><b>Do you want to continue?</b>"
|
||||
),
|
||||
() => {
|
||||
make_sales_invoice(sell_qty);
|
||||
dialog.hide();
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
make_sales_invoice(sell_qty);
|
||||
dialog.hide();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
split_asset: function (frm) {
|
||||
const title = __("Split Asset");
|
||||
|
||||
|
||||
@@ -244,6 +244,8 @@ class Asset(AccountsController):
|
||||
|
||||
def before_submit(self):
|
||||
if self.is_composite_asset and not has_active_capitalization(self.name):
|
||||
if self.split_from and has_active_capitalization(self.split_from):
|
||||
return
|
||||
frappe.throw(_("Please capitalize this asset before submitting."))
|
||||
|
||||
def on_submit(self):
|
||||
@@ -482,6 +484,9 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
def validate_linked_purchase_documents(self):
|
||||
if self.flags.is_split_asset:
|
||||
return
|
||||
|
||||
for fieldname, doctype in [
|
||||
("purchase_receipt", "Purchase Receipt"),
|
||||
("purchase_invoice", "Purchase Invoice"),
|
||||
@@ -1083,7 +1088,7 @@ def get_asset_naming_series():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=None):
|
||||
def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
@@ -1098,7 +1103,7 @@ def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=N
|
||||
"income_account": disposal_account,
|
||||
"serial_no": serial_no,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"qty": 1,
|
||||
"qty": sell_qty,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1378,6 +1383,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
|
||||
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
|
||||
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
|
||||
asset_doc = new_asset if is_new_asset else existing_asset
|
||||
asset_doc.flags.is_split_asset = True
|
||||
|
||||
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
|
||||
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
|
||||
|
||||
@@ -246,7 +246,9 @@ def _make_journal_entry_for_depreciation(
|
||||
|
||||
def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depr_series
|
||||
if depr_series:
|
||||
je.naming_series = depr_series
|
||||
|
||||
je.posting_date = depr_schedule.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = depr_schedule_doc.finance_book
|
||||
|
||||
@@ -330,7 +330,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si = make_sales_invoice(
|
||||
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||
)
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
@@ -458,7 +460,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
post_depreciation_entries(date="2021-01-01")
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si = make_sales_invoice(
|
||||
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||
)
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = nowdate()
|
||||
si.get("items")[0].rate = 25000
|
||||
@@ -698,6 +702,128 @@ class TestAsset(AssetSetup):
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
||||
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
||||
|
||||
def test_partial_asset_sale(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
depreciation_start_date = add_months(get_last_day(date), -2)
|
||||
|
||||
# create an asset
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
is_existing_asset=1,
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
depreciation_start_date=depreciation_start_date,
|
||||
net_purchase_amount=1000000.0,
|
||||
purchase_amount=1000000.0,
|
||||
asset_quantity=10,
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
asset_depr_schedule_before_sale = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||
post_depreciation_entries(date)
|
||||
asset.reload()
|
||||
|
||||
# check asset values before sale
|
||||
self.assertEqual(asset.asset_quantity, 10)
|
||||
self.assertEqual(asset.net_purchase_amount, 1000000)
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
self.assertEqual(
|
||||
asset_depr_schedule_before_sale.depreciation_schedule[0].get("depreciation_amount"), 83333.33
|
||||
)
|
||||
|
||||
# make a partial sales against the asset
|
||||
si = make_sales_invoice(
|
||||
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=5
|
||||
)
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
asset.reload()
|
||||
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||
|
||||
# check asset values after sales
|
||||
self.assertEqual(asset.asset_quantity, 5)
|
||||
self.assertEqual(asset.net_purchase_amount, 500000)
|
||||
self.assertEqual(asset.status, "Sold")
|
||||
self.assertEqual(
|
||||
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||
)
|
||||
|
||||
def test_asset_splitting_for_non_existing_asset(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
depreciation_start_date = add_months(get_last_day(date), -2)
|
||||
|
||||
asset_qty = 10
|
||||
asset_rate = 100000.0
|
||||
asset_item = "Macbook Pro"
|
||||
asset_location = "Test Location"
|
||||
|
||||
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 1)
|
||||
|
||||
# Inward asset via Purchase Receipt
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro",
|
||||
posting_date=purchase_date,
|
||||
qty=asset_qty,
|
||||
rate=asset_rate,
|
||||
location=asset_location,
|
||||
supplier="_Test Supplier",
|
||||
)
|
||||
|
||||
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = purchase_date
|
||||
asset_doc.location = asset_location
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 0,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": depreciation_start_date,
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
# check asset values before splitting
|
||||
asset_depr_schedule_before_splitting = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||
self.assertEqual(asset_doc.asset_quantity, 10)
|
||||
self.assertEqual(asset_doc.net_purchase_amount, 1000000)
|
||||
self.assertEqual(
|
||||
asset_depr_schedule_before_splitting.depreciation_schedule[0].get("depreciation_amount"), 83333.33
|
||||
)
|
||||
|
||||
# initate asset split
|
||||
new_asset = split_asset(asset_doc.name, 5)
|
||||
asset_doc.reload()
|
||||
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||
new_asset_depr_schedule = get_asset_depr_schedule_doc(new_asset.name, "Active")
|
||||
|
||||
# check asset values after splitting
|
||||
self.assertEqual(asset_doc.asset_quantity, 5)
|
||||
self.assertEqual(asset_doc.net_purchase_amount, 500000)
|
||||
self.assertEqual(
|
||||
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||
)
|
||||
|
||||
# check new asset values after splitting
|
||||
self.assertEqual(new_asset.asset_quantity, 5)
|
||||
self.assertEqual(new_asset.net_purchase_amount, 500000)
|
||||
self.assertEqual(
|
||||
new_asset_depr_schedule.depreciation_schedule[0].get("depreciation_amount"), 41666.66
|
||||
)
|
||||
|
||||
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
|
||||
|
||||
|
||||
class TestDepreciationMethods(AssetSetup):
|
||||
def test_schedule_for_straight_line_method(self):
|
||||
@@ -1691,6 +1817,71 @@ class TestDepreciationBasics(AssetSetup):
|
||||
pr.submit()
|
||||
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
||||
|
||||
def test_split_asset_created_via_capitalization(self):
|
||||
"""Test that assets created via Asset Capitalization can be split without capitalization error"""
|
||||
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
|
||||
create_asset_capitalization,
|
||||
create_asset_capitalization_data,
|
||||
)
|
||||
|
||||
# Ensure test data exists
|
||||
create_asset_capitalization_data()
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
total_amount = 2000
|
||||
|
||||
# Create composite asset
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset for Split",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
asset_quantity=2, # Set quantity > 1 to allow splitting
|
||||
)
|
||||
|
||||
# Create and submit Asset Capitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
target_asset=wip_composite_asset.name,
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Verify asset was capitalized
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
# Submit the capitalized asset
|
||||
target_asset.submit()
|
||||
self.assertEqual(target_asset.status, "Submitted")
|
||||
|
||||
# Split the asset - this should work without capitalization error
|
||||
split_qty = 1
|
||||
splitted_asset = split_asset(target_asset.name, split_qty)
|
||||
|
||||
# Verify split asset was created and submitted successfully
|
||||
self.assertIsNotNone(splitted_asset)
|
||||
self.assertEqual(splitted_asset.asset_quantity, split_qty)
|
||||
self.assertEqual(splitted_asset.split_from, target_asset.name)
|
||||
self.assertEqual(splitted_asset.docstatus, 1) # Should be submitted
|
||||
self.assertEqual(splitted_asset.status, "Submitted")
|
||||
|
||||
# Verify original asset was updated
|
||||
target_asset.reload()
|
||||
self.assertEqual(target_asset.asset_quantity, 1) # Remaining quantity
|
||||
|
||||
|
||||
def get_gl_entries(doctype, docname):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -574,13 +574,19 @@ class AssetCapitalization(StockController):
|
||||
if self.docstatus == 2:
|
||||
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
||||
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value)
|
||||
total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
|
||||
else:
|
||||
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
||||
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
|
||||
|
||||
asset_doc.db_set("net_purchase_amount", net_purchase_amount)
|
||||
asset_doc.db_set("purchase_amount", purchase_amount)
|
||||
asset_doc.db_set(
|
||||
{
|
||||
"net_purchase_amount": net_purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
"total_asset_cost": total_asset_cost,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, get_link_to_form
|
||||
from frappe.utils import cstr, get_datetime, get_link_to_form
|
||||
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
|
||||
@@ -34,6 +34,7 @@ class AssetMovement(Document):
|
||||
for d in self.assets:
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
self.validate_transaction_date(d)
|
||||
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
@@ -51,6 +52,18 @@ class AssetMovement(Document):
|
||||
else:
|
||||
self.validate_employee(d)
|
||||
|
||||
def validate_transaction_date(self, d):
|
||||
previous_movement_date = frappe.db.get_value(
|
||||
"Asset Movement",
|
||||
[["Asset Movement Item", "asset", "=", d.asset], ["docstatus", "=", 1]],
|
||||
"transaction_date",
|
||||
order_by="transaction_date desc",
|
||||
)
|
||||
if previous_movement_date and get_datetime(previous_movement_date) > get_datetime(
|
||||
self.transaction_date
|
||||
):
|
||||
frappe.throw(_("Transaction date can't be earlier than previous movement date"))
|
||||
|
||||
def validate_location_and_employee(self, d):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
@@ -4,9 +4,9 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import now
|
||||
from frappe.utils import add_days, now
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -147,6 +147,33 @@ class TestAssetMovement(IntegrationTestCase):
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_movement_transaction_date(self):
|
||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||
asset.save().submit()
|
||||
|
||||
if not frappe.db.exists("Location", "Test Location 2"):
|
||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
||||
|
||||
asset_creation_date = frappe.db.get_value(
|
||||
"Asset Movement",
|
||||
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
||||
"transaction_date",
|
||||
)
|
||||
asset_movement = create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
{
|
||||
"asset": asset.name,
|
||||
"source_location": "Test Location",
|
||||
"target_location": "Test Location 2",
|
||||
}
|
||||
],
|
||||
transaction_date=add_days(asset_creation_date, -1),
|
||||
do_not_save=True,
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, asset_movement.save)
|
||||
|
||||
|
||||
def create_asset_movement(**args):
|
||||
args = frappe._dict(args)
|
||||
@@ -165,9 +192,10 @@ def create_asset_movement(**args):
|
||||
"reference_name": args.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
movement.insert()
|
||||
movement.submit()
|
||||
if not args.do_not_save:
|
||||
movement.insert(ignore_if_duplicate=True)
|
||||
if not args.do_not_submit:
|
||||
movement.submit()
|
||||
|
||||
return movement
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si = make_sales_invoice(
|
||||
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
|
||||
)
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
|
||||
@@ -74,7 +74,7 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
|
||||
self.cancel_asset_revaluation_entry()
|
||||
self.update_asset()
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
@@ -167,6 +167,17 @@ class AssetValueAdjustment(Document):
|
||||
if dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update({dimension["fieldname"]: dimension_value})
|
||||
|
||||
def cancel_asset_revaluation_entry(self):
|
||||
if not self.journal_entry:
|
||||
return
|
||||
|
||||
revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry)
|
||||
if revaluation_entry.docstatus == 1:
|
||||
# Ignore permissions to match Journal Entry submission behavior
|
||||
revaluation_entry.flags.ignore_permissions = True
|
||||
revaluation_entry.flags.via_asset_value_adjustment = True
|
||||
revaluation_entry.cancel()
|
||||
|
||||
def update_asset(self):
|
||||
asset = self.update_asset_value_after_depreciation()
|
||||
note = self.get_adjustment_note()
|
||||
|
||||
@@ -803,7 +803,7 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
|
||||
|
||||
function prevent_past_schedule_dates(frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["schedule_date"].datepicker.update({
|
||||
frm.fields_dict["schedule_date"].datepicker?.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_section",
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"get_items_from_open_material_requests",
|
||||
@@ -21,8 +20,9 @@
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"column_break1",
|
||||
"company",
|
||||
"supplier",
|
||||
"is_subcontracted",
|
||||
"supplier_name",
|
||||
"has_unit_price_items",
|
||||
"supplier_warehouse",
|
||||
"amended_from",
|
||||
@@ -1301,7 +1301,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1309,7 +1310,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-28 11:00:56.635116",
|
||||
"modified": "2026-02-03 14:44:55.192192",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -149,6 +149,7 @@ class PurchaseOrder(BuyingController):
|
||||
supplied_items: DF.Table[PurchaseOrderItemSupplied]
|
||||
supplier: DF.Link
|
||||
supplier_address: DF.Link | None
|
||||
supplier_group: DF.Link | None
|
||||
supplier_name: DF.Data | None
|
||||
supplier_warehouse: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
@@ -190,6 +191,9 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
if self.is_subcontracted:
|
||||
self.status_updater[0]["source_field"] = "fg_item_qty"
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
|
||||
@@ -1330,6 +1330,55 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 50)
|
||||
|
||||
def test_multiple_advances_against_purchase_order_are_allocated_across_partial_purchase_invoices(self):
|
||||
# step - 1: create PO
|
||||
po = create_purchase_order(qty=10, rate=10)
|
||||
|
||||
# step - 2: create first partial advance payment
|
||||
pe1 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
|
||||
pe1.reference_no = "1"
|
||||
pe1.reference_date = nowdate()
|
||||
pe1.paid_amount = 50
|
||||
pe1.references[0].allocated_amount = 50
|
||||
pe1.save(ignore_permissions=True).submit()
|
||||
|
||||
# check first advance paid against PO
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 50)
|
||||
|
||||
# step - 3: create first PI for partial qty and allocate first advance
|
||||
pi_1 = make_pi_from_po(po.name)
|
||||
pi_1.update_stock = 1
|
||||
pi_1.allocate_advances_automatically = 1
|
||||
pi_1.items[0].qty = 5
|
||||
pi_1.save(ignore_permissions=True).submit()
|
||||
|
||||
# step - 4: create second advance payment for remaining
|
||||
pe2 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
|
||||
pe2.reference_no = "2"
|
||||
pe2.reference_date = nowdate()
|
||||
pe2.paid_amount = 50
|
||||
pe2.references[0].allocated_amount = 50
|
||||
pe2.save(ignore_permissions=True).submit()
|
||||
|
||||
# check second advance paid against PO
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
|
||||
# step - 5: create second PI for remaining qty and allocate second advance
|
||||
pi_2 = make_pi_from_po(po.name)
|
||||
pi_2.update_stock = 1
|
||||
pi_2.allocate_advances_automatically = 1
|
||||
pi_2.save(ignore_permissions=True).submit()
|
||||
|
||||
# check PO and PI status
|
||||
po.reload()
|
||||
pi_1.reload()
|
||||
pi_2.reload()
|
||||
self.assertEqual(pi_1.status, "Paid")
|
||||
self.assertEqual(pi_2.status, "Paid")
|
||||
self.assertEqual(po.status, "Completed")
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -304,12 +304,17 @@ class RequestforQuotation(BuyingController):
|
||||
else:
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
|
||||
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
||||
subject_source = (
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation")
|
||||
)
|
||||
rendered_subject = frappe.render_template(subject_source, doc_args)
|
||||
if preview:
|
||||
return {
|
||||
"message": self.message_for_supplier,
|
||||
"subject": self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation"),
|
||||
"message": rendered_message,
|
||||
"subject": rendered_subject,
|
||||
}
|
||||
|
||||
attachments = []
|
||||
@@ -333,10 +338,8 @@ class RequestforQuotation(BuyingController):
|
||||
self.send_email(
|
||||
data,
|
||||
sender,
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation"),
|
||||
self.message_for_supplier,
|
||||
rendered_subject,
|
||||
rendered_message,
|
||||
attachments,
|
||||
)
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@@ -262,7 +261,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-28 23:30:22.927989",
|
||||
"modified": "2026-01-31 19:46:27.884592",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Item",
|
||||
|
||||
@@ -135,14 +135,6 @@ frappe.ui.form.on("Supplier", {
|
||||
// indicators
|
||||
erpnext.utils.set_party_dashboard_indicators(frm);
|
||||
}
|
||||
|
||||
frm.set_query("supplier_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
get_supplier_group_details: function (frm) {
|
||||
frappe.call({
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Supplier Group",
|
||||
"link_filters": "[[\"Supplier Group\",\"is_group\",\"=\",0]]",
|
||||
"oldfieldname": "supplier_type",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Supplier Group"
|
||||
@@ -383,7 +384,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_address",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Primary Address",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -500,7 +501,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-29 05:30:50.398653",
|
||||
"modified": "2026-02-06 12:58:01.398824",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user