mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-30 14:28:37 +00:00
Compare commits
502 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a29468f6fe | ||
|
|
beb4137dac | ||
|
|
db4efd3332 | ||
|
|
7556457e3e | ||
|
|
5590b04c89 | ||
|
|
490cbc53d8 | ||
|
|
592b3ff7b7 | ||
|
|
bc9a63497a | ||
|
|
b9dc0f3896 | ||
|
|
1f21607e4f | ||
|
|
488e693ed8 | ||
|
|
ae2a8db0d7 | ||
|
|
2ba283d138 | ||
|
|
af8af2b36e | ||
|
|
789aee4544 | ||
|
|
02f56ee20e | ||
|
|
c78d4c15d3 | ||
|
|
7833138c57 | ||
|
|
b582e9c42c | ||
|
|
faf79f4d2a | ||
|
|
47c2bd7335 | ||
|
|
73618f0605 | ||
|
|
a08b97e886 | ||
|
|
282c19e7e1 | ||
|
|
53992deb10 | ||
|
|
4235e08668 | ||
|
|
949f4c3790 | ||
|
|
11d4382e19 | ||
|
|
00e86bf318 | ||
|
|
d3c4bd7f5f | ||
|
|
cb15ff1d74 | ||
|
|
55a8326d06 | ||
|
|
451c288011 | ||
|
|
7923bd7964 | ||
|
|
3bef12cb55 | ||
|
|
75bd1e6b65 | ||
|
|
b126720826 | ||
|
|
f246684694 | ||
|
|
1822325f34 | ||
|
|
986273b6d3 | ||
|
|
f4ac00595d | ||
|
|
66661d0649 | ||
|
|
326751c8bd | ||
|
|
5e29aab83b | ||
|
|
1b2831bdfe | ||
|
|
a40864dede | ||
|
|
3eac436e7b | ||
|
|
5fe40ac085 | ||
|
|
4ee8cf3907 | ||
|
|
d7c7748c03 | ||
|
|
f31070741a | ||
|
|
317a74b58c | ||
|
|
0405a2b817 | ||
|
|
e76f59803c | ||
|
|
41e46326c0 | ||
|
|
8574941cb6 | ||
|
|
4f994cf26c | ||
|
|
bb3591deec | ||
|
|
e2d041f51c | ||
|
|
63f306f1b4 | ||
|
|
72c95d31d1 | ||
|
|
1ee15f65a8 | ||
|
|
0a235584ad | ||
|
|
7f4cd3cd15 | ||
|
|
ba55d0ede7 | ||
|
|
c74482a27c | ||
|
|
510ab769f1 | ||
|
|
e9314325cc | ||
|
|
cea4ed6f88 | ||
|
|
29eb090528 | ||
|
|
aebb0c7979 | ||
|
|
dcdd3e8cc4 | ||
|
|
f84f82c7d7 | ||
|
|
32f77eae5d | ||
|
|
d785c6ce3e | ||
|
|
bc8ab51f9d | ||
|
|
5c9c3bee70 | ||
|
|
c9ea83591f | ||
|
|
10fcf5af5f | ||
|
|
e4230cf6d9 | ||
|
|
53c705506b | ||
|
|
960443d268 | ||
|
|
f71af209c0 | ||
|
|
6b20a7eed1 | ||
|
|
5cdc22adf3 | ||
|
|
9e36ab7de7 | ||
|
|
daafe45dd4 | ||
|
|
706d1111ad | ||
|
|
da33079f12 | ||
|
|
07bcc24e35 | ||
|
|
0f0a5b73f6 | ||
|
|
0aa73e31ce | ||
|
|
a3d4f63ba1 | ||
|
|
ff166e8104 | ||
|
|
a118417645 | ||
|
|
a83f3106f3 | ||
|
|
6d4ca6a1ee | ||
|
|
ebec463166 | ||
|
|
10488520e7 | ||
|
|
9e15a3c981 | ||
|
|
63eef680d0 | ||
|
|
7def475eb1 | ||
|
|
0e2f57ba89 | ||
|
|
30bdba4801 | ||
|
|
ed06f0cd1b | ||
|
|
9abc07e34b | ||
|
|
a7d13686c7 | ||
|
|
ab99414242 | ||
|
|
fa8fb7a49e | ||
|
|
4fc3a264f9 | ||
|
|
5df2f5d523 | ||
|
|
e0c35d60b4 | ||
|
|
14aa9d3d0f | ||
|
|
3bdd1e9514 | ||
|
|
171586db9b | ||
|
|
c3557a87ef | ||
|
|
4853ab1fec | ||
|
|
ed032b3224 | ||
|
|
8b7900e6a7 | ||
|
|
e0a38207f6 | ||
|
|
53b844f173 | ||
|
|
34e581715a | ||
|
|
83c120849f | ||
|
|
e08c7bad63 | ||
|
|
2905db19e5 | ||
|
|
bd7e8ed230 | ||
|
|
b2e0d24954 | ||
|
|
84a94cec6b | ||
|
|
cc195777f4 | ||
|
|
60c10f4457 | ||
|
|
871dc6b0da | ||
|
|
3549cd39f1 | ||
|
|
222005ca44 | ||
|
|
98c56535d8 | ||
|
|
f6a779e1aa | ||
|
|
1f5dbebe68 | ||
|
|
4073dfb90b | ||
|
|
e2d85c513c | ||
|
|
880f648846 | ||
|
|
84d2a80f30 | ||
|
|
fe41e2a341 | ||
|
|
a97b8fd876 | ||
|
|
6ed42e939b | ||
|
|
f9d1995954 | ||
|
|
028ae5b34f | ||
|
|
2a7f7df3e5 | ||
|
|
1331fb75a3 | ||
|
|
ca93f671df | ||
|
|
c76145985d | ||
|
|
a6b4d5b101 | ||
|
|
c7d5857014 | ||
|
|
b53e072082 | ||
|
|
dc88614f89 | ||
|
|
0acbbefa8b | ||
|
|
9a0834987c | ||
|
|
3e9449d511 | ||
|
|
80e3597338 | ||
|
|
3c84d5ec02 | ||
|
|
b0e872aad4 | ||
|
|
96e1db80ac | ||
|
|
35d3edfca4 | ||
|
|
b4981f8816 | ||
|
|
7c666435a2 | ||
|
|
b388adcb2e | ||
|
|
3ba413a150 | ||
|
|
ddb84d9036 | ||
|
|
00a3ee313b | ||
|
|
f8d7ca515c | ||
|
|
550213b4f4 | ||
|
|
a962657c7b | ||
|
|
86f3f62e00 | ||
|
|
74613c7fe8 | ||
|
|
0c222aa504 | ||
|
|
3244ef3a68 | ||
|
|
b649c50ecc | ||
|
|
d5c26efb8c | ||
|
|
e00533ff4e | ||
|
|
6509548474 | ||
|
|
c8b3478fe1 | ||
|
|
f453aaeab0 | ||
|
|
ff1647a1d2 | ||
|
|
8bc8bc1822 | ||
|
|
9f86b92eb7 | ||
|
|
04c8e9b355 | ||
|
|
fbfd0a6db0 | ||
|
|
9e33216c24 | ||
|
|
62f7d984be | ||
|
|
e219042304 | ||
|
|
011c5a69f0 | ||
|
|
e3cd35b959 | ||
|
|
53f61f1ad1 | ||
|
|
c939aa5cf8 | ||
|
|
93a861fac5 | ||
|
|
5a65a10dba | ||
|
|
1b8f572e80 | ||
|
|
07e2901e4b | ||
|
|
8816b2740a | ||
|
|
bcf8053fd9 | ||
|
|
cd1820f680 | ||
|
|
235e3893a0 | ||
|
|
80956b7956 | ||
|
|
9897c26ae6 | ||
|
|
32afe7de31 | ||
|
|
c288db0356 | ||
|
|
61f073f8b1 | ||
|
|
8a356cbe51 | ||
|
|
1503ba92e3 | ||
|
|
e282ba78c1 | ||
|
|
1b8b92b8d7 | ||
|
|
f2e19ca6fd | ||
|
|
32c6111728 | ||
|
|
f609b8ae5d | ||
|
|
dbbba046ab | ||
|
|
b43f70325c | ||
|
|
3f5b1bcd05 | ||
|
|
1394a1c5e9 | ||
|
|
7ca9ffbaa7 | ||
|
|
137ce76a35 | ||
|
|
cd870ee30c | ||
|
|
2dc8686a81 | ||
|
|
6bdf944ecf | ||
|
|
9842fb5b64 | ||
|
|
c548c57207 | ||
|
|
cb326ac0db | ||
|
|
f0480173fb | ||
|
|
eec6c25f9e | ||
|
|
bf4e514cde | ||
|
|
61595c7ede | ||
|
|
d510b46f13 | ||
|
|
15e116375b | ||
|
|
b8f4c3193a | ||
|
|
832aa5f5ab | ||
|
|
ca18853785 | ||
|
|
049864e7a2 | ||
|
|
62db0d77ca | ||
|
|
50fe4191d3 | ||
|
|
04c96ddc6c | ||
|
|
d7840559a0 | ||
|
|
6f06313023 | ||
|
|
6b2e3503d9 | ||
|
|
b2fc5e4988 | ||
|
|
16860c228d | ||
|
|
8a84f8a465 | ||
|
|
21eed78fa1 | ||
|
|
e6f3a14289 | ||
|
|
2beb3f8718 | ||
|
|
9aae439b1f | ||
|
|
b748095589 | ||
|
|
7bd2acdef1 | ||
|
|
a51f956f3e | ||
|
|
494c26e48a | ||
|
|
da7b2febe0 | ||
|
|
04037b7e38 | ||
|
|
484db65797 | ||
|
|
3f4cf15def | ||
|
|
758b9b8a81 | ||
|
|
7ca8e49b38 | ||
|
|
d882305c72 | ||
|
|
5dc2e80987 | ||
|
|
1dc85fcaf8 | ||
|
|
e4e3313a0e | ||
|
|
2598f8e7ec | ||
|
|
fe973a4b53 | ||
|
|
2448ba6bc4 | ||
|
|
f82125144a | ||
|
|
38c1350018 | ||
|
|
24cfcf36e4 | ||
|
|
0c0be03e90 | ||
|
|
cd57bbc3d0 | ||
|
|
afefae08af | ||
|
|
fec892c211 | ||
|
|
87d1b0f476 | ||
|
|
f0070b4046 | ||
|
|
5d6bc96375 | ||
|
|
4fa978ef1f | ||
|
|
f00a6f6c59 | ||
|
|
78c65f27b0 | ||
|
|
a326876cc5 | ||
|
|
9f793b9b28 | ||
|
|
bb6025ca26 | ||
|
|
4ad61fc3a5 | ||
|
|
e62dd0d7cb | ||
|
|
a147e2954f | ||
|
|
0898ea5d92 | ||
|
|
d0688e7a55 | ||
|
|
756c062733 | ||
|
|
ec7a60bd02 | ||
|
|
53eb6129ea | ||
|
|
277aa7bc2b | ||
|
|
97a51487c1 | ||
|
|
53208dfb3e | ||
|
|
1d2e831846 | ||
|
|
50300b9be6 | ||
|
|
abc99f84e9 | ||
|
|
3caf462c6b | ||
|
|
58357f8891 | ||
|
|
8cc9ddaebd | ||
|
|
1f64b8fcd8 | ||
|
|
80a2b79bba | ||
|
|
b064944753 | ||
|
|
672e6d6d06 | ||
|
|
926850d1f6 | ||
|
|
d6d54ed220 | ||
|
|
e89dce726b | ||
|
|
83288fedf1 | ||
|
|
16269b0730 | ||
|
|
f1d25700a8 | ||
|
|
00d36d14fb | ||
|
|
c912e4c99d | ||
|
|
1eee52c59c | ||
|
|
51d8a7a0c1 | ||
|
|
f0bc0aa676 | ||
|
|
f3882a854f | ||
|
|
5e4693763a | ||
|
|
82b96d3688 | ||
|
|
0454dede92 | ||
|
|
6020c8e8f7 | ||
|
|
b192ddd13b | ||
|
|
2db1e1a737 | ||
|
|
7a5a4be02f | ||
|
|
b834abbd4f | ||
|
|
7640feaaa7 | ||
|
|
9e1b443857 | ||
|
|
18575e137c | ||
|
|
0784488eec | ||
|
|
290397172e | ||
|
|
e3c473482b | ||
|
|
bc529b51dc | ||
|
|
5ee6cc4bac | ||
|
|
54a0df5164 | ||
|
|
40ec5ff90f | ||
|
|
aad39cf686 | ||
|
|
3eac0e907e | ||
|
|
d2fdce007e | ||
|
|
458064f8a1 | ||
|
|
56d510b878 | ||
|
|
57bcf9f568 | ||
|
|
2e0abbd274 | ||
|
|
73c0f3703c | ||
|
|
f3088e08bb | ||
|
|
21f90011bc | ||
|
|
9fdf5c8091 | ||
|
|
7539623fb9 | ||
|
|
cbaa617d20 | ||
|
|
d85dad7198 | ||
|
|
e5d73c780b | ||
|
|
8ad7fafe2a | ||
|
|
2e932754e0 | ||
|
|
d8cf994e94 | ||
|
|
89d1069472 | ||
|
|
3a7506ecbc | ||
|
|
f3254c2010 | ||
|
|
2a390ac2de | ||
|
|
5158884dc9 | ||
|
|
ec436c18a3 | ||
|
|
17bae5a5a1 | ||
|
|
027a75bcb0 | ||
|
|
2e919344df | ||
|
|
d2580be4fd | ||
|
|
e0755f9a9a | ||
|
|
c4794c95df | ||
|
|
12c5f0b6b5 | ||
|
|
0a3e378f5e | ||
|
|
1b399e83be | ||
|
|
49f93f9fa1 | ||
|
|
d2fe221bc7 | ||
|
|
a3cecb892a | ||
|
|
7c7c3c932f | ||
|
|
46035ed9ca | ||
|
|
7e7eb95934 | ||
|
|
83e62bbaad | ||
|
|
f8625f3eb7 | ||
|
|
4e2304818b | ||
|
|
3caabf23a5 | ||
|
|
76c6b501f9 | ||
|
|
f3ab5a33c7 | ||
|
|
e2bf03bdb2 | ||
|
|
5874be0f79 | ||
|
|
ab9fce333d | ||
|
|
0e48ef7ace | ||
|
|
a41cf62437 | ||
|
|
f704ccbb59 | ||
|
|
1ea8574be9 | ||
|
|
e0dbb573b1 | ||
|
|
82960e3312 | ||
|
|
8f643f0df8 | ||
|
|
8c2c90f77a | ||
|
|
a8f3f2343d | ||
|
|
308c6ffb4f | ||
|
|
a99470e017 | ||
|
|
eabb956aca | ||
|
|
f6eb2b521d | ||
|
|
135aced98e | ||
|
|
bf98a8f855 | ||
|
|
74606dc927 | ||
|
|
58f1df5004 | ||
|
|
e3be9c1da4 | ||
|
|
648f275797 | ||
|
|
4626ab5e19 | ||
|
|
2d9a0a8e2e | ||
|
|
32d3d4e571 | ||
|
|
d75ac136d7 | ||
|
|
88b3811c5d | ||
|
|
2a5164d170 | ||
|
|
028228ee12 | ||
|
|
4e27174c85 | ||
|
|
d65be69c4c | ||
|
|
5ec75fb6df | ||
|
|
cbbc6af128 | ||
|
|
7320440b61 | ||
|
|
637343f751 | ||
|
|
ffb6d65910 | ||
|
|
cff9e47162 | ||
|
|
dc9b4de976 | ||
|
|
a65bc77b02 | ||
|
|
02ceee6669 | ||
|
|
4bd1a5f955 | ||
|
|
6ad75e72e6 | ||
|
|
c696370cff | ||
|
|
5a807af505 | ||
|
|
4fe7988249 | ||
|
|
58de9913b9 | ||
|
|
b52ceceeb7 | ||
|
|
8990c48e7b | ||
|
|
18bd330a59 | ||
|
|
b02604396d | ||
|
|
7d844411fb | ||
|
|
fba28d6941 | ||
|
|
57f7933a4b | ||
|
|
2045306283 | ||
|
|
4fb479e2f5 | ||
|
|
77dba4834c | ||
|
|
a70be4299e | ||
|
|
4ed86dbff2 | ||
|
|
63f6970b45 | ||
|
|
7802f6c528 | ||
|
|
7fc8150617 | ||
|
|
6e92c78cbd | ||
|
|
5beea361fe | ||
|
|
aa5e16e681 | ||
|
|
27f454d07e | ||
|
|
bf8a2d0e3a | ||
|
|
d890391531 | ||
|
|
6e1d9a3dbc | ||
|
|
703be50bc7 | ||
|
|
013d0ff99c | ||
|
|
b1b157aa19 | ||
|
|
e7e5727015 | ||
|
|
ad3a5b58e4 | ||
|
|
f501c8b336 | ||
|
|
f7706211ea | ||
|
|
a551660d2a | ||
|
|
5cbe170117 | ||
|
|
f55b561ff9 | ||
|
|
753c1aa406 | ||
|
|
ff85f2a940 | ||
|
|
04c605d76b | ||
|
|
3fabca1051 | ||
|
|
32ec73dd96 | ||
|
|
f30bede2e0 | ||
|
|
1dcb065c64 | ||
|
|
feb452b740 | ||
|
|
86b276a3ec | ||
|
|
0d8a52f63b | ||
|
|
2bd96713db | ||
|
|
1d2edec550 | ||
|
|
965126df83 | ||
|
|
6e2cde4a21 | ||
|
|
a75081b8c0 | ||
|
|
b68e1f6ea6 | ||
|
|
9239e735ad | ||
|
|
576ff49943 | ||
|
|
92b5c80a4a | ||
|
|
d188c8ec0e | ||
|
|
4055543f5d | ||
|
|
fc79c6bf54 | ||
|
|
522113ba98 | ||
|
|
6469526c26 | ||
|
|
122e6902ed | ||
|
|
64d93cec66 | ||
|
|
14ee13c77e | ||
|
|
b17178bba9 | ||
|
|
3845d4294c | ||
|
|
2fcfebe1d3 | ||
|
|
e205772482 | ||
|
|
7cb38a8f22 | ||
|
|
9d5c79d6b2 | ||
|
|
dd07ecad45 | ||
|
|
b0675f6490 | ||
|
|
4150ed9b3b | ||
|
|
0b2e2a2ab5 | ||
|
|
c70e6f23df | ||
|
|
5cf0c896bb | ||
|
|
d79e6e353e | ||
|
|
ce2bd15872 | ||
|
|
deef6ea66a | ||
|
|
aa66ee64ec | ||
|
|
eeb0567a73 | ||
|
|
c7dbcbcd17 | ||
|
|
4b76cc46a1 | ||
|
|
bfe2b923e1 | ||
|
|
dbdb971e10 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
|
||||
@@ -5,7 +5,7 @@ fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "erpnext.*"
|
||||
@@ -15,6 +15,10 @@ repos:
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.44.0
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.5.0"
|
||||
__version__ = "15.13.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -118,6 +118,7 @@ class Account(NestedSet):
|
||||
self.validate_balance_must_be_debit_or_credit()
|
||||
self.validate_account_currency()
|
||||
self.validate_root_company_and_sync_account_to_children()
|
||||
self.validate_receivable_payable_account_type()
|
||||
|
||||
def validate_parent_child_account_type(self):
|
||||
if self.parent_account:
|
||||
@@ -188,6 +189,24 @@ class Account(NestedSet):
|
||||
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
|
||||
)
|
||||
|
||||
def validate_receivable_payable_account_type(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
receivable_payable_types = ["Receivable", "Payable"]
|
||||
if (
|
||||
doc_before_save
|
||||
and doc_before_save.account_type in receivable_payable_types
|
||||
and doc_before_save.account_type != self.account_type
|
||||
):
|
||||
# check for ledger entries
|
||||
if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
def validate_root_details(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"},
|
||||
"0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"},
|
||||
"0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"},
|
||||
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
|
||||
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
|
||||
"0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"},
|
||||
"0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"},
|
||||
"0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"},
|
||||
@@ -65,42 +65,41 @@
|
||||
"0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"},
|
||||
"0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
},
|
||||
"Klasse 1 Aktiva: Vorr\u00e4te": {
|
||||
"1000 Bezugsverrechnung": {"account_type": "Stock"},
|
||||
"1100 Rohstoffe": {"account_type": "Stock"},
|
||||
"1200 Bezogene Teile": {"account_type": "Stock"},
|
||||
"1300 Hilfsstoffe": {"account_type": "Stock"},
|
||||
"1350 Betriebsstoffe": {"account_type": "Stock"},
|
||||
"1360 Vorrat Energietraeger": {"account_type": "Stock"},
|
||||
"1360 Vorrat Energietraeger": {"account_type": "Stock"},
|
||||
"1400 Unfertige Erzeugnisse": {"account_type": "Stock"},
|
||||
"1500 Fertige Erzeugnisse": {"account_type": "Stock"},
|
||||
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"},
|
||||
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"},
|
||||
"1900 Wertberichtigungen": {"account_type": "Stock"},
|
||||
"1800 Geleistete Anzahlungen": {"account_type": "Stock"},
|
||||
"1900 Wertberichtigungen": {"account_type": "Stock"},
|
||||
"root_type": "Asset"
|
||||
},
|
||||
},
|
||||
"Klasse 3 Passiva: Verbindlichkeiten": {
|
||||
"3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"},
|
||||
"3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"},
|
||||
"3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"},
|
||||
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
|
||||
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
|
||||
"3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"},
|
||||
"3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"},
|
||||
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
|
||||
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
|
||||
"3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
|
||||
"3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"},
|
||||
"3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"},
|
||||
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
|
||||
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
|
||||
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
|
||||
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
|
||||
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
|
||||
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
|
||||
"3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"},
|
||||
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
|
||||
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
|
||||
"3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"},
|
||||
"3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"},
|
||||
"3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"},
|
||||
@@ -119,13 +118,13 @@
|
||||
},
|
||||
"3515 Umsatzsteuer Inland 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"3520 Umsatzsteuer aus i.g. Erwerb 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"3525 Umsatzsteuer aus i.g. Erwerb 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
|
||||
"3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
|
||||
"account_type": "Payable"
|
||||
@@ -141,7 +140,7 @@
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"root_type": "Liability"
|
||||
},
|
||||
},
|
||||
"Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": {
|
||||
"2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": {
|
||||
"account_type": "Receivable"
|
||||
@@ -154,7 +153,7 @@
|
||||
},
|
||||
"2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
},
|
||||
"2100 Forderungen aus Lieferungen und Leistungen EU": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
@@ -192,7 +191,7 @@
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"},
|
||||
|
||||
|
||||
"2460 Eingeforderte aber noch nicht eingezahlte Einlagen": {
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
@@ -243,10 +242,10 @@
|
||||
},
|
||||
"2800 Guthaben bei Bank": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2801 Guthaben bei Bank - Sparkonto": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2810 Guthaben bei Paypal": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
@@ -264,19 +263,19 @@
|
||||
},
|
||||
"2895 Schwebende Geldbewegugen": {
|
||||
"account_type": "Bank"
|
||||
},
|
||||
},
|
||||
"2513 Vorsteuer Inland 5%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"2515 Vorsteuer Inland 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
},
|
||||
"2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
|
||||
"account_type": "Tax"
|
||||
},
|
||||
@@ -286,16 +285,16 @@
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Klasse 4: Betriebliche Erträge": {
|
||||
"4000 Erlöse 20 %": {"account_type": "Income Account"},
|
||||
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
|
||||
"4000 Erlöse 20 %": {"account_type": "Income Account"},
|
||||
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
|
||||
"4010 Erl\u00f6se 10 %": {"account_type": "Income Account"},
|
||||
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
|
||||
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
|
||||
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
|
||||
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
|
||||
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
|
||||
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
|
||||
"4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"},
|
||||
"4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"},
|
||||
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
|
||||
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
|
||||
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
|
||||
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
|
||||
"4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"},
|
||||
"4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"},
|
||||
"4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
|
||||
@@ -304,15 +303,15 @@
|
||||
"4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"},
|
||||
"4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"},
|
||||
"root_type": "Income"
|
||||
},
|
||||
},
|
||||
"Klasse 5: Aufwand f\u00fcr Material und Leistungen": {
|
||||
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
|
||||
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
|
||||
"5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"},
|
||||
"5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
|
||||
"5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"},
|
||||
"5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"},
|
||||
"5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"},
|
||||
@@ -340,7 +339,7 @@
|
||||
"6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"},
|
||||
"6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
},
|
||||
"Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": {
|
||||
"7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"},
|
||||
"7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"},
|
||||
@@ -349,7 +348,7 @@
|
||||
"7310 Fahrrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7320 Kfz - Aufwand": {"account_type": "Expense Account"},
|
||||
"7330 LKW - Aufwand": {"account_type": "Expense Account"},
|
||||
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
|
||||
"7350 Reise- und Fahraufwand": {"account_type": "Expense Account"},
|
||||
"7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"},
|
||||
"7380 Nachrichtenaufwand": {"account_type": "Expense Account"},
|
||||
@@ -409,7 +408,7 @@
|
||||
"8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"},
|
||||
"8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"},
|
||||
"root_type": "Income"
|
||||
},
|
||||
},
|
||||
"Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": {
|
||||
"9000 Gezeichnetes bzw. gewidmetes Kapital": {
|
||||
"account_type": "Equity"
|
||||
@@ -435,5 +434,5 @@
|
||||
},
|
||||
"root_type": "Equity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
},
|
||||
"Stocks": {
|
||||
"Mati\u00e8res premi\u00e8res": {},
|
||||
"Stock de produits fini": {},
|
||||
"Stock de produits fini": {
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
|
||||
"Travaux en cours": {},
|
||||
"account_type": "Stock"
|
||||
@@ -395,9 +397,11 @@
|
||||
},
|
||||
"Produits": {
|
||||
"Revenus de ventes": {
|
||||
" Escomptes de volume sur ventes": {},
|
||||
"Escomptes de volume sur ventes": {},
|
||||
"Autres produits d'exploitation": {},
|
||||
"Ventes": {},
|
||||
"Ventes": {
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Ventes avec des provinces harmonis\u00e9es": {},
|
||||
"Ventes avec des provinces non-harmonis\u00e9es": {},
|
||||
"Ventes \u00e0 l'\u00e9tranger": {}
|
||||
|
||||
@@ -53,8 +53,13 @@
|
||||
},
|
||||
"II. Forderungen und sonstige Vermögensgegenstände": {
|
||||
"is_group": 1,
|
||||
"Ford. a. Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1400",
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1410",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Durchlaufende Posten": {
|
||||
@@ -180,8 +185,13 @@
|
||||
},
|
||||
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1600",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -407,13 +407,10 @@
|
||||
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "9960"
|
||||
},
|
||||
"Debitoren": {
|
||||
"is_group": 1,
|
||||
"account_number": "10000"
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1200",
|
||||
"account_type": "Receivable"
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1210"
|
||||
@@ -1138,18 +1135,15 @@
|
||||
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "9964"
|
||||
},
|
||||
"Kreditoren": {
|
||||
"account_number": "70000",
|
||||
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1,
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"account_number": "70001",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
}
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "3310"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"country_code": "hu",
|
||||
"name": "Hungary - Chart of Accounts for Microenterprises",
|
||||
"tree": {
|
||||
"SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
|
||||
"account_number": 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.account import (
|
||||
InvalidAccountMergeError,
|
||||
@@ -324,6 +325,19 @@ class TestAccount(unittest.TestCase):
|
||||
acc.account_currency = "USD"
|
||||
self.assertRaises(frappe.ValidationError, acc.save)
|
||||
|
||||
def test_account_balance(self):
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
|
||||
if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Test Percent Account %5"
|
||||
acc.parent_account = "Tax Assets - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
|
||||
balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate())
|
||||
self.assertEqual(balance, 0)
|
||||
|
||||
|
||||
def _make_test_records(verbose=None):
|
||||
from frappe.test_runner import make_test_objects
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2013-06-24 15:49:57",
|
||||
"description": "Settings for Accounts",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"editable_grid": 1,
|
||||
@@ -462,7 +461,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-20 09:37:47.650347",
|
||||
"modified": "2024-01-30 14:04:26.553554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
@@ -19,7 +20,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-01 12:32:34.044911",
|
||||
"modified": "2024-01-03 11:13:02.669632",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Allowed To Transact With",
|
||||
@@ -28,5 +29,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, get_link_to_form
|
||||
|
||||
|
||||
class BankAccount(Document):
|
||||
@@ -52,6 +53,17 @@ class BankAccount(Document):
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, 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_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if include_pos_transactions:
|
||||
pos_sales_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pos_purchase_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
account.account_currency, 0 as debit
|
||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
||||
where
|
||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
pos_sales_invoices = (
|
||||
frappe.qb.from_(si_payment)
|
||||
.inner_join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
.inner_join(acc)
|
||||
.on(si_payment.account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Sales Invoice").as_("payment_document"),
|
||||
si.name.as_("payment_entry"),
|
||||
si_payment.reference_no.as_("cheque_number"),
|
||||
si_payment.amount.as_("debit"),
|
||||
si.posting_date,
|
||||
si.customer.as_("against_account"),
|
||||
si_payment.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si_payment.account == account)
|
||||
& (si.posting_date >= from_date)
|
||||
& (si.posting_date <= to_date)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
pos_purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
|
||||
@@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||
args: {
|
||||
bank_account: frm.doc.bank_account,
|
||||
till_date: frm.doc.bank_statement_from_date,
|
||||
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
|
||||
},
|
||||
callback: (response) => {
|
||||
frm.set_value("account_opening_balance", response.message);
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
|
||||
get_entries,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class BankReconciliationTool(Document):
|
||||
@@ -150,7 +151,7 @@ def create_journal_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
||||
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
@@ -164,29 +165,94 @@ def create_journal_entry_bts(
|
||||
)
|
||||
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
|
||||
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
|
||||
|
||||
# determine if multi-currency Journal or not
|
||||
is_multi_currency = (
|
||||
True
|
||||
if company_default_currency != company_account_currency
|
||||
or company_default_currency != second_account_currency
|
||||
or company_default_currency != bank_transaction.currency
|
||||
else False
|
||||
)
|
||||
|
||||
accounts = []
|
||||
# Multi Currency?
|
||||
accounts.append(
|
||||
{
|
||||
"account": second_account,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
second_account_dict = {
|
||||
"account": second_account,
|
||||
"account_currency": second_account_currency,
|
||||
"credit_in_account_currency": bank_transaction.deposit,
|
||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": company_account,
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
)
|
||||
company_account_dict = {
|
||||
"account": company_account,
|
||||
"account_currency": company_account_currency,
|
||||
"bank_account": bank_transaction.bank_account,
|
||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||
"debit_in_account_currency": bank_transaction.deposit,
|
||||
"cost_center": get_default_cost_center(company),
|
||||
}
|
||||
|
||||
# convert transaction amount to company currency
|
||||
if is_multi_currency:
|
||||
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
|
||||
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
|
||||
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
|
||||
else:
|
||||
withdrawal_in_company_currency = bank_transaction.withdrawal
|
||||
deposit_in_company_currency = bank_transaction.deposit
|
||||
|
||||
# if second account is of foreign currency, convert and set debit and credit fields.
|
||||
if second_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
|
||||
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
second_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": deposit_in_company_currency,
|
||||
"debit": withdrawal_in_company_currency,
|
||||
"credit_in_account_currency": deposit_in_company_currency,
|
||||
"debit_in_account_currency": withdrawal_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
# if company account is of foreign currency, convert and set debit and credit fields.
|
||||
if company_account_currency != company_default_currency:
|
||||
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": exc_rate,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
else:
|
||||
company_account_dict.update(
|
||||
{
|
||||
"exchange_rate": 1,
|
||||
"credit": withdrawal_in_company_currency,
|
||||
"debit": deposit_in_company_currency,
|
||||
"credit_in_account_currency": withdrawal_in_company_currency,
|
||||
"debit_in_account_currency": deposit_in_company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(second_account_dict)
|
||||
accounts.append(company_account_dict)
|
||||
|
||||
journal_entry_dict = {
|
||||
"voucher_type": entry_type,
|
||||
@@ -196,6 +262,9 @@ def create_journal_entry_bts(
|
||||
"cheque_no": reference_number,
|
||||
"mode_of_payment": mode_of_payment,
|
||||
}
|
||||
if is_multi_currency:
|
||||
journal_entry_dict.update({"multi_currency": True})
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.update(journal_entry_dict)
|
||||
journal_entry.set("accounts", accounts)
|
||||
@@ -375,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.add_payment_entries(vouchers)
|
||||
transaction.validate_duplicate_references()
|
||||
transaction.allocate_payment_entries()
|
||||
transaction.update_allocated_amount()
|
||||
transaction.set_status()
|
||||
transaction.save()
|
||||
|
||||
return transaction
|
||||
|
||||
@@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
||||
"deposit": 100,
|
||||
"bank_account": self.bank_account,
|
||||
"reference_number": "123",
|
||||
"currency": "INR",
|
||||
}
|
||||
)
|
||||
.save()
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
|
||||
class BankTransaction(StatusUpdater):
|
||||
class BankTransaction(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -49,6 +49,33 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
def validate(self):
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
def validate_currency(self):
|
||||
"""
|
||||
Bank Transaction should be on the same currency as the Bank Account.
|
||||
"""
|
||||
if self.currency and self.bank_account:
|
||||
account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
|
||||
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||
|
||||
if self.currency != account_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||
).format(
|
||||
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
|
||||
)
|
||||
)
|
||||
|
||||
def set_status(self):
|
||||
if self.docstatus == 2:
|
||||
self.db_set("status", "Cancelled")
|
||||
elif self.docstatus == 1:
|
||||
if self.unallocated_amount > 0:
|
||||
self.db_set("status", "Unreconciled")
|
||||
elif self.unallocated_amount <= 0:
|
||||
self.db_set("status", "Reconciled")
|
||||
|
||||
def validate_duplicate_references(self):
|
||||
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
|
||||
@@ -83,12 +110,13 @@ class BankTransaction(StatusUpdater):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
|
||||
self.set_status(update=True)
|
||||
self.set_status()
|
||||
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
@@ -366,15 +394,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||
):
|
||||
return
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
if doctype == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
dict(parenttype=doctype, parent=docname),
|
||||
"clearance_date",
|
||||
clearance_date,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||
|
||||
elif doctype == "Bank Transaction":
|
||||
# For when a second bank transaction has fixed another, e.g. refund
|
||||
@@ -404,3 +434,21 @@ def unclear_reference_payment(doctype, docname, bt_name):
|
||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||
set_voucher_clearance(doctype, docname, None, bt)
|
||||
return docname
|
||||
|
||||
|
||||
def remove_from_bank_transaction(doctype, docname):
|
||||
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
||||
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||
if bt.docstatus == DocStatus.cancelled():
|
||||
continue
|
||||
|
||||
modified = False
|
||||
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == doctype and pe.payment_entry == docname:
|
||||
bt.remove(pe)
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
bt.save()
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import utils
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
@@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
|
||||
frappe.db.delete(dt)
|
||||
clear_loan_transactions()
|
||||
make_pos_profile()
|
||||
add_transactions()
|
||||
add_vouchers()
|
||||
|
||||
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
|
||||
uniq_identifier = frappe.generate_hash(length=10)
|
||||
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
|
||||
bank_account = create_bank_account(
|
||||
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
|
||||
)
|
||||
|
||||
add_transactions(bank_account=bank_account)
|
||||
add_vouchers(gl_account=gl_account)
|
||||
|
||||
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
|
||||
def test_linked_payments(self):
|
||||
@@ -81,6 +89,29 @@ class TestBankTransaction(FrappeTestCase):
|
||||
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
|
||||
self.assertFalse(clearance_date)
|
||||
|
||||
def test_cancel_voucher(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
payment.reload()
|
||||
payment.cancel()
|
||||
bank_transaction.reload()
|
||||
self.assertEqual(bank_transaction.docstatus, DocStatus.submitted())
|
||||
self.assertEqual(bank_transaction.unallocated_amount, 1700)
|
||||
self.assertEqual(bank_transaction.payment_entries, [])
|
||||
|
||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
@@ -196,7 +227,9 @@ def clear_loan_transactions():
|
||||
frappe.db.delete("Loan Repayment")
|
||||
|
||||
|
||||
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
def create_bank_account(
|
||||
bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
|
||||
):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@@ -208,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
pass
|
||||
|
||||
try:
|
||||
frappe.get_doc(
|
||||
bank_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "Checking Account",
|
||||
"account_name": bank_account_name,
|
||||
"bank": bank_name,
|
||||
"account": account_name,
|
||||
"account": gl_account,
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
return bank_account.name
|
||||
|
||||
def add_transactions():
|
||||
create_bank_account()
|
||||
|
||||
def create_gl_account(gl_account_name="_Test Bank - _TC"):
|
||||
gl_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"company": "_Test Company",
|
||||
"parent_account": "Current Assets - _TC",
|
||||
"account_type": "Bank",
|
||||
"is_group": 0,
|
||||
"account_name": gl_account_name,
|
||||
}
|
||||
).insert()
|
||||
return gl_account.name
|
||||
|
||||
|
||||
def add_transactions(bank_account="_Test Bank - _TC"):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
@@ -230,7 +277,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1200,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@@ -242,7 +289,7 @@ def add_transactions():
|
||||
"date": "2018-10-23",
|
||||
"deposit": 1700,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@@ -254,7 +301,7 @@ def add_transactions():
|
||||
"date": "2018-10-26",
|
||||
"withdrawal": 690,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@@ -266,7 +313,7 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"deposit": 3900,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
@@ -278,13 +325,13 @@ def add_transactions():
|
||||
"date": "2018-10-27",
|
||||
"withdrawal": 109080,
|
||||
"currency": "INR",
|
||||
"bank_account": "Checking Account - Citi Bank",
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
).insert()
|
||||
doc.submit()
|
||||
|
||||
|
||||
def add_vouchers():
|
||||
def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@@ -300,7 +347,7 @@ def add_vouchers():
|
||||
|
||||
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Conrad Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
@@ -319,14 +366,14 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Herr G Oct 18"
|
||||
pe.reference_date = "2018-10-24"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Herr G Nov 18"
|
||||
pe.reference_date = "2018-11-01"
|
||||
pe.insert()
|
||||
@@ -357,10 +404,10 @@ def add_vouchers():
|
||||
pass
|
||||
|
||||
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
|
||||
pi.cash_bank_account = "_Test Bank - _TC"
|
||||
pi.cash_bank_account = gl_account
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.paid_amount = 690
|
||||
@@ -369,7 +416,7 @@ def add_vouchers():
|
||||
pe.submit()
|
||||
|
||||
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
|
||||
pe.reference_no = "Poore Simon's Oct 18"
|
||||
pe.reference_date = "2018-10-28"
|
||||
pe.insert()
|
||||
@@ -392,16 +439,12 @@ def add_vouchers():
|
||||
if not frappe.db.get_value(
|
||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||
):
|
||||
mode_of_payment.append(
|
||||
"accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
|
||||
)
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
|
||||
)
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -1,457 +1,152 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"beta": 0,
|
||||
"creation": "2018-06-18 16:51:49.994750",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"user",
|
||||
"date",
|
||||
"from_time",
|
||||
"time",
|
||||
"expense",
|
||||
"custody",
|
||||
"returns",
|
||||
"outstanding_amount",
|
||||
"payments",
|
||||
"net_amount",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "POS-CLO-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Series",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "POS-CLO-",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Today",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "From Time",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "time",
|
||||
"fieldtype": "Time",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "To Time",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0.00",
|
||||
"fieldname": "expense",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expense",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Expense"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0.00",
|
||||
"fieldname": "custody",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Custody",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Custody"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0.00",
|
||||
"fieldname": "returns",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Returns",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "2",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0.00",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Outstanding Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0.0",
|
||||
"fieldname": "payments",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Payments",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Cashier Closing Payments",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Cashier Closing Payments"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "net_amount",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Net Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Cashier Closing",
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-02-19 08:35:24.157327",
|
||||
"links": [],
|
||||
"modified": "2023-12-28 13:15:46.858427",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cashier Closing",
|
||||
"name_case": "",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"autoname": "field:year",
|
||||
"creation": "2013-01-22 16:50:25",
|
||||
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
|
||||
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@@ -82,10 +82,11 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -122,5 +123,6 @@
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "name",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -39,7 +39,7 @@ def test_record_generator():
|
||||
]
|
||||
|
||||
start = 2012
|
||||
end = now_datetime().year + 5
|
||||
end = now_datetime().year + 25
|
||||
for year in range(start, end):
|
||||
test_records.append(
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"against_voucher_type",
|
||||
"against_voucher",
|
||||
"voucher_type",
|
||||
"voucher_subtype",
|
||||
"voucher_no",
|
||||
"voucher_detail_no",
|
||||
"project",
|
||||
@@ -142,8 +143,7 @@
|
||||
"label": "Against Voucher Type",
|
||||
"oldfieldname": "against_voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher",
|
||||
@@ -162,8 +162,7 @@
|
||||
"label": "Voucher Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
@@ -280,13 +279,18 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Transaction Currency",
|
||||
"options": "transaction_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_subtype",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Voucher Subtype"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-16 21:38:44.072267",
|
||||
"modified": "2023-12-18 15:38:14.006208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -321,4 +325,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,9 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import (
|
||||
InvalidAccountCurrency,
|
||||
InvalidAccountDimensionError,
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@@ -39,6 +32,8 @@ class GLEntry(Document):
|
||||
account: DF.Link | None
|
||||
account_currency: DF.Link | None
|
||||
against: DF.Text | None
|
||||
against_link: DF.DynamicLink | None
|
||||
against_type: DF.Link | None
|
||||
against_voucher: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
company: DF.Link | None
|
||||
@@ -66,6 +61,7 @@ class GLEntry(Document):
|
||||
transaction_exchange_rate: DF.Float
|
||||
voucher_detail_no: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_subtype: DF.SmallText | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -95,7 +91,6 @@ class GLEntry(Document):
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
@@ -205,42 +200,6 @@ class GLEntry(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_dimensions(self):
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if self.account == account:
|
||||
if value["is_mandatory"] and not self.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
def check_pl_account(self):
|
||||
if (
|
||||
self.is_opening == "Yes"
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -150,6 +150,20 @@ class JournalEntry(AccountsController):
|
||||
if not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
|
||||
@@ -1,97 +1,94 @@
|
||||
[
|
||||
{
|
||||
"cheque_date": "2013-03-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Payable - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": "_Test Supplier",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
{
|
||||
"cheque_date": "2013-03-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "_Test Payable - _TC",
|
||||
"party_type": "Supplier",
|
||||
"party": "_Test Supplier",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
},
|
||||
|
||||
{
|
||||
"cheque_date": "2013-02-14",
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"account": "Sales - _TC",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": "2013-02-14",
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,173 +1,77 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:distribution_id",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-10 16:34:05",
|
||||
"custom": 0,
|
||||
"description": "**Monthly Distribution** helps you distribute the Budget/Target across months if you have seasonality in your business.",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "field:distribution_id",
|
||||
"creation": "2013-01-10 16:34:05",
|
||||
"description": "Helps you distribute the Budget/Target across months if you have seasonality in your business.",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"distribution_id",
|
||||
"fiscal_year",
|
||||
"percentages"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Name of the Monthly Distribution",
|
||||
"fieldname": "distribution_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Distribution Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "distribution_id",
|
||||
"oldfieldtype": "Data",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"description": "Name of the Monthly Distribution",
|
||||
"fieldname": "distribution_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Distribution Name",
|
||||
"oldfieldname": "distribution_id",
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Fiscal Year",
|
||||
"oldfieldname": "fiscal_year",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Fiscal Year",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "percentages",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Monthly Distribution Percentages",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "budget_distribution_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Monthly Distribution Percentage",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "percentages",
|
||||
"fieldtype": "Table",
|
||||
"label": "Monthly Distribution Percentages",
|
||||
"oldfieldname": "budget_distribution_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Monthly Distribution Percentage"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-bar-chart",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-11-21 14:54:35.998761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Monthly Distribution",
|
||||
"name_case": "Title Case",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-bar-chart",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 13:57:55.802744",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Monthly Distribution",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"is_custom": 0,
|
||||
"permlevel": 2,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"permlevel": 2,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
let fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
{fieldtype:"Date", label: __("From Date"),
|
||||
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
|
||||
@@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
fieldname:"outstanding_amt_greater_than", default: 0},
|
||||
{fieldtype:"Column Break"},
|
||||
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
}
|
||||
];
|
||||
|
||||
if (frm.dimension_filters) {
|
||||
let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
|
||||
|
||||
fields.push({fieldtype:"Section Break"});
|
||||
frm.dimension_filters.map((elem, idx)=>{
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
|
||||
options: elem.document_type,
|
||||
fieldname: elem.fieldname || elem.document_type
|
||||
});
|
||||
if(idx+1 == column_break_insertion_point) {
|
||||
fields.push({fieldtype:"Column Break"});
|
||||
}
|
||||
},
|
||||
{fieldtype:"Column Break"},
|
||||
});
|
||||
}
|
||||
|
||||
fields = fields.concat([
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
]);
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
@@ -747,6 +758,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
args["get_orders_to_be_billed"] = true;
|
||||
}
|
||||
|
||||
if (frm.doc.book_advance_payments_in_separate_party_account) {
|
||||
args["book_advance_payments_in_separate_party_account"] = true;
|
||||
}
|
||||
|
||||
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
||||
|
||||
return frappe.call({
|
||||
@@ -929,7 +944,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if(frm.doc.payment_type == "Receive"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions - flt(frm.doc.base_total_taxes_and_charges)
|
||||
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||
} else if (frm.doc.payment_type == "Pay"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
|
||||
|
||||
@@ -87,12 +87,14 @@
|
||||
"status",
|
||||
"custom_remarks",
|
||||
"remarks",
|
||||
"base_in_words",
|
||||
"column_break_16",
|
||||
"letter_head",
|
||||
"print_heading",
|
||||
"bank",
|
||||
"bank_account_no",
|
||||
"payment_order",
|
||||
"in_words",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
@@ -746,6 +748,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "In Words (Company Currency)",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "in_words",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "In Words",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -759,7 +775,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:07:20.887885",
|
||||
"modified": "2024-01-03 12:46:41.759121",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -13,6 +13,7 @@ from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||
get_bank_account_details,
|
||||
get_party_bank_account,
|
||||
@@ -95,6 +96,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
@@ -107,7 +109,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_liability_account(self):
|
||||
# Auto setting liability account should only be done during 'draft' status
|
||||
if self.docstatus > 0:
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
if not frappe.db.get_value(
|
||||
@@ -256,6 +258,7 @@ class PaymentEntry(AccountsController):
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
"vouchers": vouchers,
|
||||
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
||||
},
|
||||
validate=True,
|
||||
)
|
||||
@@ -701,6 +704,21 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.db_set("status", self.status, update_modified=True)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
||||
if self.payment_type in ("Pay", "Internal Transfer"):
|
||||
base_amount = abs(self.base_paid_amount)
|
||||
amount = abs(self.paid_amount)
|
||||
currency = self.paid_from_account_currency
|
||||
elif self.payment_type == "Receive":
|
||||
base_amount = abs(self.base_received_amount)
|
||||
amount = abs(self.received_amount)
|
||||
currency = self.paid_to_account_currency
|
||||
|
||||
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||
self.in_words = money_in_words(amount, currency)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if not self.party_type == "Supplier":
|
||||
return
|
||||
@@ -928,19 +946,19 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
|
||||
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
if self.payment_type == "Receive":
|
||||
self.difference_amount = base_party_amount - self.base_received_amount + included_taxes
|
||||
elif self.payment_type == "Pay":
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes
|
||||
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
|
||||
self.difference_amount = flt(
|
||||
self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
|
||||
self.difference_amount - total_deductions, self.precision("difference_amount")
|
||||
)
|
||||
|
||||
def get_included_taxes(self):
|
||||
@@ -1587,6 +1605,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||
"due_date": ["from_due_date", "to_due_date"],
|
||||
@@ -1614,11 +1639,16 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
outstanding_invoices = []
|
||||
negative_outstanding_invoices = []
|
||||
|
||||
if args.get("book_advance_payments_in_separate_party_account"):
|
||||
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
|
||||
else:
|
||||
party_account = args.get("party_account")
|
||||
|
||||
if args.get("get_outstanding_invoices"):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
get_party_account(args.get("party_type"), args.get("party"), args.get("company")),
|
||||
party_account,
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
@@ -1689,13 +1719,43 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
return data
|
||||
|
||||
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
invoice_ref_based_on_payment_terms = {}
|
||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
|
||||
"""Split a list of invoices based on their payment terms."""
|
||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
for entry in outstanding_invoices:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if payment_term_template := frappe.db.get_value(
|
||||
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
||||
):
|
||||
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
|
||||
if not split_rows:
|
||||
continue
|
||||
|
||||
if len(split_rows) > 1:
|
||||
frappe.msgprint(
|
||||
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
||||
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
outstanding_invoices_after_split += split_rows
|
||||
continue
|
||||
|
||||
# If not an invoice or no payment terms template, add as it is
|
||||
outstanding_invoices_after_split.append(entry)
|
||||
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
exc_rates = frappe._dict()
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
for x in frappe.db.get_all(
|
||||
@@ -1710,72 +1770,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
company_currency=company_currency,
|
||||
)
|
||||
|
||||
for idx, d in enumerate(outstanding_invoices):
|
||||
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
payment_term_template = frappe.db.get_value(
|
||||
d.voucher_type, d.voucher_no, "payment_terms_template"
|
||||
return exc_rates
|
||||
|
||||
|
||||
def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
|
||||
"""Split invoice based on its payment schedule table."""
|
||||
split_rows = []
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not allocate_payment_based_on_payment_terms:
|
||||
return [invoice]
|
||||
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
|
||||
)
|
||||
for payment_term in payment_schedule:
|
||||
if not payment_term.outstanding > 0.1:
|
||||
continue
|
||||
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
split_rows.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": invoice.due_date,
|
||||
"currency": invoice.currency,
|
||||
"voucher_no": invoice.voucher_no,
|
||||
"voucher_type": invoice.voucher_type,
|
||||
"posting_date": invoice.posting_date,
|
||||
"invoice_amount": flt(invoice.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else invoice.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
)
|
||||
if payment_term_template:
|
||||
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
|
||||
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
if allocate_payment_based_on_payment_terms:
|
||||
payment_schedule = frappe.get_all(
|
||||
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
|
||||
)
|
||||
)
|
||||
|
||||
for payment_term in payment_schedule:
|
||||
if payment_term.outstanding > 0.1:
|
||||
doc_details = exc_rates.get(payment_term.parent, None)
|
||||
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
|
||||
doc_details.party_account_currency != doc_details.company_currency
|
||||
)
|
||||
payment_term_outstanding = flt(payment_term.outstanding)
|
||||
if not is_multi_currency_acc:
|
||||
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
|
||||
|
||||
invoice_ref_based_on_payment_terms.setdefault(idx, [])
|
||||
invoice_ref_based_on_payment_terms[idx].append(
|
||||
frappe._dict(
|
||||
{
|
||||
"due_date": d.due_date,
|
||||
"currency": d.currency,
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
"account": d.account,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = ref[0]["voucher_no"]
|
||||
voucher_type = ref[0]["voucher_type"]
|
||||
|
||||
frappe.msgprint(
|
||||
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
return split_rows
|
||||
|
||||
|
||||
def get_orders_to_be_billed(
|
||||
@@ -1803,6 +1845,12 @@ def get_orders_to_be_billed(
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
rounded_total_field = "base_rounded_total"
|
||||
|
||||
@@ -6,11 +6,12 @@ import unittest
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_outstanding_reference_documents,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
)
|
||||
@@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
for field in ["account", "debit", "credit"]:
|
||||
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
|
||||
|
||||
def test_outstanding_invoices_api(self):
|
||||
"""
|
||||
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
|
||||
"""
|
||||
customer = create_customer("Max Mustermann", "INR")
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
|
||||
args = {
|
||||
"posting_date": nowdate(),
|
||||
"company": "_Test Company",
|
||||
"party_type": "Customer",
|
||||
"payment_type": "Pay",
|
||||
"party": customer,
|
||||
"party_account": "Debtors - _TC",
|
||||
}
|
||||
args.update(
|
||||
{
|
||||
"get_outstanding_invoices": True,
|
||||
"from_posting_date": add_days(nowdate(), -4),
|
||||
"to_posting_date": add_days(nowdate(), 2),
|
||||
}
|
||||
)
|
||||
references = get_outstanding_reference_documents(args)
|
||||
|
||||
self.assertEqual(len(references), 3)
|
||||
self.assertEqual(references[0].voucher_no, si.name)
|
||||
self.assertEqual(references[1].voucher_no, si2.name)
|
||||
self.assertEqual(references[2].voucher_no, si2.name)
|
||||
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
|
||||
self.assertEqual(references[2].payment_term, "Tax Receivable")
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1531,6 +1571,9 @@ def create_payment_terms_template():
|
||||
def create_payment_terms_template_with_discount(
|
||||
name=None, discount_type=None, discount=None, template_name=None
|
||||
):
|
||||
"""
|
||||
Create a Payment Terms Template with % or amount discount.
|
||||
"""
|
||||
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||
template_name = template_name or "Test Discount Template"
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
|
||||
create_bank_account,
|
||||
create_gl_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_payment_entry,
|
||||
make_payment_order,
|
||||
@@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
|
||||
class TestPaymentOrder(unittest.TestCase):
|
||||
class TestPaymentOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_bank_account()
|
||||
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
|
||||
uniq_identifier = frappe.generate_hash(length=10)
|
||||
self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
|
||||
self.bank_account = create_bank_account(
|
||||
gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for bt in frappe.get_all("Payment Order"):
|
||||
doc = frappe.get_doc("Payment Order", bt.name)
|
||||
doc.cancel()
|
||||
doc.delete()
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_payment_order_creation_against_payment_entry(self):
|
||||
purchase_invoice = make_purchase_invoice()
|
||||
payment_entry = get_payment_entry(
|
||||
"Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
|
||||
"Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
|
||||
)
|
||||
payment_entry.reference_no = "_Test_Payment_Order"
|
||||
payment_entry.reference_date = getdate()
|
||||
payment_entry.party_bank_account = "Checking Account - Citi Bank"
|
||||
payment_entry.party_bank_account = self.bank_account
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
||||
doc = create_payment_order_against_payment_entry(
|
||||
payment_entry, "Payment Entry", self.bank_account
|
||||
)
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
@@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
|
||||
self.assertEqual(reference_doc.amount, 250)
|
||||
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
|
||||
payment_order = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account="Checking Account - Citi Bank",
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
)
|
||||
doc = make_payment_order(ref_doc.name, payment_order)
|
||||
|
||||
@@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
|
||||
}
|
||||
|
||||
this.frm.trigger("set_query_for_dimension_filters");
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
this.frm.call({
|
||||
@@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
}
|
||||
set_query_for_dimension_filters() {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
|
||||
args: {
|
||||
company: this.frm.doc.company,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
r.message.forEach(x => {
|
||||
this.frm.set_query(x.fieldname, () => {
|
||||
return {
|
||||
'filters': x.filters
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
company() {
|
||||
this.frm.set_value('party', '');
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@@ -208,6 +210,18 @@
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Payment"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.invoices.length == 0",
|
||||
"depends_on": "eval:doc.receivable_payable_account",
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions Filter"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
@@ -215,7 +229,7 @@
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:55.701726",
|
||||
"modified": "2023-12-14 13:38:16.264013",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
@@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
self.dimensions = get_dimensions()[0]
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
@@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
|
||||
if self.payment_name:
|
||||
condition.update({"name": self.payment_name})
|
||||
|
||||
# pass dynamic dimension filter values to query builder
|
||||
dimensions = {}
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
dimensions.update({dimension: self.get(dimension)})
|
||||
condition.update({"accounting_dimensions": dimensions})
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
|
||||
return payment_entries
|
||||
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions = self.get_journal_filter_conditions()
|
||||
|
||||
# Dimension filters
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
conditions.append(jea[dimension] == self.get(dimension))
|
||||
|
||||
if self.payment_name:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
|
||||
bank_account_condition = (
|
||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
|
||||
journal_query = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("reference_type"),
|
||||
je.name.as_("reference_name"),
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
jea.cost_center.as_("cost_center"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.party_type == self.party_type)
|
||||
& (jea.party == self.party)
|
||||
& (jea.account == self.receivable_payable_account)
|
||||
& (
|
||||
(jea.reference_type == "")
|
||||
| (jea.reference_type.isnull())
|
||||
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
|
||||
)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(je.posting_date)
|
||||
)
|
||||
|
||||
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
|
||||
if self.payment_limit:
|
||||
journal_query = journal_query.limit(self.payment_limit)
|
||||
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||
t2.account_currency as currency, t2.cost_center as cost_center
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
|
||||
and t2.party_type = %(party_type)s and t2.party = %(party)s
|
||||
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
|
||||
and (t2.reference_type is null or t2.reference_type = '' or
|
||||
(t2.reference_type in ('Sales Order', 'Purchase Order')
|
||||
and t2.reference_name is not null and t2.reference_name != ''))
|
||||
and (CASE
|
||||
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
|
||||
THEN 1=1
|
||||
ELSE {bank_account_condition}
|
||||
END)
|
||||
order by t1.posting_date
|
||||
{limit}
|
||||
""".format(
|
||||
**{
|
||||
"dr_or_cr": dr_or_cr,
|
||||
"bank_account_condition": bank_account_condition,
|
||||
"condition": condition,
|
||||
"limit": limit,
|
||||
}
|
||||
),
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"account": self.receivable_payable_account,
|
||||
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
journal_entries = journal_query.run(as_dict=True)
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
@@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
for inv in return_outstanding:
|
||||
@@ -446,8 +458,15 @@ class PaymentReconciliation(Document):
|
||||
row = self.append("allocation", {})
|
||||
row.update(entry)
|
||||
|
||||
def update_dimension_values_in_allocated_entries(self, res):
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
res[dimension] = self.get(dimension)
|
||||
return res
|
||||
|
||||
def get_allocated_entry(self, pay, inv, allocated_amount):
|
||||
return frappe._dict(
|
||||
res = frappe._dict(
|
||||
{
|
||||
"reference_type": pay.get("reference_type"),
|
||||
"reference_name": pay.get("reference_name"),
|
||||
@@ -463,6 +482,9 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
res = self.update_dimension_values_in_allocated_entries(res)
|
||||
return res
|
||||
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
adjust_allocations_for_taxes(self)
|
||||
dr_or_cr = (
|
||||
@@ -485,10 +507,10 @@ class PaymentReconciliation(Document):
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
|
||||
|
||||
if dr_or_cr_notes:
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
@@ -517,7 +539,7 @@ class PaymentReconciliation(Document):
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
payment_details = frappe._dict(
|
||||
{
|
||||
"voucher_type": row.get("reference_type"),
|
||||
"voucher_no": row.get("reference_name"),
|
||||
@@ -539,6 +561,12 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
for x in self.dimensions:
|
||||
if row.get(x.fieldname):
|
||||
payment_details[x.fieldname] = row.get(x.fieldname)
|
||||
|
||||
return payment_details
|
||||
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
@@ -592,6 +620,27 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
if journals:
|
||||
journals = list(set(journals))
|
||||
journals_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"exchange_rate",
|
||||
],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
@@ -625,6 +674,13 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
frappe.throw(_("No records found in Allocation table"))
|
||||
|
||||
def build_dimensions_filter_conditions(self):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(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):
|
||||
self.common_filter_conditions.clear()
|
||||
self.accounting_dimension_filter_conditions.clear()
|
||||
@@ -648,40 +704,30 @@ class PaymentReconciliation(Document):
|
||||
if self.to_payment_date:
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
self.build_dimensions_filter_conditions()
|
||||
|
||||
if self.get("cost_center") and get_payments:
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
if self.from_payment_date:
|
||||
conditions.append(je.posting_date.gte(self.from_payment_date))
|
||||
|
||||
if self.to_payment_date:
|
||||
conditions.append(je.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
|
||||
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.lte(self.maximum_payment_amount))
|
||||
|
||||
return condition
|
||||
return conditions
|
||||
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
for inv in dr_cr_notes:
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
@@ -731,6 +777,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
}
|
||||
)
|
||||
|
||||
# Credit Note(JE) will inherit the same dimension values as payment
|
||||
dimensions_dict = frappe._dict()
|
||||
if active_dimensions:
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
|
||||
|
||||
jv.accounts[0].update(dimensions_dict)
|
||||
jv.accounts[1].update(dimensions_dict)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
jv.remark = None
|
||||
@@ -764,9 +819,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
inv.against_voucher,
|
||||
None,
|
||||
inv.cost_center,
|
||||
dimensions_dict,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def adjust_allocations_for_taxes(doc):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_queries_for_dimension_filters(company: str = None):
|
||||
dimensions_with_filters = []
|
||||
for d in get_dimensions()[0]:
|
||||
filters = {}
|
||||
meta = frappe.get_meta(d.document_type)
|
||||
if meta.has_field("company") and company:
|
||||
filters.update({"company": company})
|
||||
|
||||
if meta.is_tree:
|
||||
filters.update({"is_group": 0})
|
||||
|
||||
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
|
||||
|
||||
return dimensions_with_filters
|
||||
|
||||
@@ -591,6 +591,70 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_invoice_status_after_cr_note_cancellation(self):
|
||||
# This test case is made after the 'always standalone Credit/Debit notes' feature is introduced
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note.return_against = si.name
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"is_system_generated": 1,
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Credit Note",
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
self.assertEqual(len(journals), 1)
|
||||
|
||||
# assert status and outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Credit Note Issued")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
cr_note.reload()
|
||||
cr_note.cancel()
|
||||
# 'Credit Note' Journal should be auto cancelled
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"is_system_generated": 1,
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Credit Note",
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
self.assertEqual(len(journals), 0)
|
||||
# assert status and outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Unpaid")
|
||||
self.assertEqual(si.outstanding_amount, 100)
|
||||
|
||||
def test_cr_note_partial_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"cost_center"
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -151,12 +153,26 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:38.612615",
|
||||
"modified": "2023-12-14 13:38:26.104150",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
|
||||
unreconciled_amount: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
|
||||
remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -11,7 +11,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
SalesInvoice,
|
||||
get_bank_cash_account,
|
||||
get_mode_of_payment_info,
|
||||
update_multi_mode_option,
|
||||
)
|
||||
@@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_stock_availablility()
|
||||
self.validate_return_items_qty()
|
||||
self.set_status()
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.validate_pos()
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
@@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
|
||||
update_multi_mode_option(self, pos_profile)
|
||||
self.paid_amount = 0
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
for pay in self.payments:
|
||||
if not pay.account:
|
||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_payment_request(self):
|
||||
for pay in self.payments:
|
||||
@@ -765,7 +758,7 @@ def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.qty).as_("qty"))
|
||||
.select(Sum(p_item.stock_qty).as_("stock_qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
@@ -775,7 +768,7 @@ def get_pos_reserved_qty(item_code, warehouse):
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -80,13 +80,16 @@
|
||||
"target_warehouse",
|
||||
"quality_inspection",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"serial_no",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_tlhi",
|
||||
"serial_no",
|
||||
"column_break_ciit",
|
||||
"batch_no",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@@ -628,13 +631,13 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break5",
|
||||
@@ -649,14 +652,14 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_tax_rate",
|
||||
@@ -824,17 +827,33 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "section_break_tlhi",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ciit",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:33:22.585715",
|
||||
"modified": "2024-02-04 16:36:25.665743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
||||
@@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
|
||||
target_warehouse: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
weight_uom: DF.Link | None
|
||||
|
||||
@@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
get_pricing_rule_items,
|
||||
)
|
||||
|
||||
if isinstance(item_details, str):
|
||||
item_details = json.loads(item_details)
|
||||
item_details = frappe._dict(item_details)
|
||||
|
||||
for d in get_applied_pricing_rules(pricing_rules):
|
||||
if not d or not frappe.db.exists("Pricing Rule", d):
|
||||
continue
|
||||
|
||||
@@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
|
||||
values.extend(warehouses)
|
||||
|
||||
if items:
|
||||
condition = " and `tab{child_doc}`.{apply_on} in ({items})".format(
|
||||
condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
|
||||
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
|
||||
)
|
||||
|
||||
@@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if d.price_or_product_discount == "Price":
|
||||
if d.apply_discount_on:
|
||||
doc.set("apply_discount_on", d.apply_discount_on)
|
||||
# Variable to track whether the condition has been met
|
||||
condition_met = False
|
||||
|
||||
for field in ["additional_discount_percentage", "discount_amount"]:
|
||||
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
|
||||
@@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
if coupon_code_pricing_rule == d.name:
|
||||
# if selected coupon code is linked with pricing rule
|
||||
doc.set(field, d.get(pr_field))
|
||||
|
||||
# Set the condition_met variable to True and break out of the loop
|
||||
condition_met = True
|
||||
break
|
||||
|
||||
else:
|
||||
# reset discount if not linked
|
||||
doc.set(field, 0)
|
||||
@@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
doc.set(field, 0)
|
||||
|
||||
doc.calculate_taxes_and_totals()
|
||||
|
||||
# Break out of the main loop if the condition is met
|
||||
if condition_met:
|
||||
break
|
||||
elif d.price_or_product_discount == "Product":
|
||||
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
|
||||
get_product_discount_rule(d, item_details, doc=doc)
|
||||
|
||||
@@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None:
|
||||
# Update the parent doc about the exception
|
||||
frappe.db.rollback()
|
||||
|
||||
traceback = frappe.get_traceback()
|
||||
traceback = frappe.get_traceback(with_context=True)
|
||||
if traceback:
|
||||
message = "Traceback: <br>" + traceback
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"group_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -376,10 +377,16 @@
|
||||
"fieldname": "pdf_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "PDF Name"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-08-28 12:59:53.071334",
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
letter_head: DF.Link | None
|
||||
@@ -131,6 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
)
|
||||
|
||||
filters = get_common_filters(doc)
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
|
||||
@@ -35,7 +35,17 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
super.onload();
|
||||
|
||||
// Ignore linked advances
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = [
|
||||
"Journal Entry",
|
||||
"Payment Entry",
|
||||
"Purchase Invoice",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Accounting Ledger",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
@@ -163,6 +173,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
this.frm.doc.items.forEach((item) => {
|
||||
this.frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
|
||||
|
||||
@@ -396,6 +418,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
on_submit() {
|
||||
super.on_submit();
|
||||
|
||||
$.each(this.frm.doc["items"] || [], function(i, row) {
|
||||
if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt)
|
||||
})
|
||||
|
||||
@@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
self.set_percentage_received()
|
||||
|
||||
def set_percentage_received(self):
|
||||
total_billed_qty = 0.0
|
||||
total_received_qty = 0.0
|
||||
for row in self.items:
|
||||
if row.purchase_receipt and row.pr_detail and row.received_qty:
|
||||
total_billed_qty += row.qty
|
||||
total_received_qty += row.received_qty
|
||||
|
||||
if total_billed_qty and total_received_qty:
|
||||
self.per_received = total_received_qty / total_billed_qty * 100
|
||||
|
||||
def validate_release_date(self):
|
||||
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
|
||||
@@ -684,6 +696,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
@@ -1084,17 +1097,6 @@ class PurchaseInvoice(BuyingController):
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# update gross amount of asset bought through this document
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
|
||||
)
|
||||
|
||||
if (
|
||||
self.auto_accounting_for_stock
|
||||
and self.is_opening == "No"
|
||||
@@ -1134,12 +1136,25 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
|
||||
purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def make_stock_adjustment_entry(
|
||||
self, gl_entries, item, voucher_wise_stock_value, account_currency
|
||||
@@ -1816,10 +1831,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
|
||||
@@ -1227,11 +1227,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
unlink_enabled = frappe.db.get_single_value(
|
||||
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
|
||||
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
|
||||
|
||||
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
|
||||
frappe.db.set_value(
|
||||
@@ -1422,7 +1422,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
pay.cancel()
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
|
||||
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||
|
||||
@@ -1985,6 +1985,41 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
|
||||
|
||||
def test_debit_note_with_account_mismatch(self):
|
||||
new_creditors = create_account(
|
||||
parent_account="Accounts Payable - _TC",
|
||||
account_name="Creditors 2",
|
||||
company="_Test Company",
|
||||
account_type="Payable",
|
||||
)
|
||||
pi = make_purchase_invoice(qty=1, rate=1000)
|
||||
dr_note = make_purchase_invoice(
|
||||
qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True
|
||||
)
|
||||
dr_note.credit_to = new_creditors
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dr_note.save)
|
||||
|
||||
def test_debit_note_without_item(self):
|
||||
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
|
||||
pi.items[0].item_code = ""
|
||||
pi.save()
|
||||
|
||||
self.assertFalse(pi.items[0].item_code)
|
||||
pi.submit()
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
item_name="_Test Item",
|
||||
is_return=1,
|
||||
return_against=pi.name,
|
||||
qty=-10,
|
||||
do_not_save=True,
|
||||
)
|
||||
return_pi.items[0].item_code = ""
|
||||
return_pi.save()
|
||||
return_pi.submit()
|
||||
self.assertEqual(return_pi.docstatus, 1)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
@@ -2121,6 +2156,7 @@ def make_purchase_invoice(**args):
|
||||
"items",
|
||||
{
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"item_name": args.item_name,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 5,
|
||||
"received_qty": args.received_qty or 0,
|
||||
|
||||
@@ -62,15 +62,19 @@
|
||||
"rm_supp_cost",
|
||||
"warehouse_section",
|
||||
"warehouse",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_br_wh",
|
||||
"from_warehouse",
|
||||
"quality_inspection",
|
||||
"serial_and_batch_bundle",
|
||||
"serial_no",
|
||||
"col_br_wh",
|
||||
"rejected_warehouse",
|
||||
"rejected_serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"section_break_rqbe",
|
||||
"serial_no",
|
||||
"rejected_serial_no",
|
||||
"column_break_vbbb",
|
||||
"batch_no",
|
||||
"manufacture_details",
|
||||
"manufacturer",
|
||||
"column_break_13",
|
||||
@@ -288,7 +292,6 @@
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -440,13 +443,11 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -454,21 +455,18 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"read_only": 1
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"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",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting",
|
||||
@@ -891,7 +889,7 @@
|
||||
"label": "Apply TDS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -901,7 +899,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1",
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "rejected_serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@@ -914,12 +912,33 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "section_break_rqbe",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vbbb",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:26:05.629780",
|
||||
"modified": "2024-02-04 14:11:52.742228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
|
||||
stock_uom_rate: DF.Currency
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
@@ -230,7 +230,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-05 20:04:36.618240",
|
||||
"modified": "2024-01-14 10:04:36.618240",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
@@ -239,4 +239,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-01-10 16:34:08",
|
||||
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
|
||||
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@@ -77,7 +77,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-16 16:15:29.059370",
|
||||
"modified": "2024-01-30 13:08:09.537242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges Template",
|
||||
|
||||
@@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
if len(self.vouchers) > 5:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
@@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
|
||||
@@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
@@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
@@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
@@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
|
||||
traceback = frappe.get_traceback()
|
||||
traceback = frappe.get_traceback(with_context=True)
|
||||
if traceback:
|
||||
message = "Traceback: <br>" + traceback
|
||||
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
|
||||
|
||||
@@ -36,8 +36,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
var me = this;
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = [
|
||||
"POS Invoice",
|
||||
"Timesheet",
|
||||
"POS Invoice Merge Log",
|
||||
"POS Closing Entry",
|
||||
"Journal Entry",
|
||||
"Payment Entry",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Accounting Ledger",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@@ -197,6 +209,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
on_submit(doc, dt, dn) {
|
||||
var me = this;
|
||||
|
||||
super.on_submit();
|
||||
if (frappe.get_route()[0] != 'Form') {
|
||||
return
|
||||
}
|
||||
@@ -895,8 +908,8 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||
}
|
||||
});
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
async get_exchange_rate(frm, from_currency, to_currency) {
|
||||
|
||||
@@ -420,7 +420,8 @@ class SalesInvoice(SellingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def before_save(self):
|
||||
set_account_for_mode_of_payment(self)
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_pos_paid_amount()
|
||||
@@ -445,6 +446,7 @@ class SalesInvoice(SellingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -586,6 +588,8 @@ class SalesInvoice(SellingController):
|
||||
"Serial and Batch Bundle",
|
||||
)
|
||||
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if cint(self.update_stock):
|
||||
self.status_updater.append(
|
||||
@@ -704,9 +708,6 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update(self):
|
||||
self.set_paid_amount()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
@@ -737,6 +738,11 @@ class SalesInvoice(SellingController):
|
||||
self.paid_amount = paid_amount
|
||||
self.base_paid_amount = base_paid_amount
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
for payment in self.payments:
|
||||
if not payment.account:
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||
|
||||
def validate_time_sheets_are_submitted(self):
|
||||
for data in self.timesheets:
|
||||
if data.time_sheet:
|
||||
@@ -2105,12 +2111,6 @@ def make_sales_return(source_name, target_doc=None):
|
||||
return make_return_doc("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
for data in self.payments:
|
||||
if not data.account:
|
||||
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
|
||||
|
||||
|
||||
def get_inter_company_details(doc, doctype):
|
||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
||||
parties = frappe.db.get_all(
|
||||
@@ -2356,9 +2356,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
|
||||
filters = {
|
||||
reference_field: reference_name,
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
@@ -2540,10 +2549,6 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -1414,10 +1414,11 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
def test_serialized_cancel(self):
|
||||
si = self.test_serialized()
|
||||
si.cancel()
|
||||
|
||||
si.reload()
|
||||
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
|
||||
|
||||
si.cancel()
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
|
||||
)
|
||||
@@ -1532,6 +1533,19 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
||||
|
||||
def test_return_invoice_with_account_mismatch(self):
|
||||
debtors2 = create_account(
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
account_name="Debtors 2",
|
||||
company="_Test Company",
|
||||
account_type="Receivable",
|
||||
)
|
||||
si = create_sales_invoice(qty=1, rate=1000)
|
||||
cr_note = create_sales_invoice(
|
||||
qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, cr_note.save)
|
||||
|
||||
def test_gle_made_when_asset_is_returned(self):
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro")
|
||||
@@ -2799,6 +2813,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
|
||||
@@ -81,15 +81,19 @@
|
||||
"warehouse",
|
||||
"target_warehouse",
|
||||
"quality_inspection",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"incoming_rate",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"serial_no",
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_eoec",
|
||||
"serial_no",
|
||||
"column_break_ytgd",
|
||||
"batch_no",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@@ -599,12 +603,11 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -620,13 +623,12 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
@@ -890,6 +892,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -897,12 +900,33 @@
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1",
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "section_break_eoec",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ytgd",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:34:10.479329",
|
||||
"modified": "2024-02-04 11:52:16.106541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
|
||||
target_warehouse: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
warehouse: DF.Link | None
|
||||
weight_per_unit: DF.Float
|
||||
weight_uom: DF.Link | None
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"default",
|
||||
"mode_of_payment",
|
||||
"amount",
|
||||
"reference_no",
|
||||
"column_break_3",
|
||||
"account",
|
||||
"type",
|
||||
@@ -75,11 +76,16 @@
|
||||
"hidden": 1,
|
||||
"label": "Default",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference No"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 12:45:39.986598",
|
||||
"modified": "2024-01-23 16:20:06.436979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
@@ -87,5 +93,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_no: DF.Data | None
|
||||
type: DF.ReadOnly | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
@@ -218,7 +218,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-17 13:08:17.776528",
|
||||
"modified": "2024-01-14 10:08:17.776528",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges",
|
||||
@@ -227,4 +227,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-01-10 16:34:09",
|
||||
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.",
|
||||
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
@@ -79,7 +79,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-16 16:14:52.061672",
|
||||
"modified": "2024-01-30 13:07:28.801104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges Template",
|
||||
|
||||
@@ -148,13 +148,13 @@
|
||||
{
|
||||
"fieldname": "additional_discount_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Additional DIscount Percentage"
|
||||
"label": "Additional Discount Percentage"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional DIscount Amount"
|
||||
"label": "Additional Discount Amount"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -267,7 +267,7 @@
|
||||
"link_fieldname": "subscription"
|
||||
}
|
||||
],
|
||||
"modified": "2023-09-18 17:48:21.900252",
|
||||
"modified": "2023-12-28 17:20:42.687789",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
||||
@@ -16,6 +16,7 @@ from frappe.utils.data import (
|
||||
date_diff,
|
||||
flt,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
@@ -317,6 +318,37 @@ class Subscription(Document):
|
||||
if self.is_new():
|
||||
self.set_subscription_status()
|
||||
|
||||
self.validate_party_billing_currency()
|
||||
|
||||
def validate_party_billing_currency(self):
|
||||
"""
|
||||
Subscription should be of the same currency as the Party's default billing currency or company default.
|
||||
"""
|
||||
if self.party:
|
||||
party_billing_currency = frappe.get_cached_value(
|
||||
self.party_type, self.party, "default_currency"
|
||||
) or frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
|
||||
plans = [x.plan for x in self.plans]
|
||||
subscription_plan_currencies = frappe.db.get_all(
|
||||
"Subscription Plan", filters={"name": ("in", plans)}, fields=["name", "currency"]
|
||||
)
|
||||
unsupported_plans = []
|
||||
for x in subscription_plan_currencies:
|
||||
if x.currency != party_billing_currency:
|
||||
unsupported_plans.append("{0}".format(get_link_to_form("Subscription Plan", x.name)))
|
||||
|
||||
if unsupported_plans:
|
||||
unsupported_plans = [
|
||||
_(
|
||||
"Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}"
|
||||
).format(frappe.bold(party_billing_currency))
|
||||
] + unsupported_plans
|
||||
|
||||
frappe.throw(
|
||||
unsupported_plans, frappe.ValidationError, "Unsupported Subscription Plans", as_list=True
|
||||
)
|
||||
|
||||
def validate_trial_period(self) -> None:
|
||||
"""
|
||||
Runs sanity checks on trial period dates for the `Subscription`
|
||||
@@ -356,18 +388,20 @@ class Subscription(Document):
|
||||
self,
|
||||
from_date: Optional[Union[str, datetime.date]] = None,
|
||||
to_date: Optional[Union[str, datetime.date]] = None,
|
||||
posting_date: Optional[Union[str, datetime.date]] = None,
|
||||
) -> Document:
|
||||
"""
|
||||
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
|
||||
saves the `Subscription`.
|
||||
Backwards compatibility
|
||||
"""
|
||||
return self.create_invoice(from_date=from_date, to_date=to_date)
|
||||
return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date)
|
||||
|
||||
def create_invoice(
|
||||
self,
|
||||
from_date: Optional[Union[str, datetime.date]] = None,
|
||||
to_date: Optional[Union[str, datetime.date]] = None,
|
||||
posting_date: Optional[Union[str, datetime.date]] = None,
|
||||
) -> Document:
|
||||
"""
|
||||
Creates a `Invoice`, submits it and returns it
|
||||
@@ -385,11 +419,13 @@ class Subscription(Document):
|
||||
invoice = frappe.new_doc(self.invoice_document_type)
|
||||
invoice.company = company
|
||||
invoice.set_posting_time = 1
|
||||
invoice.posting_date = (
|
||||
self.current_invoice_start
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period"
|
||||
else self.current_invoice_end
|
||||
)
|
||||
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period":
|
||||
invoice.posting_date = self.current_invoice_start
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
invoice.posting_date = posting_date or self.current_invoice_start
|
||||
else:
|
||||
invoice.posting_date = self.current_invoice_end
|
||||
|
||||
invoice.cost_center = self.cost_center
|
||||
|
||||
@@ -413,6 +449,7 @@ class Subscription(Document):
|
||||
# Subscription is better suited for service items. I won't update `update_stock`
|
||||
# for that reason
|
||||
items_list = self.get_items_from_plans(self.plans, is_prorate())
|
||||
|
||||
for item in items_list:
|
||||
item["cost_center"] = self.cost_center
|
||||
invoice.append("items", item)
|
||||
@@ -556,8 +593,10 @@ class Subscription(Document):
|
||||
if not self.is_current_invoice_generated(
|
||||
self.current_invoice_start, self.current_invoice_end
|
||||
) and self.can_generate_new_invoice(posting_date):
|
||||
self.generate_invoice()
|
||||
self.generate_invoice(posting_date=posting_date)
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period()
|
||||
|
||||
if self.cancel_at_period_end and (
|
||||
getdate(posting_date) >= getdate(self.current_invoice_end)
|
||||
|
||||
@@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase):
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multi_currency_subscription(self):
|
||||
party = "_Test Subscription Customer"
|
||||
frappe.db.set_value("Customer", party, "default_currency", "USD")
|
||||
subscription = create_subscription(
|
||||
start_date="2018-01-01",
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
|
||||
party="_Test Subscription Customer",
|
||||
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process()
|
||||
@@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase):
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900)
|
||||
create_plan(plan_name="_Test Plan Name 2", cost=1999)
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR")
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14
|
||||
plan_name="_Test Plan Name 3",
|
||||
cost=1999,
|
||||
billing_interval="Day",
|
||||
billing_interval_count=14,
|
||||
currency="INR",
|
||||
)
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3
|
||||
plan_name="_Test Plan Name 4",
|
||||
cost=20000,
|
||||
billing_interval="Month",
|
||||
billing_interval_count=3,
|
||||
currency="INR",
|
||||
)
|
||||
create_plan(
|
||||
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"options": "Currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -148,10 +149,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-12-10 15:24:15.794477",
|
||||
"modified": "2024-01-14 17:59:34.687977",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -193,5 +195,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class SubscriptionPlan(Document):
|
||||
billing_interval_count: DF.Int
|
||||
cost: DF.Currency
|
||||
cost_center: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
currency: DF.Link
|
||||
item: DF.Link
|
||||
payment_gateway: DF.Link | None
|
||||
plan_name: DF.Data
|
||||
|
||||
@@ -13,9 +13,13 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
def make_gl_entries(
|
||||
@@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
|
||||
process_debit_credit_difference(gl_map)
|
||||
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
if gl_map:
|
||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
||||
@@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||
|
||||
for entry in gl_map:
|
||||
validate_allowed_dimensions(entry, dimension_filter_map)
|
||||
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
||||
|
||||
|
||||
@@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no):
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, voucher_type, voucher_no),
|
||||
)
|
||||
|
||||
|
||||
def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if gl_entry.account == account:
|
||||
if value["is_mandatory"] and not gl_entry.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(gl_entry.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(gl_entry.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
@@ -114,14 +114,12 @@ def _get_party_details(
|
||||
set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)
|
||||
)
|
||||
party = party_details[party_type.lower()]
|
||||
|
||||
if not ignore_permissions and not (
|
||||
frappe.has_permission(party_type, "read", party)
|
||||
or frappe.has_permission(party_type, "select", party)
|
||||
):
|
||||
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
|
||||
|
||||
party = frappe.get_doc(party_type, party)
|
||||
|
||||
if not ignore_permissions:
|
||||
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
|
||||
frappe.has_permission(party_type, ptype, party, throw=True)
|
||||
|
||||
currency = party.get("default_currency") or currency or get_company_currency(company)
|
||||
|
||||
party_address, shipping_address = set_address_details(
|
||||
@@ -637,9 +635,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
||||
return due_date
|
||||
|
||||
|
||||
def validate_due_date(
|
||||
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
|
||||
):
|
||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
|
||||
if getdate(due_date) < getdate(posting_date):
|
||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||
else:
|
||||
|
||||
@@ -153,8 +153,12 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2>
|
||||
<h4 class="text-center">
|
||||
{% if (filters.customer_name) { %}
|
||||
{%= filters.customer_name %}
|
||||
{% } else { %}
|
||||
{%= filters.customer || filters.supplier %}
|
||||
{% if (filters.party) { %}
|
||||
{%= __(filters.party) %}
|
||||
{% } %}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
@@ -141,7 +139,7 @@
|
||||
<th style="width: 24%">{%= __("Reference") %}</th>
|
||||
{% } %}
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
<th style="width: 20%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 20%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
|
||||
{% } %}
|
||||
<th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th>
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
@@ -158,7 +156,7 @@
|
||||
<th style="width: 10%">{%= __("Remaining Balance") %}</th>
|
||||
{% } %}
|
||||
{% } else { %}
|
||||
<th style="width: 40%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 40%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 15%">{%= __("Total Invoiced Amount") %}</th>
|
||||
<th style="width: 15%">{%= __("Total Paid Amount") %}</th>
|
||||
<th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th>
|
||||
@@ -187,7 +185,7 @@
|
||||
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
<td>
|
||||
{% if(!(filters.customer || filters.supplier)) { %}
|
||||
{% if(!(filters.party)) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
@@ -260,7 +258,7 @@
|
||||
{% if(data[i]["party"]|| " ") { %}
|
||||
{% if(!data[i]["is_total_row"]) { %}
|
||||
<td>
|
||||
{% if(!(filters.customer || filters.supplier)) { %}
|
||||
{% if(!(filters.party)) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
|
||||
@@ -185,9 +185,12 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
67
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
67
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
@@ -5,7 +5,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision, get_party_types_from_
|
||||
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
|
||||
# 7. For overpayment against an invoice with payment terms, there will be an additional row
|
||||
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
|
||||
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
|
||||
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
|
||||
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
|
||||
# 10. This report is based on Payment Ledger Entries
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -82,6 +82,9 @@ class ReceivablePayableReport(object):
|
||||
self.total_row_map = {}
|
||||
self.skip_total_row = 1
|
||||
|
||||
if self.filters.get("in_party_currency"):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
@@ -143,7 +146,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, ple):
|
||||
@@ -222,8 +225,7 @@ class ReceivablePayableReport(object):
|
||||
if not row:
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
@@ -242,8 +244,12 @@ class ReceivablePayableReport(object):
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
if self.is_invoice(ple):
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
@@ -254,8 +260,10 @@ class ReceivablePayableReport(object):
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
if total_row:
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
total_row["currency"] = row.get("currency", "")
|
||||
|
||||
def append_subtotal_row(self, party):
|
||||
sub_total_row = self.total_row_map.get(party)
|
||||
@@ -316,7 +324,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.append_subtotal_row(self.previous_party)
|
||||
if self.data:
|
||||
self.data.append(self.total_row_map.get("Total"))
|
||||
self.data.append(self.total_row_map.get("Total", {}))
|
||||
|
||||
def append_row(self, row):
|
||||
self.allocate_future_payments(row)
|
||||
@@ -447,7 +455,7 @@ class ReceivablePayableReport(object):
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
@@ -568,6 +576,8 @@ class ReceivablePayableReport(object):
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.inner_join(pe_ref)
|
||||
@@ -579,6 +589,11 @@ class ReceivablePayableReport(object):
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
ifelse(
|
||||
pe.payment_type == "Receive",
|
||||
pe.source_exchange_rate * pe_ref.allocated_amount,
|
||||
pe.target_exchange_rate * pe_ref.allocated_amount,
|
||||
).as_("future_amount_in_base_currency"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
@@ -615,13 +630,24 @@ class ReceivablePayableReport(object):
|
||||
query = query.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
|
||||
"future_amount_in_base_currency"
|
||||
)
|
||||
)
|
||||
query = query.select(
|
||||
Sum(
|
||||
jea.debit_in_account_currency
|
||||
if self.account_type == "Payable"
|
||||
else jea.credit_in_account_currency
|
||||
).as_("future_amount")
|
||||
)
|
||||
|
||||
query = query.having(qb.Field("future_amount") > 0)
|
||||
@@ -637,14 +663,19 @@ class ReceivablePayableReport(object):
|
||||
row.remaining_balance = row.outstanding
|
||||
row.future_amount = 0.0
|
||||
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
||||
if row.remaining_balance > 0 and future.future_amount:
|
||||
if future.future_amount > row.outstanding:
|
||||
if self.filters.in_party_currency:
|
||||
future_amount_field = "future_amount"
|
||||
else:
|
||||
future_amount_field = "future_amount_in_base_currency"
|
||||
|
||||
if row.remaining_balance > 0 and future.get(future_amount_field):
|
||||
if future.get(future_amount_field) > row.outstanding:
|
||||
row.future_amount = row.outstanding
|
||||
future.future_amount = future.future_amount - row.outstanding
|
||||
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
|
||||
row.remaining_balance = 0
|
||||
else:
|
||||
row.future_amount += future.future_amount
|
||||
future.future_amount = 0
|
||||
row.future_amount += future.get(future_amount_field)
|
||||
future[future_amount_field] = 0
|
||||
row.remaining_balance = row.outstanding - row.future_amount
|
||||
|
||||
row.setdefault("future_ref", []).append(
|
||||
|
||||
@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
return credit_note
|
||||
|
||||
def test_pos_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": add_days(today(), 2),
|
||||
"based_on_payment_terms": 0,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": False,
|
||||
}
|
||||
|
||||
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
pos_inv.posting_date = add_days(today(), 2)
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
frappe._dict(
|
||||
mode_of_payment="Cash",
|
||||
amount=flt(pos_inv.grand_total / 2),
|
||||
),
|
||||
)
|
||||
pos_inv.disable_rounded_total = 1
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
report = execute(filters)
|
||||
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
|
||||
|
||||
row = report[1][-1]
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
@@ -544,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
@@ -581,6 +616,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
@@ -736,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||
report_output = sorted(report_output, key=lambda x: x[0])
|
||||
self.assertEqual(expected_data, report_output)
|
||||
|
||||
def test_future_payments_on_foreign_currency(self):
|
||||
self.customer2 = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.posting_date = add_days(today(), -1)
|
||||
si.customer = self.customer2
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.debit_to = self.debtors_usd
|
||||
si.save().submit()
|
||||
|
||||
# full payment in USD
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.base_received_amount = 7500
|
||||
pe.received_amount = 7500
|
||||
pe.source_exchange_rate = 75
|
||||
pe.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_future_payments": True,
|
||||
"in_party_currency": False,
|
||||
}
|
||||
)
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
|
||||
expected_data = [8000.0, 8000.0, 500.0, 7500.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
filters.in_party_currency = True
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, 0.0, 100.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
pe.cancel()
|
||||
# partial payment in USD on a future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
pe.base_received_amount = 6750
|
||||
pe.received_amount = 6750
|
||||
pe.source_exchange_rate = 75
|
||||
pe.paid_amount = 90 # in USD
|
||||
pe.references[0].allocated_amount = 90
|
||||
pe.save().submit()
|
||||
|
||||
filters.in_party_currency = False
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
filters.in_party_currency = True
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [100.0, 100.0, 10.0, 90.0]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"group_by",
|
||||
"label": __("Group By"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Asset Category", "Asset"],
|
||||
"default": "Asset Category",
|
||||
},
|
||||
{
|
||||
"fieldname":"asset_category",
|
||||
"label": __("Asset Category"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category"
|
||||
}
|
||||
"options": "Asset Category",
|
||||
"depends_on": "eval: doc.group_by == 'Asset Category'",
|
||||
},
|
||||
{
|
||||
"fieldname":"asset",
|
||||
"label": __("Asset"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset",
|
||||
"depends_on": "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,10 +14,17 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
return get_group_by_asset_category_data(filters)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
return get_group_by_asset_data(filters)
|
||||
|
||||
|
||||
def get_group_by_asset_category_data(filters):
|
||||
data = []
|
||||
|
||||
asset_categories = get_asset_categories(filters)
|
||||
assets = get_assets(filters)
|
||||
asset_categories = get_asset_categories_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_category(filters)
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
@@ -38,6 +45,7 @@ def get_data(filters):
|
||||
if asset["asset_category"] == asset_category.get("asset_category", "")
|
||||
)
|
||||
)
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
@@ -57,7 +65,7 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_categories(filters):
|
||||
def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
@@ -116,7 +124,105 @@ def get_asset_categories(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets(filters):
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and name = %(asset)s"
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT name,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
|
||||
group by name
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
# row.asset_category = asset_category
|
||||
row.update(asset_detail)
|
||||
|
||||
row.cost_as_on_to_date = (
|
||||
flt(row.cost_as_on_from_date)
|
||||
+ flt(row.cost_of_new_purchase)
|
||||
- flt(row.cost_of_sold_asset)
|
||||
- flt(row.cost_of_scrapped_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
@@ -178,15 +284,93 @@ def get_assets(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = " and a.name = '{}'".format(filters.get("asset"))
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
group by a.name) as results
|
||||
group by results.name
|
||||
""".format(
|
||||
condition
|
||||
),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
},
|
||||
columns = []
|
||||
|
||||
if filters.get("group_by") == "Asset Category":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Category"),
|
||||
"fieldname": "asset_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset Category",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
elif filters.get("group_by") == "Asset":
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset"),
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "cost_as_on_from_date",
|
||||
@@ -254,3 +438,5 @@ def get_columns(filters):
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
@@ -8,6 +8,20 @@ frappe.query_reports["Balance Sheet"] = $.extend(
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||
{
|
||||
"fieldname": "selected_view",
|
||||
"label": __("Select View"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Report", "label": __("Report View") },
|
||||
{ "value": "Growth", "label": __("Growth View") }
|
||||
],
|
||||
"default": "Report",
|
||||
"reqd": 1
|
||||
},
|
||||
);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
|
||||
@@ -2,7 +2,44 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Budget Variance Report"] = {
|
||||
"filters": [
|
||||
"filters": get_filters(),
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname.includes(__("variance"))) {
|
||||
|
||||
if (data[column.fieldname] < 0) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
else if (data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
function get_filters() {
|
||||
function get_dimensions() {
|
||||
let result = [];
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
|
||||
args: {
|
||||
'with_cost_center_and_project': true
|
||||
},
|
||||
async: false,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
result = r.message[0].map(elem => elem.document_type);
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
let budget_against_options = get_dimensions();
|
||||
|
||||
let filters = [
|
||||
{
|
||||
fieldname: "from_fiscal_year",
|
||||
label: __("From Fiscal Year"),
|
||||
@@ -44,7 +81,7 @@ frappe.query_reports["Budget Variance Report"] = {
|
||||
fieldname: "budget_against",
|
||||
label: __("Budget Against"),
|
||||
fieldtype: "Select",
|
||||
options: ["Cost Center", "Project"],
|
||||
options: budget_against_options,
|
||||
default: "Cost Center",
|
||||
reqd: 1,
|
||||
on_change: function() {
|
||||
@@ -71,24 +108,8 @@ frappe.query_reports["Budget Variance Report"] = {
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
]
|
||||
|
||||
if (column.fieldname.includes(__("variance"))) {
|
||||
|
||||
if (data[column.fieldname] < 0) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
else if (data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
erpnext.dimension_filters.forEach((dimension) => {
|
||||
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ frappe.query_reports["Consolidated Financial Statement"] = {
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (!data.parent_account) {
|
||||
if (data && !data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object):
|
||||
"""
|
||||
Additional Columns for 'User Permission' based access control
|
||||
"""
|
||||
from frappe import qb
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
self.territories = frappe._dict({})
|
||||
@@ -365,13 +364,33 @@ class PartyLedgerSummaryReport(object):
|
||||
|
||||
def get_party_adjustment_amounts(self):
|
||||
conditions = self.prepare_conditions()
|
||||
income_or_expense = (
|
||||
"Expense Account" if self.filters.party_type == "Customer" else "Income Account"
|
||||
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
|
||||
income_or_expense_accounts = frappe.db.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
|
||||
)
|
||||
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
|
||||
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
if not income_or_expense_accounts:
|
||||
# prevent empty 'in' condition
|
||||
income_or_expense_accounts.append("")
|
||||
else:
|
||||
# escape '%' in account name
|
||||
# ignoring frappe.db.escape as it replaces single quotes with double quotes
|
||||
income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts]
|
||||
|
||||
accounts_query = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_type, gl.voucher_no)
|
||||
.where(
|
||||
(gl.account.isin(income_or_expense_accounts))
|
||||
& (gl.posting_date.gte(self.filters.from_date))
|
||||
& (gl.posting_date.lte(self.filters.to_date))
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
@@ -381,16 +400,15 @@ class PartyLedgerSummaryReport(object):
|
||||
where
|
||||
docstatus < 2 and is_cancelled = 0
|
||||
and (voucher_type, voucher_no) in (
|
||||
select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
|
||||
where acc.name = gle.account and acc.account_type = '{income_or_expense}'
|
||||
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2
|
||||
{accounts_query}
|
||||
) and (voucher_type, voucher_no) in (
|
||||
select voucher_type, voucher_no from `tabGL Entry` gle
|
||||
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
|
||||
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
|
||||
)
|
||||
""".format(
|
||||
conditions=conditions, income_or_expense=income_or_expense
|
||||
""".format(
|
||||
accounts_query=accounts_query,
|
||||
conditions=conditions,
|
||||
),
|
||||
self.filters,
|
||||
as_dict=True,
|
||||
@@ -414,7 +432,7 @@ class PartyLedgerSummaryReport(object):
|
||||
elif gle.party:
|
||||
parties.setdefault(gle.party, 0)
|
||||
parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr)
|
||||
elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense:
|
||||
elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type:
|
||||
accounts.setdefault(gle.account, 0)
|
||||
accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
|
||||
else:
|
||||
|
||||
@@ -177,8 +177,8 @@ def add_solvency_ratios(
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = total_income[year] + total_expense[year]
|
||||
share_holder_fund = total_asset[year] - total_liability[year]
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
|
||||
|
||||
debt_equity_ratio[year] = calculate_ratio(
|
||||
total_liability.get(year), share_holder_fund, precision
|
||||
|
||||
@@ -199,7 +199,13 @@ def get_data(
|
||||
ignore_accumulated_values_for_fy,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
|
||||
out = prepare_data(accounts, balance_must_be, period_list, company_currency)
|
||||
out = prepare_data(
|
||||
accounts,
|
||||
balance_must_be,
|
||||
period_list,
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
|
||||
if out and total:
|
||||
@@ -258,7 +264,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list):
|
||||
) + d.get("opening_balance", 0.0)
|
||||
|
||||
|
||||
def prepare_data(accounts, balance_must_be, period_list, company_currency):
|
||||
def prepare_data(accounts, balance_must_be, period_list, company_currency, accumulated_values):
|
||||
data = []
|
||||
year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d")
|
||||
year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
|
||||
@@ -298,8 +304,14 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency):
|
||||
has_value = True
|
||||
total += flt(row[period.key])
|
||||
|
||||
row["has_value"] = has_value
|
||||
row["total"] = total
|
||||
if accumulated_values:
|
||||
# when 'accumulated_values' is enabled, periods have running balance.
|
||||
# so, last period will have the net amount.
|
||||
row["has_value"] = has_value
|
||||
row["total"] = flt(d.get(period_list[-1].key, 0.0), 3)
|
||||
else:
|
||||
row["has_value"] = has_value
|
||||
row["total"] = total
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
@@ -52,6 +52,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"against_voucher_no",
|
||||
"label": __("Against Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{
|
||||
"fieldtype": "Break",
|
||||
},
|
||||
@@ -198,8 +203,14 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_err",
|
||||
"label": __("Ignore Exchange Rate Revaluation Journals"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
"""
|
||||
select
|
||||
name as gl_entry, posting_date, account, party_type, party,
|
||||
voucher_type, voucher_no, {dimension_fields}
|
||||
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
||||
cost_center, project, {transaction_currency_fields}
|
||||
against_voucher_type, against_voucher, account_currency,
|
||||
against, is_opening, creation {select_fields}
|
||||
@@ -238,6 +238,25 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("against_voucher_no"):
|
||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
||||
|
||||
if filters.get("ignore_err"):
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
@@ -605,6 +624,12 @@ def get_columns(filters):
|
||||
|
||||
columns += [
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
|
||||
{
|
||||
"label": _("Voucher Subtype"),
|
||||
"fieldname": "voucher_subtype",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
"fieldname": "voucher_no",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
|
||||
@@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
self.assertEqual(data[2]["credit"], 900)
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
company = "_Test Company"
|
||||
account = frappe.get_doc(
|
||||
{
|
||||
"account_name": account_name,
|
||||
"is_group": 0,
|
||||
"company": company,
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"account_currency": "USD",
|
||||
"parent_account": "Accounts Receivable - _TC",
|
||||
"account_type": "Receivable",
|
||||
"doctype": "Account",
|
||||
}
|
||||
)
|
||||
account.insert(ignore_if_duplicate=True)
|
||||
# create a JV to debit 1000 USD at 75 exchange rate
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = today()
|
||||
jv.company = company
|
||||
jv.multi_currency = 1
|
||||
jv.cost_center = "_Test Cost Center - _TC"
|
||||
jv.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": account.name,
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer USD",
|
||||
"debit_in_account_currency": 1000,
|
||||
"credit_in_account_currency": 0,
|
||||
"exchange_rate": 75,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
{
|
||||
"account": "Cash - _TC",
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 75000,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
],
|
||||
)
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
revaluation = frappe.new_doc("Exchange Rate Revaluation")
|
||||
revaluation.posting_date = today()
|
||||
revaluation.company = company
|
||||
accounts = revaluation.get_accounts_data()
|
||||
revaluation.extend("accounts", accounts)
|
||||
row = revaluation.accounts[0]
|
||||
row.new_exchange_rate = 83
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
revaluation.set_total_gain_loss()
|
||||
revaluation = revaluation.save().submit()
|
||||
|
||||
# post journal entry for Revaluation doc
|
||||
frappe.db.set_value(
|
||||
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
revaluation_jv = revaluation.make_jv_for_revaluation()
|
||||
revaluation_jv.cost_center = "_Test Cost Center - _TC"
|
||||
for acc in revaluation_jv.get("accounts"):
|
||||
acc.cost_center = "_Test Cost Center - _TC"
|
||||
revaluation_jv.save()
|
||||
revaluation_jv.submit()
|
||||
|
||||
# With ignore_err enabled
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_err": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
# Without ignore_err enabled
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_err": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
@@ -134,7 +134,7 @@ def get_revenue(data, period_list, include_in_gross=1):
|
||||
|
||||
def remove_parent_with_no_child(data):
|
||||
data_to_be_removed = False
|
||||
for parent in data:
|
||||
for parent in list(data):
|
||||
if "is_group" in parent and parent.get("is_group") == 1:
|
||||
have_child = False
|
||||
for child in data:
|
||||
|
||||
@@ -8,6 +8,21 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend(
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
"fieldname": "selected_view",
|
||||
"label": __("Select View"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Report", "label": __("Report View") },
|
||||
{ "value": "Growth", "label": __("Growth View") },
|
||||
{ "value": "Margin", "label": __("Margin View") },
|
||||
],
|
||||
"default": "Report",
|
||||
"reqd": 1
|
||||
},
|
||||
);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
|
||||
@@ -82,14 +82,25 @@ def get_report_summary(
|
||||
if filters.get("accumulated_in_group_company"):
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
if filters.accumulated_values:
|
||||
# when 'accumulated_values' is enabled, periods have running balance.
|
||||
# so, last period will have the net amount.
|
||||
key = period_list[-1].key
|
||||
if income:
|
||||
net_income += income[-2].get(key)
|
||||
net_income = income[-2].get(key)
|
||||
if expense:
|
||||
net_expense += expense[-2].get(key)
|
||||
net_expense = expense[-2].get(key)
|
||||
if net_profit_loss:
|
||||
net_profit += net_profit_loss.get(key)
|
||||
net_profit = net_profit_loss.get(key)
|
||||
else:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
if income:
|
||||
net_income += income[-2].get(key)
|
||||
if expense:
|
||||
net_expense += expense[-2].get(key)
|
||||
if net_profit_loss:
|
||||
net_profit += net_profit_loss.get(key)
|
||||
|
||||
if len(period_list) == 1 and periodicity == "Yearly":
|
||||
profit_label = _("Profit This Year")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=rate,
|
||||
price_list_rate=rate,
|
||||
qty=qty,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def get_fiscal_year(self):
|
||||
active_fy = frappe.db.get_all(
|
||||
"Fiscal Year",
|
||||
filters={"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
|
||||
)[0]
|
||||
return frappe.get_doc("Fiscal Year", active_fy.name)
|
||||
|
||||
def get_report_filters(self):
|
||||
fy = self.get_fiscal_year()
|
||||
return frappe._dict(
|
||||
company=self.company,
|
||||
from_fiscal_year=fy.name,
|
||||
to_fiscal_year=fy.name,
|
||||
period_start_date=fy.year_start_date,
|
||||
period_end_date=fy.year_end_date,
|
||||
filter_based_on="Fiscal Year",
|
||||
periodicity="Monthly",
|
||||
accumulated_vallues=True,
|
||||
)
|
||||
|
||||
def test_profit_and_loss_output_and_summary(self):
|
||||
si = self.create_sales_invoice(qty=1, rate=150)
|
||||
|
||||
filters = self.get_report_filters()
|
||||
period_list = get_period_list(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.period_start_date,
|
||||
filters.period_end_date,
|
||||
filters.filter_based_on,
|
||||
filters.periodicity,
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
result = execute(filters)[1]
|
||||
current_period = [x for x in period_list if x.from_date <= getdate() and x.to_date >= getdate()][
|
||||
0
|
||||
]
|
||||
current_period_key = current_period.key
|
||||
without_current_period = [x for x in period_list if x.key != current_period.key]
|
||||
# all period except current period(whence invoice was posted), should be '0'
|
||||
for acc in result:
|
||||
if acc:
|
||||
with self.subTest(acc=acc):
|
||||
for period in without_current_period:
|
||||
self.assertEqual(acc[period.key], 0)
|
||||
|
||||
for acc in result:
|
||||
if acc:
|
||||
with self.subTest(current_period_key=current_period_key):
|
||||
self.assertEqual(acc[current_period_key], 150)
|
||||
self.assertEqual(acc["total"], 150)
|
||||
@@ -117,8 +117,3 @@ frappe.query_reports["Profitability Analysis"] = {
|
||||
"parent_field": "parent_account",
|
||||
"initial_depth": 3
|
||||
}
|
||||
|
||||
erpnext.dimension_filters.forEach((dimension) => {
|
||||
frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
"payable_account": inv.credit_to,
|
||||
"mode_of_payment": inv.mode_of_payment,
|
||||
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
|
||||
"bill_no": inv.bill_no,
|
||||
"bill_date": inv.bill_date,
|
||||
"remarks": inv.remarks,
|
||||
"purchase_order": ", ".join(purchase_order),
|
||||
"purchase_receipt": ", ".join(purchase_receipt),
|
||||
|
||||
@@ -46,12 +46,10 @@ def get_result(
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
bill_no, bill_date = "", ""
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
tax_withholding_category, rate = None, None
|
||||
bill_no, bill_date = "", ""
|
||||
party = entry.party or entry.against
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
@@ -61,13 +59,18 @@ def get_result(
|
||||
if party_list:
|
||||
party = party_list[0]
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
if entry.account in tds_accounts:
|
||||
if entry.account in tds_accounts.keys():
|
||||
tax_amount += entry.credit - entry.debit
|
||||
# infer tax withholding category from the account if it's the single account for this category
|
||||
tax_withholding_category = tds_accounts.get(entry.account)
|
||||
# or else the consolidated value from the voucher document
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
# or else from the party default
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if net_total_map.get(name):
|
||||
if voucher_type == "Journal Entry":
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
@@ -80,41 +83,41 @@ def get_result(
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column(filters.party_type, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
"supplier_invoice_no": bill_no,
|
||||
"supplier_invoice_date": bill_date,
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column(filters.party_type, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
"supplier_invoice_no": bill_no,
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
|
||||
@@ -282,11 +285,20 @@ def get_tds_docs(filters):
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
tds_accounts = frappe.get_all(
|
||||
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account"
|
||||
_tds_accounts = frappe.get_all(
|
||||
"Tax Withholding Account",
|
||||
{"company": filters.get("company")},
|
||||
["account", "parent"],
|
||||
)
|
||||
tds_accounts = {}
|
||||
for tds_acc in _tds_accounts:
|
||||
# if it turns out not to be the only tax withholding category, then don't include in the map
|
||||
if tds_acc["account"] in tds_accounts:
|
||||
tds_accounts[tds_acc["account"]] = None
|
||||
else:
|
||||
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
|
||||
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True)
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
|
||||
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
@@ -345,21 +357,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
||||
)
|
||||
else:
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
return query
|
||||
|
||||
|
||||
@@ -399,7 +406,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
],
|
||||
"Journal Entry": ["tax_withholding_category", "total_amount"],
|
||||
"Journal Entry": ["total_amount"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
|
||||
@@ -5,7 +5,6 @@ import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -17,36 +16,63 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
|
||||
class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
create_tax_accounts()
|
||||
create_tcs_category()
|
||||
|
||||
def test_tax_withholding_for_customers(self):
|
||||
create_tax_category(cumulative_threshold=300)
|
||||
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
|
||||
si = create_sales_invoice(rate=1000)
|
||||
pe = create_tcs_payment_entry()
|
||||
jv = create_tcs_journal_entry()
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
expected_values = [
|
||||
# Check for JV totals using back calculation logic
|
||||
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def test_single_account_for_multiple_categories(self):
|
||||
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1.tax_withholding_category = "TDS - 1"
|
||||
inv_1.submit()
|
||||
|
||||
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
|
||||
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_2.tax_withholding_category = "TDS - 2"
|
||||
inv_2.submit()
|
||||
result = execute(
|
||||
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
|
||||
)[1]
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def check_expected_values(self, result, expected_values):
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
voucher_expected_values = expected_values[i]
|
||||
self.assertEqual(voucher.ref_no, voucher_expected_values[0])
|
||||
self.assertEqual(voucher.section_code, voucher_expected_values[1])
|
||||
self.assertEqual(voucher.rate, voucher_expected_values[2])
|
||||
self.assertEqual(voucher.base_total, voucher_expected_values[3])
|
||||
self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
|
||||
self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
|
||||
voucher_actual_values = (
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.rate,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
voucher.grand_total,
|
||||
)
|
||||
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
|
||||
|
||||
def tearDown(self):
|
||||
self.clear_old_entries()
|
||||
@@ -67,24 +93,20 @@ def create_tax_accounts():
|
||||
).insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def create_tcs_category():
|
||||
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
from_date = fiscal_year[1]
|
||||
to_date = fiscal_year[2]
|
||||
|
||||
tax_category = create_tax_withholding_category(
|
||||
category_name="TCS",
|
||||
rate=0.075,
|
||||
create_tax_withholding_category(
|
||||
category_name=category,
|
||||
rate=rate,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TCS - _TC",
|
||||
cumulative_threshold=300,
|
||||
account=account,
|
||||
cumulative_threshold=cumulative_threshold,
|
||||
)
|
||||
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
customer.tax_withholding_category = "TCS"
|
||||
customer.save()
|
||||
|
||||
|
||||
def create_tcs_payment_entry():
|
||||
payment_entry = create_payment_entry(
|
||||
@@ -109,3 +131,32 @@ def create_tcs_payment_entry():
|
||||
)
|
||||
payment_entry.submit()
|
||||
return payment_entry
|
||||
|
||||
|
||||
def create_tcs_journal_entry():
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = today()
|
||||
jv.company = "_Test Company"
|
||||
jv.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 10000,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"debit_in_account_currency": 9992.5,
|
||||
},
|
||||
{
|
||||
"account": "TCS - _TC",
|
||||
"debit_in_account_currency": 7.5,
|
||||
},
|
||||
],
|
||||
)
|
||||
jv.insert()
|
||||
return jv.submit()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user