mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-28 05:18:34 +00:00
Compare commits
456 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f02b88119 | ||
|
|
a91fb260e1 | ||
|
|
91105344bb | ||
|
|
6895efe577 | ||
|
|
2a890f9061 | ||
|
|
b2d361b495 | ||
|
|
d51cf281c3 | ||
|
|
3574d11946 | ||
|
|
37b8715096 | ||
|
|
e8aae5018a | ||
|
|
a4b9dda4b2 | ||
|
|
a5a0c0cb07 | ||
|
|
14ae3edc1d | ||
|
|
5e33961448 | ||
|
|
2db15d7375 | ||
|
|
53496ed79f | ||
|
|
4536e8bc47 | ||
|
|
36f65fc592 | ||
|
|
c237f3c4c4 | ||
|
|
cf78f9702c | ||
|
|
de0b8c07f6 | ||
|
|
4dd06b69a1 | ||
|
|
b6f352a024 | ||
|
|
a3a40febf3 | ||
|
|
6bfc8a0152 | ||
|
|
5113b0063e | ||
|
|
9e77a0245a | ||
|
|
7c37d4c71a | ||
|
|
873962a109 | ||
|
|
f0542613e0 | ||
|
|
77892f4e24 | ||
|
|
4bd7ce3e0b | ||
|
|
e7b17e05b0 | ||
|
|
f5069524f3 | ||
|
|
202ebbe140 | ||
|
|
3691a500eb | ||
|
|
f4032b64c9 | ||
|
|
2913785bac | ||
|
|
3a57ef4c5f | ||
|
|
610f74ca62 | ||
|
|
012248bd12 | ||
|
|
ed185a6171 | ||
|
|
3912c688ac | ||
|
|
c7fb6e5f61 | ||
|
|
22b283d4a8 | ||
|
|
56b4e79305 | ||
|
|
e8cfb4d602 | ||
|
|
c87f2d2f7c | ||
|
|
ec00b2cf48 | ||
|
|
c55f8e3388 | ||
|
|
4951d01102 | ||
|
|
a2d5024d3f | ||
|
|
ef7a83c354 | ||
|
|
810bc65e30 | ||
|
|
cfb5a6592c | ||
|
|
9afbd43f05 | ||
|
|
6fda0e8e1b | ||
|
|
9db7f7b033 | ||
|
|
82b2046805 | ||
|
|
6cb9ae2035 | ||
|
|
dda4002da4 | ||
|
|
91bff147e0 | ||
|
|
b3c9697b7c | ||
|
|
7648d8db80 | ||
|
|
578f9b61c6 | ||
|
|
23c5edaf08 | ||
|
|
35c10fa97f | ||
|
|
10025a2c8d | ||
|
|
813b699d5e | ||
|
|
94b09bb836 | ||
|
|
ff14d72a46 | ||
|
|
0c2d7f2d9a | ||
|
|
094411f5f0 | ||
|
|
c0b5f7c8eb | ||
|
|
73eab91631 | ||
|
|
2f4f2a8eec | ||
|
|
6a2210e46a | ||
|
|
820dcd79bb | ||
|
|
97205ce355 | ||
|
|
8207b12f11 | ||
|
|
09946c7ea7 | ||
|
|
15aeec8a2d | ||
|
|
29fe7bea6d | ||
|
|
c58987ba38 | ||
|
|
ef93c96f43 | ||
|
|
c29b95e306 | ||
|
|
e6438b293d | ||
|
|
18a75fef9a | ||
|
|
12d5e247c5 | ||
|
|
72cc42997b | ||
|
|
3a02800fbc | ||
|
|
6cf248578f | ||
|
|
5186872b9b | ||
|
|
54842ea0b3 | ||
|
|
a95a0524d9 | ||
|
|
dffd5f291d | ||
|
|
164b417136 | ||
|
|
8412938442 | ||
|
|
30ad25d86c | ||
|
|
651e853156 | ||
|
|
c2fdef4d5b | ||
|
|
678f08898a | ||
|
|
c79088f46a | ||
|
|
d5896cd8a0 | ||
|
|
c5dedab304 | ||
|
|
cf34219c71 | ||
|
|
79edcb2a94 | ||
|
|
bd2f408f00 | ||
|
|
3b02f80a4e | ||
|
|
bebd1a0426 | ||
|
|
f6f341ad58 | ||
|
|
752f93c630 | ||
|
|
d14e386e18 | ||
|
|
9f687eee4e | ||
|
|
c98ab15a44 | ||
|
|
333c7577eb | ||
|
|
ea7c82c6a3 | ||
|
|
54815b44e7 | ||
|
|
ed568fb01d | ||
|
|
a520d06c3f | ||
|
|
47a9325177 | ||
|
|
db319f95f1 | ||
|
|
450b3f338e | ||
|
|
2796aa8b57 | ||
|
|
a5ca5b5081 | ||
|
|
695297b917 | ||
|
|
8882b85888 | ||
|
|
1cddb4ff39 | ||
|
|
aaa6d66666 | ||
|
|
fa44b0d745 | ||
|
|
54735469c1 | ||
|
|
714a432c1c | ||
|
|
367b4177c6 | ||
|
|
2d0dca943a | ||
|
|
456f5b644b | ||
|
|
cca5aa8a96 | ||
|
|
366f383d1f | ||
|
|
7752de3a1c | ||
|
|
f0f663e552 | ||
|
|
f35bc43242 | ||
|
|
b0b5b25a53 | ||
|
|
25096185c9 | ||
|
|
a5b5b5e62c | ||
|
|
e421e16fdc | ||
|
|
cef3573d79 | ||
|
|
1ebcc33cdd | ||
|
|
a9871e379a | ||
|
|
34662e6e3c | ||
|
|
1844cb60a4 | ||
|
|
71416902f6 | ||
|
|
e289793308 | ||
|
|
365ef6b88a | ||
|
|
e44daf73fb | ||
|
|
a43136da5d | ||
|
|
d0fbb4d532 | ||
|
|
29cfb7cf25 | ||
|
|
c4c001b6f9 | ||
|
|
93d8e3f3b0 | ||
|
|
db27dd8702 | ||
|
|
cc97d4e869 | ||
|
|
eb26610d54 | ||
|
|
29449cbaf9 | ||
|
|
f29cad695a | ||
|
|
003653c572 | ||
|
|
8f19832aa5 | ||
|
|
8d7e150afb | ||
|
|
4a77653e94 | ||
|
|
fa74efc1fc | ||
|
|
b73df8f5f0 | ||
|
|
a8a8a033a1 | ||
|
|
f0230b2dfd | ||
|
|
f4811c3846 | ||
|
|
a9fa9e86ea | ||
|
|
c67be05280 | ||
|
|
186701b54f | ||
|
|
7cd6debd0e | ||
|
|
f8fd354d75 | ||
|
|
bc9b46126b | ||
|
|
f0f8a2f01b | ||
|
|
83928bbf78 | ||
|
|
bc50512c97 | ||
|
|
b7731c8fd7 | ||
|
|
8667bcd86b | ||
|
|
7086a96462 | ||
|
|
2a38e14cca | ||
|
|
f4ba879203 | ||
|
|
5c5349ed16 | ||
|
|
8df1079618 | ||
|
|
38739bec45 | ||
|
|
6ceb3476a6 | ||
|
|
6cba69c7da | ||
|
|
ffd3aea07e | ||
|
|
989ef52f59 | ||
|
|
baa36c6d5e | ||
|
|
f7f191fe50 | ||
|
|
703f58ceac | ||
|
|
e1da019fe7 | ||
|
|
f0bdc41a94 | ||
|
|
392ba36dcf | ||
|
|
1b6539c3c5 | ||
|
|
1da744dc4d | ||
|
|
ac763f8c19 | ||
|
|
0a83c8b00c | ||
|
|
b20b15af74 | ||
|
|
4089af5a7b | ||
|
|
06c73ef2da | ||
|
|
cccfbf193e | ||
|
|
3fb6f97f66 | ||
|
|
35f6657bae | ||
|
|
e82c441326 | ||
|
|
c8d85364b9 | ||
|
|
64234a571e | ||
|
|
a5e14324da | ||
|
|
37a7da3371 | ||
|
|
81c362dbe4 | ||
|
|
53034c332b | ||
|
|
0441984405 | ||
|
|
3ba6f40063 | ||
|
|
8e8d0c7bd0 | ||
|
|
f42f1bb35f | ||
|
|
0256c64634 | ||
|
|
e4f583d25a | ||
|
|
7259c0fe30 | ||
|
|
02547115e5 | ||
|
|
0bc9825238 | ||
|
|
bb66126dfa | ||
|
|
0e2abbd08e | ||
|
|
ff78fab176 | ||
|
|
04840762dd | ||
|
|
e7432fc60d | ||
|
|
97f2e88f4c | ||
|
|
37f24ae763 | ||
|
|
25b9127bae | ||
|
|
6e74e6f314 | ||
|
|
240118ee8b | ||
|
|
c1fd95ac66 | ||
|
|
8e340bb7fd | ||
|
|
96a6172999 | ||
|
|
d0d587432d | ||
|
|
07509b5e99 | ||
|
|
99d5b6dc71 | ||
|
|
56b1582027 | ||
|
|
149109649d | ||
|
|
0284328e2c | ||
|
|
a243873ab0 | ||
|
|
197e043fc9 | ||
|
|
925a164101 | ||
|
|
8f1a4b9717 | ||
|
|
67d4020241 | ||
|
|
fe4b2e36cc | ||
|
|
6759b90f85 | ||
|
|
99bc8e849c | ||
|
|
7a25d33547 | ||
|
|
2466e28bf5 | ||
|
|
1096528bb9 | ||
|
|
6ecb064264 | ||
|
|
92300b27c9 | ||
|
|
4fa9626de0 | ||
|
|
9b828b829a | ||
|
|
2936988cc6 | ||
|
|
9fca232578 | ||
|
|
fac22e93d0 | ||
|
|
3109efaf09 | ||
|
|
90ee21f868 | ||
|
|
36c46bb344 | ||
|
|
8c1f6196b8 | ||
|
|
12a31de25a | ||
|
|
3b9400755e | ||
|
|
2ce7300c3c | ||
|
|
b96b3b51b6 | ||
|
|
8f03769bf2 | ||
|
|
d20f3ab492 | ||
|
|
980ca1d8c5 | ||
|
|
4668a2d7d8 | ||
|
|
1a7efbb654 | ||
|
|
ccc2a47e73 | ||
|
|
f98716cc2a | ||
|
|
7903e8d669 | ||
|
|
9a50a0a129 | ||
|
|
1646517dc4 | ||
|
|
38811e792c | ||
|
|
edfb408464 | ||
|
|
ac48c3d4e7 | ||
|
|
1d66b7e5a3 | ||
|
|
ab9bde86f9 | ||
|
|
5c75bb8775 | ||
|
|
115a0123ed | ||
|
|
fdf1dfe46e | ||
|
|
106c154a16 | ||
|
|
00e8b862dd | ||
|
|
4195c50f02 | ||
|
|
fdb8e5b379 | ||
|
|
51cbbee4ca | ||
|
|
5000c09759 | ||
|
|
49e50662b6 | ||
|
|
f2f1f32826 | ||
|
|
fcf6500144 | ||
|
|
f2d5a69af4 | ||
|
|
d5c1c62622 | ||
|
|
e2f8e02c73 | ||
|
|
30cba7ee2c | ||
|
|
21a60c9927 | ||
|
|
0f275a9ff0 | ||
|
|
18402677da | ||
|
|
e9357c193d | ||
|
|
13895fa060 | ||
|
|
64f8498576 | ||
|
|
62ad466a3b | ||
|
|
45899b3017 | ||
|
|
d92a042bf7 | ||
|
|
4d6a71ab4b | ||
|
|
d5fa968078 | ||
|
|
71cbebd31b | ||
|
|
99317768f6 | ||
|
|
9fde7330e0 | ||
|
|
49fb6bec6a | ||
|
|
0f1f5b6f3d | ||
|
|
454e147592 | ||
|
|
701dd9e19b | ||
|
|
2cf82561f6 | ||
|
|
f2d113dcf2 | ||
|
|
97e4495f1f | ||
|
|
26c99351fd | ||
|
|
4d99449aa8 | ||
|
|
72b93805fd | ||
|
|
f31ed75578 | ||
|
|
003d7e9f3e | ||
|
|
650b25fc23 | ||
|
|
d5366c5873 | ||
|
|
bf36b4fa11 | ||
|
|
f4fc26b52e | ||
|
|
952a7b46d5 | ||
|
|
cc9857affd | ||
|
|
6de7a8de10 | ||
|
|
b593f57637 | ||
|
|
21bf7fd1f8 | ||
|
|
a2ee4631cc | ||
|
|
92ca6c6538 | ||
|
|
6028a19e6b | ||
|
|
87dc586543 | ||
|
|
22216b275c | ||
|
|
0e6edf763e | ||
|
|
e77534ba2b | ||
|
|
ae14b86a8c | ||
|
|
9665212607 | ||
|
|
5ae9ec2657 | ||
|
|
890289563a | ||
|
|
ee49e83020 | ||
|
|
598c581623 | ||
|
|
8f5278e3d4 | ||
|
|
aa38f69a67 | ||
|
|
57a81c4012 | ||
|
|
2fabcb0c50 | ||
|
|
f833923f2f | ||
|
|
077cb9e983 | ||
|
|
bcfc83d8d5 | ||
|
|
a5d1feef02 | ||
|
|
b050110544 | ||
|
|
e8f5c45751 | ||
|
|
eae5f27ec8 | ||
|
|
d48455393e | ||
|
|
7c699c8a38 | ||
|
|
20b8ee1e90 | ||
|
|
f9a7bb0544 | ||
|
|
6441bc7862 | ||
|
|
a5df0f2c94 | ||
|
|
e33416a4a7 | ||
|
|
245c6d8672 | ||
|
|
32fba94b2a | ||
|
|
a6ed10b712 | ||
|
|
7234625d65 | ||
|
|
0c4a2af9ab | ||
|
|
5ba0082bc7 | ||
|
|
47b3e96a37 | ||
|
|
810378c899 | ||
|
|
bdf198c94f | ||
|
|
e8286d7b3c | ||
|
|
fd73a8a348 | ||
|
|
91de46922d | ||
|
|
3823e0e494 | ||
|
|
625aa4ef6b | ||
|
|
e9b7d00afb | ||
|
|
e79e9e94d0 | ||
|
|
e94e15259b | ||
|
|
ab90b815e3 | ||
|
|
ee47440063 | ||
|
|
bc24e75c24 | ||
|
|
5d97c7cff9 | ||
|
|
e38383a757 | ||
|
|
a5bda0180e | ||
|
|
b6648eebfa | ||
|
|
faa3c7c3a4 | ||
|
|
5ab5bd138f | ||
|
|
dc3265751c | ||
|
|
cf8f0e4096 | ||
|
|
8c09968e1b | ||
|
|
39885b2b01 | ||
|
|
6a6a6971ba | ||
|
|
13dfbe3d80 | ||
|
|
7e3c15e0b6 | ||
|
|
570985f40e | ||
|
|
eb418e8659 | ||
|
|
014486de39 | ||
|
|
a53a80f01d | ||
|
|
b1a911aa9c | ||
|
|
18b6d50a31 | ||
|
|
fcb2f4f084 | ||
|
|
308f200793 | ||
|
|
4fcfbe6e9f | ||
|
|
5b2c13dacf | ||
|
|
76b7884fb7 | ||
|
|
518dad8ecb | ||
|
|
220ae118e8 | ||
|
|
f1f2f6e338 | ||
|
|
c16c41ee59 | ||
|
|
dcffb3886b | ||
|
|
7035969db7 | ||
|
|
0cf97f2559 | ||
|
|
459d136368 | ||
|
|
a2bfe31876 | ||
|
|
75b021dc23 | ||
|
|
b2e2e951da | ||
|
|
0f9b5074a6 | ||
|
|
190900cd1b | ||
|
|
3512f7d528 | ||
|
|
0dc2f78a2e | ||
|
|
c834a9de85 | ||
|
|
c910b8ab03 | ||
|
|
b9ebb50a02 | ||
|
|
38cc28a4c3 | ||
|
|
bbb9b9e3b6 | ||
|
|
94c3ee645d | ||
|
|
4d8c35aa5d | ||
|
|
5230d411bf | ||
|
|
a6bf7c1ebd | ||
|
|
50f6afd588 | ||
|
|
649c192abe | ||
|
|
b7d6a54bed | ||
|
|
05e4dae1b8 | ||
|
|
97fdda8a7c | ||
|
|
9802333397 | ||
|
|
82d206b709 | ||
|
|
80810c2ebb | ||
|
|
331a743d69 | ||
|
|
32a1fea8f0 | ||
|
|
dfb4c47089 | ||
|
|
0cbf049608 | ||
|
|
3f0cb47464 | ||
|
|
89d507e07e | ||
|
|
26595351cc | ||
|
|
0aa246c39e | ||
|
|
5468a3b0b7 | ||
|
|
d0a0a35d72 | ||
|
|
3b2044dcd7 | ||
|
|
7f2a54d95b | ||
|
|
819ced4cb3 |
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep==0.97.0
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
||||
@@ -69,12 +69,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.70.0"
|
||||
__version__ = "14.74.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
)
|
||||
|
||||
if not amount:
|
||||
return
|
||||
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
gl_posting_date = end_date
|
||||
prev_posting_date = None
|
||||
# check if books nor frozen till endate:
|
||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
prev_posting_date = end_date
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
submit_journal_entry,
|
||||
)
|
||||
else:
|
||||
make_gl_entries(
|
||||
doc,
|
||||
credit_account,
|
||||
debit_account,
|
||||
against,
|
||||
amount,
|
||||
base_amount,
|
||||
gl_posting_date,
|
||||
project,
|
||||
account_currency,
|
||||
item.cost_center,
|
||||
item,
|
||||
deferred_process,
|
||||
)
|
||||
|
||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||
if frappe.flags.deferred_accounting_error:
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-20 09:37:47.650347",
|
||||
"modified": "2024-01-22 12:10:10.151819",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -36,8 +36,12 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
refresh: function (frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "primary");
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +49,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "primary");
|
||||
frm.change_custom_button_type("Update Clearance Date", null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +58,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "default");
|
||||
frm.change_custom_button_type("Update Clearance Date", null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -210,8 +210,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -126,6 +126,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
!(frm.doc.accounts || []).length ||
|
||||
((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)
|
||||
) {
|
||||
if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) {
|
||||
if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) {
|
||||
return frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
|
||||
@@ -283,7 +283,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
filters: [[jvd.reference_type, "docstatus", "=", 1]],
|
||||
};
|
||||
|
||||
if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) {
|
||||
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
|
||||
// Filter by cost center
|
||||
if (jvd.cost_center) {
|
||||
@@ -295,7 +295,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
|
||||
}
|
||||
|
||||
if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
|
||||
if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) {
|
||||
// party_type and party mandatory
|
||||
frappe.model.validate_missing(jvd, "party_type");
|
||||
frappe.model.validate_missing(jvd, "party");
|
||||
|
||||
@@ -557,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -21,8 +21,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_from", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ["in", account_types],
|
||||
@@ -75,8 +80,11 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_to", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ["in", account_types],
|
||||
@@ -121,7 +129,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
if (['Purchase Invoice', 'Sales Invoice'].includes(child.reference_doctype) && child.reference_name) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
@@ -145,10 +153,27 @@ frappe.ui.form.on('Payment Entry', {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
erpnext.hide_company();
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
@@ -179,7 +204,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
hide_unhide_fields: function(frm) {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
|
||||
|
||||
frm.toggle_display("source_exchange_rate",
|
||||
(frm.doc.paid_amount && frm.doc.paid_from_account_currency != company_currency));
|
||||
@@ -225,7 +250,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
set_dynamic_labels: function(frm) {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
|
||||
|
||||
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
|
||||
@@ -309,6 +334,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.customer_query"
|
||||
}
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -485,8 +516,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
@@ -853,7 +884,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
|
||||
if(paid_amount > total_negative_outstanding) {
|
||||
if(total_negative_outstanding == 0) {
|
||||
@@ -988,7 +1019,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Customer" &&
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||
!["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
|
||||
@@ -996,7 +1027,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Supplier" &&
|
||||
!in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype)
|
||||
!["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry", [row.idx]));
|
||||
@@ -1080,7 +1111,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
bank_account: function(frm) {
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from":"paid_to";
|
||||
if (frm.doc.bank_account && in_list(['Pay', 'Receive'], frm.doc.payment_type)) {
|
||||
if (frm.doc.bank_account && ['Pay', 'Receive'].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details",
|
||||
args: {
|
||||
@@ -1395,8 +1426,9 @@ frappe.ui.form.on('Payment Entry Reference', {
|
||||
args: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
party_account_currency: frm.doc.payment_type=="Receive" ?
|
||||
frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency
|
||||
party_account_currency: frm.doc.payment_type == "Receive" ? frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
|
||||
@@ -9,8 +9,7 @@ import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
from pypika.functions import Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -75,7 +74,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
@@ -90,6 +88,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
@@ -348,7 +347,11 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
@@ -671,9 +674,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -717,7 +718,26 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
@@ -725,12 +745,15 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -1540,7 +1563,7 @@ def get_outstanding_reference_documents(args):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
@@ -1883,33 +1906,42 @@ def get_company_defaults(company):
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
res = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
Case()
|
||||
.when(
|
||||
gl.party_type == "Customer",
|
||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
||||
)
|
||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
||||
.as_("outstanding_amount")
|
||||
)
|
||||
def get_outstanding_on_journal_entry(voucher_no, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
|
||||
outstanding = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(Coalesce(gl.party_type, "") != "")
|
||||
& (gl.is_cancelled == 0)
|
||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
||||
(ple.against_voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
).run()
|
||||
|
||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
||||
outstanding_amount = outstanding[0][0] if outstanding else 0
|
||||
|
||||
return outstanding_amount
|
||||
total = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(ple.voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run()
|
||||
|
||||
total_amount = total[0][0] if total else 0
|
||||
|
||||
return outstanding_amount, total_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reference_details(reference_doctype, reference_name, party_account_currency):
|
||||
def get_reference_details(
|
||||
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
@@ -1920,12 +1952,13 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
if ref_doc.multi_currency:
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype != "Journal Entry":
|
||||
if not total_amount:
|
||||
|
||||
@@ -1087,7 +1087,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
ref_details = get_reference_details(
|
||||
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||
)
|
||||
expected_response = {
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
|
||||
// payment Entry
|
||||
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") {
|
||||
frm.add_custom_button(__("Create Payment Entries"), function () {
|
||||
frm.add_custom_button(__("Create Journal Entries"), function () {
|
||||
frm.trigger("make_payment_records");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ class PaymentReconciliation(Document):
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
conditions.append(doc.outstanding_amount != 0)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
@@ -1335,6 +1335,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_cr_note_payment_limit_filter(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
for _ in range(6):
|
||||
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 = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 6)
|
||||
self.assertEqual(len(pr.payments), 6)
|
||||
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"), [])
|
||||
|
||||
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 = cr_note.save().submit()
|
||||
|
||||
# Limit should not affect in fetching the unallocated cr_note
|
||||
pr.invoice_limit = 5
|
||||
pr.payment_limit = 5
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
if (
|
||||
frm.doc.payment_request_type == "Inward" &&
|
||||
frm.doc.payment_channel !== "Phone" &&
|
||||
!in_list(["Initiated", "Paid"], frm.doc.status) &&
|
||||
!["Initiated", "Paid"].includes(frm.doc.status) &&
|
||||
!frm.doc.__islocal &&
|
||||
frm.doc.docstatus == 1
|
||||
) {
|
||||
|
||||
@@ -19,7 +19,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
@@ -284,6 +284,17 @@ class PaymentRequest(Document):
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
self.payment_request_type == "Outward"
|
||||
and payment_entry.paid_from_account_currency == ref_doc.company_currency
|
||||
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
|
||||
):
|
||||
payment_entry.paid_amount = payment_entry.base_paid_amount = (
|
||||
payment_entry.target_exchange_rate * payment_entry.received_amount
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
@@ -509,7 +520,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
@@ -596,7 +607,11 @@ def update_payment_req_status(doc, method):
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype, ref.reference_name, doc.party_account_currency
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
@@ -32,7 +34,7 @@ payment_method = [
|
||||
]
|
||||
|
||||
|
||||
class TestPaymentRequest(unittest.TestCase):
|
||||
class TestPaymentRequest(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
# Try to make Payment Request more than SO amount, should give validation
|
||||
pr2.grand_total = 900
|
||||
self.assertRaises(frappe.ValidationError, pr2.save)
|
||||
|
||||
def test_conversion_on_foreign_currency_accounts(self):
|
||||
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
|
||||
po_doc.conversion_rate = 80
|
||||
po_doc.items[0].qty = 1
|
||||
po_doc.items[0].rate = 10
|
||||
po_doc.save().submit()
|
||||
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
@@ -33,7 +33,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
inv.save()
|
||||
|
||||
self.assertEqual(inv.net_total, 4298.25)
|
||||
self.assertEqual(inv.net_total, 4298.24)
|
||||
self.assertEqual(inv.grand_total, 4900.00)
|
||||
|
||||
def test_tax_calculation_with_multiple_items(self):
|
||||
|
||||
@@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -441,7 +441,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.status, "Return")
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -74,15 +74,21 @@
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_13",
|
||||
"threshold_percentage",
|
||||
"priority",
|
||||
"dynamic_condition_tab",
|
||||
"condition",
|
||||
"column_break_66",
|
||||
"section_break_13",
|
||||
"apply_multiple_pricing_rules",
|
||||
"apply_discount_on_rate",
|
||||
"column_break_66",
|
||||
"threshold_percentage",
|
||||
"validate_pricing_rule_section",
|
||||
"validate_applied_rule",
|
||||
"column_break_texp",
|
||||
"rule_description",
|
||||
"priority_section",
|
||||
"has_priority",
|
||||
"column_break_sayg",
|
||||
"priority",
|
||||
"help_section",
|
||||
"pricing_rule_help",
|
||||
"reference_section",
|
||||
@@ -477,7 +483,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
@@ -487,6 +493,7 @@
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_priority",
|
||||
"description": "Higher the number, higher the priority",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@@ -513,6 +520,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -525,7 +533,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Help Article",
|
||||
"options": "Simple"
|
||||
},
|
||||
{
|
||||
@@ -603,12 +612,42 @@
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_condition_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dynamic Condition"
|
||||
},
|
||||
{
|
||||
"fieldname": "validate_pricing_rule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validate Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_texp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sayg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -31,6 +31,7 @@ class PricingRule(Document):
|
||||
self.validate_price_list_with_currency()
|
||||
self.validate_dates()
|
||||
self.validate_condition()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
if not self.margin_type:
|
||||
self.margin_rate_or_amount = 0.0
|
||||
@@ -47,6 +48,12 @@ class PricingRule(Document):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.has_priority and not self.priority:
|
||||
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||
|
||||
if self.priority and not self.has_priority:
|
||||
self.has_priority = 1
|
||||
|
||||
for apply_on, field in apply_on_dict.items():
|
||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||
@@ -195,6 +202,10 @@ class PricingRule(Document):
|
||||
):
|
||||
frappe.throw(_("Invalid condition expression"))
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions and self.is_recursive:
|
||||
frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -1031,6 +1033,137 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_priority_of_multiple_pricing_rules(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"has_priority": 1,
|
||||
"priority": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"has_priority": 1,
|
||||
"priority": 3,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_percentage, 20)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_validation_on_mixed_condition_with_recursion(self):
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=10,
|
||||
selling=1,
|
||||
priority=2,
|
||||
min_qty=4,
|
||||
title="_Test Pricing Rule with Min Qty - 2",
|
||||
)
|
||||
pricing_rule.mixed_conditions = True
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
@@ -1059,6 +1192,7 @@ def make_pricing_rule(**args):
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
||||
|
||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and pricing_rules[0].has_priority:
|
||||
continue
|
||||
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
break
|
||||
|
||||
@@ -482,7 +485,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
|
||||
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_advance_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
|
||||
root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company(frm) {
|
||||
frm.set_value("party", "");
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
},
|
||||
party_type(frm) {
|
||||
frm.set_value("party", "");
|
||||
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
|
||||
party(frm) {
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
include_advance: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
if (typeof r.message === "string") {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
frm.set_value("receivable_payable_account", r.message[0]);
|
||||
frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"column_break_io6c",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"filter_section",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
@@ -141,12 +142,23 @@
|
||||
{
|
||||
"fieldname": "section_break_a8yx",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-11 10:56:51.699137",
|
||||
"modified": "2024-08-27 14:48:56.715320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -180,4 +192,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -74,6 +76,7 @@ def get_pr_instance(doc: str):
|
||||
"party_type",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
@@ -479,7 +482,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"ignore_cr_dr_notes",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -381,10 +382,16 @@
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -79,6 +79,9 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.ignore_cr_dr_notes:
|
||||
filters.update({"ignore_cr_dr_notes": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
col, res = get_soa(filters)
|
||||
|
||||
@@ -77,6 +77,7 @@ class PromotionalScheme(Document):
|
||||
|
||||
self.validate_applicable_for()
|
||||
self.validate_pricing_rules()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if self.applicable_for:
|
||||
@@ -94,7 +95,7 @@ class PromotionalScheme(Document):
|
||||
docnames = []
|
||||
|
||||
# If user has changed applicable for
|
||||
if self._doc_before_save.applicable_for == self.applicable_for:
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
@@ -108,6 +109,7 @@ class PromotionalScheme(Document):
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
pricing_rules = (
|
||||
frappe.get_all(
|
||||
"Pricing Rule",
|
||||
@@ -119,6 +121,15 @@ class PromotionalScheme(Document):
|
||||
)
|
||||
self.update_pricing_rules(pricing_rules)
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions:
|
||||
if self.product_discount_slabs:
|
||||
for slab in self.product_discount_slabs:
|
||||
if slab.is_recursive:
|
||||
frappe.throw(
|
||||
_("Recursive Discounts with Mixed condition is not supported by the system")
|
||||
)
|
||||
|
||||
def update_pricing_rules(self, pricing_rules):
|
||||
rules = {}
|
||||
count = 0
|
||||
|
||||
@@ -107,6 +107,25 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_validation_on_recurse_with_mixed_condition(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.set("price_discount_slabs", [])
|
||||
ps.set(
|
||||
"product_discount_slabs",
|
||||
[
|
||||
{
|
||||
"rule_description": "12+1",
|
||||
"min_qty": 12,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 12,
|
||||
}
|
||||
],
|
||||
)
|
||||
ps.mixed_conditions = True
|
||||
self.assertRaises(frappe.ValidationError, ps.save)
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -59,25 +59,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||
if(doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -440,8 +421,12 @@ function hide_fields(doc) {
|
||||
|
||||
var item_fields_stock = ['warehouse_section', 'received_qty', 'rejected_qty'];
|
||||
|
||||
cur_frm.fields_dict['items'].grid.set_column_disp(item_fields_stock,
|
||||
(cint(doc.update_stock)==1 || cint(doc.is_return)==1 ? true : false));
|
||||
if (cur_frm.fields_dict["items"]) {
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
}
|
||||
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
@@ -361,7 +360,8 @@
|
||||
"description": "Once set, this invoice will be on hold till the set date",
|
||||
"fieldname": "release_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Release Date"
|
||||
"label": "Release Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_17",
|
||||
@@ -1590,15 +1590,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
@@ -1619,7 +1610,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 15:57:00.736868",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -284,7 +284,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_received_but_not_billed = None
|
||||
self.asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -367,26 +367,60 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
account = None
|
||||
if not item.pr_detail and item.po_detail:
|
||||
receipt_item = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
{
|
||||
"purchase_order": item.purchase_order,
|
||||
"purchase_order_item": item.po_detail,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["name", "parent"],
|
||||
as_dict=1,
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
if receipt_item:
|
||||
item.pr_detail = receipt_item.name
|
||||
item.purchase_receipt = receipt_item.parent
|
||||
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
"asset_received_but_not_billed"
|
||||
)
|
||||
|
||||
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||
arbnb_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"account": self.asset_received_but_not_billed,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
if arbnb_booked_in_pr:
|
||||
account = self.asset_received_but_not_billed
|
||||
|
||||
if not account:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||
form_link, self.company
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = account
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -394,7 +428,7 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
validate_account_head(item.idx, item.expense_account, self.company, "Expense")
|
||||
|
||||
def set_against_expense_account(self):
|
||||
def set_against_expense_account(self, force=False):
|
||||
against_accounts = []
|
||||
for item in self.get("items"):
|
||||
if item.expense_account and (item.expense_account not in against_accounts):
|
||||
@@ -402,6 +436,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.against_expense_account = ",".join(against_accounts)
|
||||
|
||||
def force_set_against_expense_account(self):
|
||||
self.set_against_expense_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
@@ -567,17 +605,17 @@ class PurchaseInvoice(BuyingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
@@ -982,7 +1020,7 @@ class PurchaseInvoice(BuyingController):
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
@@ -1010,6 +1048,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
@@ -1026,7 +1065,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
@@ -1471,6 +1513,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
@@ -1512,8 +1557,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
@@ -1528,7 +1571,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
self.set("advance_tax", [])
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
|
||||
@@ -10,7 +10,10 @@ import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
@@ -1908,18 +1911,15 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
def test_default_cost_center_for_purchase(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,14 +53,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified": "2024-06-03 17:30:37.012593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -71,7 +70,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -149,6 +149,10 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
doc.force_set_against_income_account()
|
||||
else:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-07 14:24:13.321522",
|
||||
"modified": "2024-06-06 13:56:37.908879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
@@ -30,13 +30,17 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
"select": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,13 +98,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified": "2024-06-03 17:31:04.472279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -114,7 +115,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,25 +49,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
}
|
||||
@@ -200,7 +181,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
if(cur_frm.meta._default_print_format) {
|
||||
cur_frm.meta.default_print_format = cur_frm.meta._default_print_format;
|
||||
cur_frm.meta._default_print_format = null;
|
||||
} else if(in_list([cur_frm.pos_print_format, cur_frm.return_print_format], cur_frm.meta.default_print_format)) {
|
||||
} else if([cur_frm.pos_print_format, cur_frm.return_print_format].includes(cur_frm.meta.default_print_format)) {
|
||||
cur_frm.meta.default_print_format = null;
|
||||
cur_frm.meta._default_print_format = null;
|
||||
}
|
||||
@@ -428,12 +409,16 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
const for_validate = me.frm.doc.is_return ? true : false;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "set_missing_values",
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message && r.message.print_format) {
|
||||
args: {
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
|
||||
@@ -213,7 +213,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2184,7 +2183,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:02:28.549041",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -385,7 +385,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -532,23 +531,23 @@ class SalesInvoice(SellingController):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
@@ -768,6 +767,10 @@ class SalesInvoice(SellingController):
|
||||
against_acc.append(d.income_account)
|
||||
self.against_income_account = ",".join(against_acc)
|
||||
|
||||
def force_set_against_income_account(self):
|
||||
self.set_against_income_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_income_account", self.against_income_account)
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
@@ -1291,6 +1294,10 @@ class SalesInvoice(SellingController):
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1304,7 +1311,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
@@ -305,7 +307,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.insert()
|
||||
|
||||
# with inclusive tax
|
||||
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
|
||||
self.assertEqual(si.items[0].net_amount, 3947.37)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 3947.37)
|
||||
self.assertEqual(si.grand_total, 5000)
|
||||
|
||||
@@ -412,8 +415,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.base_grand_total, 1500.01)
|
||||
self.assertEqual(si.grand_total, 1500.01)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
@@ -649,7 +652,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
62.5,
|
||||
625.0,
|
||||
50,
|
||||
499.97600115194473,
|
||||
499.98,
|
||||
],
|
||||
"_Test Item Home Desktop 200": [
|
||||
190.66,
|
||||
@@ -660,7 +663,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
190.66,
|
||||
953.3,
|
||||
150,
|
||||
749.9968530500239,
|
||||
750,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -673,20 +676,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||
|
||||
# check net total
|
||||
self.assertEqual(si.net_total, 1249.97)
|
||||
self.assertEqual(si.base_net_total, si.net_total)
|
||||
self.assertEqual(si.net_total, 1249.98)
|
||||
self.assertEqual(si.total, 1578.3)
|
||||
|
||||
# check tax calculation
|
||||
expected_values = {
|
||||
"keys": ["tax_amount", "total"],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@@ -722,7 +726,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 2500,
|
||||
"base_amount": 25000,
|
||||
"net_rate": 40,
|
||||
"net_amount": 399.9808009215558,
|
||||
"net_amount": 399.98,
|
||||
"base_net_rate": 2000,
|
||||
"base_net_amount": 19999,
|
||||
},
|
||||
@@ -736,7 +740,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 7500,
|
||||
"base_amount": 37500,
|
||||
"net_rate": 118.01,
|
||||
"net_amount": 590.0531205155963,
|
||||
"net_amount": 590.05,
|
||||
"base_net_rate": 5900.5,
|
||||
"base_net_amount": 29502.5,
|
||||
},
|
||||
@@ -774,8 +778,13 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(si.base_grand_total, 60795)
|
||||
self.assertEqual(si.grand_total, 1215.90)
|
||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
||||
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
|
||||
self.assertEqual(si.rounding_adjustment, 0.10)
|
||||
self.assertEqual(si.base_rounding_adjustment, 5.0)
|
||||
else:
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.0)
|
||||
|
||||
def test_outstanding(self):
|
||||
w = self.make()
|
||||
@@ -2097,7 +2106,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.net_total, 19453.13)
|
||||
self.assertEqual(si.grand_total, 24900)
|
||||
self.assertEqual(si.total_taxes_and_charges, 5446.88)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.rounding_adjustment, 0.00)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
@@ -2124,7 +2133,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
def test_rounding_adjustment_2(self):
|
||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
||||
for rate in [400, 600, 100]:
|
||||
for rate in [400.25, 600.30, 100.65]:
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2150,17 +2159,18 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 1271.19)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 1272.20)
|
||||
self.assertEqual(si.grand_total, 1501.20)
|
||||
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.50],
|
||||
["_Test Account VAT - _TC", 0.0, 114.50],
|
||||
[si.debit_to, 1501, 0.0],
|
||||
["Round Off - _TC", 0.20, 0.0],
|
||||
["Sales - _TC", 0.0, 1272.20],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -2218,7 +2228,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 4007.15)
|
||||
self.assertEqual(si.grand_total, 4488.02)
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
@@ -2230,7 +2241,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
["Round Off - _TC", 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2883,13 +2894,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
@@ -2899,9 +2906,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
si.load_from_db()
|
||||
self.assertFalse(si.repost_required)
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
@@ -2978,10 +2982,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["2021-06-30", 20000.0, 21366.12, True],
|
||||
["2022-06-30", 20000.0, 41366.12, False],
|
||||
["2023-06-30", 20000.0, 61366.12, False],
|
||||
["2024-06-30", 20000.0, 81366.12, False],
|
||||
["2025-06-06", 18633.88, 100000.0, False],
|
||||
["2024-06-06", 38633.88, 100000.0, False],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
@@ -3033,6 +3035,128 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
cust_doc.save()
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||
)
|
||||
si.department = "All Departments"
|
||||
si.save().submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"department": "All Departments",
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
dim.disabled = True
|
||||
dim.save()
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_cancel_with_common_party_advance_jv(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier")
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier").name
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(customer=customer)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="parent",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
|
||||
# cancel sales invoice
|
||||
si.cancel()
|
||||
|
||||
# check cancellation of journal entry
|
||||
jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
|
||||
self.assertEqual(jv_status, 2)
|
||||
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3479,9 +3603,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
@@ -3520,6 +3644,122 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.update_outstanding_for_self = False
|
||||
pos_return.save().submit()
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gle)
|
||||
.select(gle.against_voucher)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
|
||||
)
|
||||
.run(as_list=1)
|
||||
)
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0][0], pos_return.return_against)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_foreign_currency_jv(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors USD",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -867,7 +867,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -892,7 +893,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:34:10.479329",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,6 +37,12 @@ frappe.ui.form.on("Subscription", {
|
||||
frm.add_custom_button(__("Fetch Subscription Updates"), () =>
|
||||
frm.events.get_subscription_updates(frm)
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Force-Fetch Subscription Updates"),
|
||||
() => frm.trigger("force_fetch_subscription_updates"),
|
||||
__("Actions")
|
||||
);
|
||||
} else if (frm.doc.status === "Cancelled") {
|
||||
frm.add_custom_button(__("Restart Subscription"), () =>
|
||||
frm.events.renew_this_subscription(frm)
|
||||
@@ -96,4 +102,11 @@ frappe.ui.form.on("Subscription", {
|
||||
},
|
||||
});
|
||||
},
|
||||
force_fetch_subscription_updates: function (frm) {
|
||||
frm.call("force_fetch_subscription_updates").then((r) => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -674,6 +674,28 @@ class Subscription(Document):
|
||||
if invoice:
|
||||
return invoice.precision("grand_total")
|
||||
|
||||
@frappe.whitelist()
|
||||
def force_fetch_subscription_updates(self):
|
||||
"""
|
||||
Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
|
||||
It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
|
||||
"""
|
||||
|
||||
# Don't process future subscriptions
|
||||
if nowdate() < self.current_invoice_start:
|
||||
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
|
||||
return
|
||||
|
||||
processing_date = None
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period":
|
||||
processing_date = self.current_invoice_start
|
||||
elif self.generate_invoice_at == "End of the current subscription period":
|
||||
processing_date = self.current_invoice_end
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||
|
||||
self.process(posting_date=processing_date)
|
||||
|
||||
|
||||
def get_calendar_months(billing_interval):
|
||||
calendar_months = []
|
||||
|
||||
@@ -712,3 +712,18 @@ class TestSubscription(FrappeTestCase):
|
||||
self.assertEqual(pi.total, 55333.33)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_future_subscription(self):
|
||||
"""Force-Fetch should not process future subscriptions"""
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Customer"
|
||||
subscription.party = "_Test Customer"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.generate_new_invoices_past_due_date = 1
|
||||
subscription.start_date = add_months(nowdate(), 1)
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
subscription.save()
|
||||
|
||||
subscription.force_fetch_subscription_updates()
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
@@ -236,6 +236,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -243,7 +248,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
to_date=tax_details.to_date,
|
||||
party_type=party_type,
|
||||
)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
||||
tax_deducted_on_advances = 0
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
@@ -337,12 +343,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
AND ja.party in %s
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
AND j.company = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -355,6 +363,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
payment_entry_filters = {
|
||||
"party_type": party_type,
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
||||
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
@@ -445,6 +467,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -142,7 +144,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_party_details(
|
||||
pos_profile=None,
|
||||
):
|
||||
if not party:
|
||||
return {}
|
||||
return frappe._dict()
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||
return _get_party_details(
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
||||
|
||||
if filters.account_type:
|
||||
conditions["account_type"] = filters.account_type
|
||||
return conditions
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
@@ -162,6 +162,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
label: __("Group by Voucher"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "handle_employee_advances",
|
||||
label: __("Handle Employee Advances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -112,6 +112,26 @@ class ReceivablePayableReport:
|
||||
|
||||
self.build_data()
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
@@ -123,23 +143,8 @@ class ReceivablePayableReport:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -207,6 +212,18 @@ class ReceivablePayableReport:
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
# Build and use a separate row for Employee Advances.
|
||||
# This allows Payments or Journals made against Emp Advance to be processed.
|
||||
if (
|
||||
not row
|
||||
and ple.against_voucher_type == "Employee Advance"
|
||||
and self.filters.handle_employee_advances
|
||||
):
|
||||
_d = self.build_voucher_dict(ple)
|
||||
_d.voucher_type = ple.against_voucher_type
|
||||
_d.voucher_no = ple.against_voucher_no
|
||||
row = self.voucher_balance[key] = _d
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
if self.filters.get("ignore_accounts"):
|
||||
@@ -253,7 +270,7 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if ple.cost_center:
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
@@ -288,13 +305,13 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
|
||||
@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
def create_payment_entry(self, docname, do_not_submit=False):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
if not do_not_submit:
|
||||
pe.submit()
|
||||
return pe
|
||||
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
@@ -955,3 +957,69 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def test_accounts_receivable_output_for_minor_outstanding(self):
|
||||
"""
|
||||
AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 99.99, 0.01]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
def test_cost_center_on_report_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.cost_center = self.cost_center
|
||||
si.save().submit()
|
||||
|
||||
new_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "East Wing",
|
||||
"parent_cost_center": self.company + " - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_cc.save()
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
pe = self.create_payment_entry(si.name, do_not_submit=True)
|
||||
pe.cost_center = new_cc.name
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [si.name, si.cost_center, 60]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
open_utility_report: function () {
|
||||
frappe.route_options = {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account: frappe.query_report.get_filter_value("account"),
|
||||
report_date: frappe.query_report.get_filter_value("report_date"),
|
||||
};
|
||||
frappe.open_in_new_tab = true;
|
||||
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -150,8 +150,8 @@ def get_payment_entries(filters):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -43,7 +43,7 @@ function get_filters() {
|
||||
label: __("From Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@ function get_filters() {
|
||||
label: __("To Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Bank Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
default: frappe.defaults.get_user_default("Company")
|
||||
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
|
||||
: "",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_account_list",
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "disabled", "=", 0],
|
||||
["Account", "company", "=", company],
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-07-30 17:20:07.570971",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-07-30 17:20:07.570971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cheques and Deposits Incorrectly cleared",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Entry",
|
||||
"report_name": "Cheques and Deposits Incorrectly cleared",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = build_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def build_payment_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
}
|
||||
)
|
||||
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": row.get("amount"),
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": row.get("amount"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_journal_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
"debit": row.get("debit_in_account_currency"),
|
||||
"credit": row.get("credit_in_account_currency"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_data(filters):
|
||||
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
|
||||
data = []
|
||||
for x in vouchers:
|
||||
if x.doctype == "Payment Entry":
|
||||
data.append(build_payment_entry_dict(x))
|
||||
elif x.doctype == "Journal Entry":
|
||||
data.append(build_journal_entry_dict(x))
|
||||
return data
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
doctype_name = ConstantColumn("Journal Entry")
|
||||
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
je.name,
|
||||
jea.debit_in_account_currency,
|
||||
jea.credit_in_account_currency,
|
||||
je.posting_date,
|
||||
je.clearance_date,
|
||||
)
|
||||
.where(
|
||||
je.docstatus.eq(1)
|
||||
& jea.account.eq(filters.account)
|
||||
& je.posting_date.gt(filters.report_date)
|
||||
& je.clearance_date.lte(filters.report_date)
|
||||
& (je.is_opening.isnull() | je.is_opening.eq("No"))
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
pe.clearance_date,
|
||||
)
|
||||
.where(
|
||||
pe.docstatus.eq(1)
|
||||
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
|
||||
& pe.posting_date.gt(filters.report_date)
|
||||
& pe.clearance_date.lte(filters.report_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
return journals + payments
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "payment_document",
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry",
|
||||
"label": _("Payment Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "payment_document",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
|
||||
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
|
||||
]
|
||||
@@ -109,7 +109,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
report_summary = get_bs_summary(
|
||||
report_summary, primitive_summary = get_bs_summary(
|
||||
companies,
|
||||
asset,
|
||||
liability,
|
||||
@@ -180,7 +180,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
report_summary = get_pl_summary(
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
)
|
||||
|
||||
|
||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_zero_amount(self):
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 12
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=3910,
|
||||
price_list_rate=3910,
|
||||
warehouse=self.warehouse,
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-12-30"
|
||||
pi.items[0].service_end_date = "2022-12-30"
|
||||
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||
pi.items[0].expense_account = self.expense_account
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2022-01-01",
|
||||
end_date="2022-01-31",
|
||||
type="Expense",
|
||||
company=self.company,
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2022-01-01",
|
||||
"period_end_date": "2022-01-31",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
|
||||
# fetch the invoice from deferred invoices list
|
||||
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||
# make sure the list isn't empty
|
||||
self.assertTrue(inv)
|
||||
# calculate the total deferred expense for the period
|
||||
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||
# make sure the total deferred expense is greater than 0
|
||||
self.assertLess(deferred_exp, 0)
|
||||
|
||||
@@ -199,6 +199,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_cr_dr_notes",
|
||||
label: __("Ignore System Generated Credit / Debit Notes"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -211,7 +211,8 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
if filters.account:
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@@ -233,6 +234,23 @@ def get_conditions(filters):
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if filters.get("ignore_cr_dr_notes"):
|
||||
system_generated_cr_dr_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
|
||||
"is_system_generated": 1,
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
if system_generated_cr_dr_journals:
|
||||
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
|
||||
x[0] for x in system_generated_cr_dr_journals
|
||||
]
|
||||
filters.update({"voucher_no_not_in": vouchers_to_ignore})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
@@ -316,7 +334,7 @@ def get_accounts_with_children(accounts):
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
return list(set(all_accounts)) if all_accounts else None
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
|
||||
@@ -2,13 +2,32 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_records()
|
||||
|
||||
def clear_old_records(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def test_foreign_account_balance_after_exchange_rate_revaluation(self):
|
||||
"""
|
||||
Checks the correctness of balance after exchange rate revaluation
|
||||
@@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
)
|
||||
)
|
||||
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
def test_ignore_cr_dr_notes_filter(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
cr_note = make_return_doc(si.doctype, si.name)
|
||||
cr_note.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = si.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = si.customer
|
||||
pr.receivable_payable_account = si.debit_to
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name]
|
||||
payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
system_generated_journal = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"voucher_type": "Credit Note",
|
||||
"is_system_generated": True,
|
||||
},
|
||||
fields=["name"],
|
||||
)
|
||||
self.assertEqual(len(system_generated_journal), 1)
|
||||
expected = set([si.name, cr_note.name, system_generated_journal[0].name])
|
||||
# Without ignore_cr_dr_notes
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Without ignore_cr_dr_notes
|
||||
expected = set([si.name, cr_note.name])
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -694,7 +694,8 @@ class GrossProfitGenerator:
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
args = row
|
||||
if item_code not in self.average_buying_rate:
|
||||
key = (item_code, row.warehouse)
|
||||
if key not in self.average_buying_rate:
|
||||
args.update(
|
||||
{
|
||||
"voucher_type": row.parenttype,
|
||||
@@ -705,9 +706,9 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
average_buying_rate = get_incoming_rate(args)
|
||||
self.average_buying_rate[item_code] = flt(average_buying_rate)
|
||||
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||
|
||||
return self.average_buying_rate[item_code]
|
||||
return self.average_buying_rate[key]
|
||||
|
||||
def get_last_purchase_rate(self, item_code, row):
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("Start Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("End Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Invalid Ledger Entries"] = {
|
||||
filters: get_filters(),
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-09-09 12:31:25.295976",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-09 12:31:25.295976",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Invalid Ledger Entries",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Invalid Ledger Entries",
|
||||
"report_type": "Script Report",
|
||||
"roles": [],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
"""Return columns and data for the report.
|
||||
|
||||
This is the main entry point for the report. It accepts the filters as a
|
||||
dictionary and should return columns and data. It is called by the framework
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns() -> list[dict]:
|
||||
"""Return columns for the report.
|
||||
|
||||
One field definition per column, just like a DocType field definition.
|
||||
"""
|
||||
return [
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
active_vouchers = get_active_vouchers_for_period(filters)
|
||||
invalid_vouchers = identify_cancelled_vouchers(active_vouchers)
|
||||
|
||||
return invalid_vouchers
|
||||
|
||||
|
||||
def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]:
|
||||
cancelled_vouchers = []
|
||||
if active_vouchers:
|
||||
# Group by voucher types and use single query to identify cancelled vouchers
|
||||
vtypes = set([x.voucher_type for x in active_vouchers])
|
||||
|
||||
for _t in vtypes:
|
||||
_names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t]
|
||||
dt = qb.DocType(_t)
|
||||
non_active_vouchers = (
|
||||
qb.from_(dt)
|
||||
.select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no"))
|
||||
.where(dt.docstatus.ne(1) & dt.name.isin(_names))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if non_active_vouchers:
|
||||
cancelled_vouchers.extend(non_active_vouchers)
|
||||
return cancelled_vouchers
|
||||
|
||||
|
||||
def validate_filters(filters: dict | None = None):
|
||||
if not filters:
|
||||
frappe.throw(_("Filters missing"))
|
||||
|
||||
if not filters.company:
|
||||
frappe.throw(_("Company is mandatory"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("Start Date should be lower than End Date"))
|
||||
|
||||
|
||||
def build_query_filters(filters: dict | None = None) -> list:
|
||||
qb_filters = []
|
||||
if filters:
|
||||
if filters.account:
|
||||
qb_filters.append(qb.Field("account").isin(filters.account))
|
||||
|
||||
if filters.voucher_no:
|
||||
qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no))
|
||||
|
||||
return qb_filters
|
||||
|
||||
|
||||
def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]:
|
||||
uniq_vouchers = []
|
||||
|
||||
if filters:
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
qb_filters = build_query_filters(filters)
|
||||
|
||||
gl_vouchers = (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_type)
|
||||
.distinct()
|
||||
.select(gle.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0)
|
||||
& gle.company.eq(filters.company)
|
||||
& gle.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
pl_vouchers = (
|
||||
qb.from_(ple)
|
||||
.select(ple.voucher_type)
|
||||
.distinct()
|
||||
.select(ple.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
ple.delinked.eq(0)
|
||||
& ple.company.eq(filters.company)
|
||||
& ple.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
uniq_vouchers.extend(gl_vouchers)
|
||||
uniq_vouchers.extend(pl_vouchers)
|
||||
|
||||
return uniq_vouchers
|
||||
@@ -46,7 +46,7 @@ frappe.query_reports["Item-wise Purchase Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Supplier", "Item Group", "Item", "Invoice"],
|
||||
options: ["", "Supplier", "Item Group", "Item", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_group_by_conditions,
|
||||
get_tax_accounts,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
@@ -29,7 +30,7 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
item_list = get_items(filters, get_query_columns(additional_table_columns))
|
||||
item_list = get_items(filters, additional_table_columns)
|
||||
aii_account_map = get_aii_accounts()
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
@@ -287,59 +288,92 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
def apply_conditions(query, pi, pii, filters):
|
||||
for opts in ("company", "supplier", "mode_of_payment"):
|
||||
if filters.get(opts):
|
||||
query = query.where(pi[opts] == filters[opts])
|
||||
|
||||
for opts in (
|
||||
("company", " and `tabPurchase Invoice`.company=%(company)s"),
|
||||
("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
|
||||
("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
|
||||
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
|
||||
("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
|
||||
("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
|
||||
):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
if filters.get("from_date"):
|
||||
query = query.where(pi.posting_date >= filters.get("from_date"))
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(pi.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(pii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
conditions += (
|
||||
"ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
|
||||
)
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
conditions += get_group_by_conditions(filters, "Purchase Invoice")
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return conditions
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
if additional_query_columns:
|
||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
|
||||
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
|
||||
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
|
||||
`tabPurchase Invoice`.unrealized_profit_loss_account,
|
||||
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
|
||||
`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group`
|
||||
,`tabPurchase Invoice Item`.`item_group` as pi_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
|
||||
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
|
||||
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
|
||||
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
|
||||
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {}
|
||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
|
||||
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
|
||||
`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
|
||||
`tabPurchase Invoice`.docstatus = 1 {}
|
||||
""".format(additional_query_columns, conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
def get_items(filters, additional_table_columns):
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
.join(pii)
|
||||
.on(pi.name == pii.parent)
|
||||
.left_join(Item)
|
||||
.on(pii.item_code == Item.name)
|
||||
.select(
|
||||
pii.name,
|
||||
pii.parent,
|
||||
pi.posting_date,
|
||||
pi.credit_to,
|
||||
pi.company,
|
||||
pi.supplier,
|
||||
pi.remarks,
|
||||
pi.base_net_total,
|
||||
pi.unrealized_profit_loss_account,
|
||||
pii.item_code,
|
||||
pii.description,
|
||||
pii.item_group,
|
||||
pii.item_name.as_("pi_item_name"),
|
||||
pii.item_group.as_("pi_item_group"),
|
||||
Item.item_name.as_("i_item_name"),
|
||||
Item.item_group.as_("i_item_group"),
|
||||
pii.project,
|
||||
pii.purchase_order,
|
||||
pii.purchase_receipt,
|
||||
pii.po_detail,
|
||||
pii.expense_account,
|
||||
pii.stock_qty,
|
||||
pii.stock_uom,
|
||||
pii.base_net_amount,
|
||||
pi.supplier_name,
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.where(pii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
query = query.where(pi.supplier == filters["supplier"])
|
||||
if filters.get("company"):
|
||||
query = query.where(pi.company == filters["company"])
|
||||
|
||||
if additional_table_columns:
|
||||
for column in additional_table_columns:
|
||||
if column.get("_doctype"):
|
||||
table = frappe.qb.DocType(column.get("_doctype"))
|
||||
query = query.select(table[column.get("fieldname")])
|
||||
else:
|
||||
query = query.select(pi[column.get("fieldname")])
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWisePurchaseRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
do_not_save=1,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
qty=1,
|
||||
)
|
||||
|
||||
pi = pi.save()
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_basic_report_output(self):
|
||||
pi = self.create_purchase_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": pi.items[0].item_code,
|
||||
"invoice": pi.name,
|
||||
"posting_date": getdate(),
|
||||
"supplier": pi.supplier,
|
||||
"credit_to": pi.credit_to,
|
||||
"company": self.company,
|
||||
"expense_account": pi.items[0].expense_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": pi.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -41,6 +41,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: { company: company },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "brand",
|
||||
@@ -48,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
@@ -58,7 +70,7 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
options: ["", "Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
@@ -26,7 +27,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, get_query_columns(additional_table_columns), additional_conditions)
|
||||
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -83,9 +84,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
"company": d.company,
|
||||
"sales_order": d.sales_order,
|
||||
"delivery_note": d.delivery_note,
|
||||
"income_account": d.unrealized_profit_loss_account
|
||||
if d.is_internal_customer == 1
|
||||
else d.income_account,
|
||||
"income_account": get_income_account(d),
|
||||
"cost_center": d.cost_center,
|
||||
"stock_qty": d.stock_qty,
|
||||
"stock_uom": d.stock_uom,
|
||||
@@ -150,6 +149,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
return columns, data, None, None, None, skip_total_row
|
||||
|
||||
|
||||
def get_income_account(row):
|
||||
if row.enable_deferred_revenue:
|
||||
return row.deferred_revenue_account
|
||||
elif row.is_internal_customer == 1:
|
||||
return row.unrealized_profit_loss_account
|
||||
else:
|
||||
return row.income_account
|
||||
|
||||
|
||||
def get_columns(additional_table_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -333,93 +341,145 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
for opts in ("company", "customer"):
|
||||
if filters.get(opts):
|
||||
query = query.where(si[opts] == filters[opts])
|
||||
|
||||
for opts in (
|
||||
("company", " and `tabSales Invoice`.company=%(company)s"),
|
||||
("customer", " and `tabSales Invoice`.customer = %(customer)s"),
|
||||
("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
|
||||
("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),
|
||||
("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"),
|
||||
):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
if filters.get("from_date"):
|
||||
query = query.where(si.posting_date >= filters.get("from_date"))
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
if filters.get("to_date"):
|
||||
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
||||
sales_invoice = frappe.db.get_all(
|
||||
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||
)
|
||||
query = query.where(si.name.isin(sales_invoice))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
lft, rgt = frappe.db.get_all(
|
||||
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
||||
)[0]
|
||||
conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
|
||||
warehouses = frappe.db.get_all("Warehouse", {"lft": (">", lft), "rgt": ("<", rgt)}, pluck="name")
|
||||
query = query.where(sii.warehouse.isin(warehouses))
|
||||
else:
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
||||
query = query.where(sii.warehouse == filters.get("warehouse"))
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
|
||||
query = query.where(sii.brand == filters.get("brand"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(sii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s"""
|
||||
query = query.where(sii.item_group == filters.get("item_group"))
|
||||
|
||||
if filters.get("income_account"):
|
||||
query = query.where(
|
||||
(sii.income_account == filters.get("income_account"))
|
||||
| (sii.deferred_revenue_account == filters.get("income_account"))
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc"
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
conditions += get_group_by_conditions(filters, "Sales Invoice")
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
return conditions
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_group_by_conditions(filters, doctype):
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
return f"ORDER BY `tab{doctype} Item`.parent desc"
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
elif filters.get("group_by") == "Item":
|
||||
return f"ORDER BY `tab{doctype} Item`.`item_code`"
|
||||
query = query.orderby(ii.item_code)
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
query = query.orderby(ii.item_group)
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.join(sii)
|
||||
.on(si.name == sii.parent)
|
||||
.left_join(item)
|
||||
.on(sii.item_code == item.name)
|
||||
.select(
|
||||
sii.name,
|
||||
sii.parent,
|
||||
si.posting_date,
|
||||
si.debit_to,
|
||||
si.unrealized_profit_loss_account,
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
sii.item_code,
|
||||
sii.description,
|
||||
sii.item_name,
|
||||
sii.item_group,
|
||||
sii.item_name.as_("si_item_name"),
|
||||
sii.item_group.as_("si_item_group"),
|
||||
item.item_name.as_("i_item_name"),
|
||||
item.item_group.as_("i_item_group"),
|
||||
sii.sales_order,
|
||||
sii.delivery_note,
|
||||
sii.income_account,
|
||||
sii.cost_center,
|
||||
sii.enable_deferred_revenue,
|
||||
sii.deferred_revenue_account,
|
||||
sii.stock_qty,
|
||||
sii.stock_uom,
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
|
||||
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {}
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
|
||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
|
||||
`tabItem`.name = `tabSales Invoice Item`.`item_code` and
|
||||
`tabSales Invoice`.docstatus = 1 {}
|
||||
""".format(additional_query_columns, conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
for column in additional_query_columns:
|
||||
if column.get("_doctype"):
|
||||
table = frappe.qb.DocType(column.get("_doctype"))
|
||||
query = query.select(table[column.get("fieldname")])
|
||||
else:
|
||||
query = query.select(si[column.get("fieldname")])
|
||||
|
||||
if filters.get("customer"):
|
||||
query = query.where(si.customer == filters["customer"])
|
||||
|
||||
if filters.get("customer_group"):
|
||||
query = query.where(si.customer_group == filters["customer_group"])
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
@@ -427,16 +487,14 @@ def get_delivery_notes_against_sales_order(item_list):
|
||||
so_item_rows = list(set([d.so_detail for d in item_list]))
|
||||
|
||||
if so_item_rows:
|
||||
delivery_notes = frappe.db.sql(
|
||||
"""
|
||||
select parent, so_detail
|
||||
from `tabDelivery Note Item`
|
||||
where docstatus=1 and so_detail in (%s)
|
||||
group by so_detail, parent
|
||||
"""
|
||||
% (", ".join(["%s"] * len(so_item_rows))),
|
||||
tuple(so_item_rows),
|
||||
as_dict=1,
|
||||
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||
delivery_notes = (
|
||||
frappe.qb.from_(dn_item)
|
||||
.select(dn_item.parent, dn_item.so_detail)
|
||||
.where(dn_item.docstatus == 1)
|
||||
.where(dn_item.so_detail.isin(so_item_rows))
|
||||
.groupby(dn_item.so_detail, dn_item.parent)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for dn in delivery_notes:
|
||||
@@ -446,15 +504,16 @@ def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
|
||||
def get_grand_total(filters, doctype):
|
||||
return frappe.db.sql(
|
||||
f""" SELECT
|
||||
SUM(`tab{doctype}`.base_grand_total)
|
||||
FROM `tab{doctype}`
|
||||
WHERE `tab{doctype}`.docstatus = 1
|
||||
and posting_date between %s and %s
|
||||
""",
|
||||
(filters.get("from_date"), filters.get("to_date")),
|
||||
)[0][0] # nosec
|
||||
return flt(
|
||||
frappe.db.get_value(
|
||||
doctype,
|
||||
{
|
||||
"docstatus": 1,
|
||||
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||
},
|
||||
"sum(base_grand_total)",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_tax_accounts(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
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=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": si.items[0].item_code,
|
||||
"invoice": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": si.customer,
|
||||
"debit_to": si.debit_to,
|
||||
"company": self.company,
|
||||
"income_account": si.items[0].income_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": si.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total_other_charges": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
||||
|
||||
if d.reference_type == "Purchase Invoice":
|
||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
||||
else:
|
||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
||||
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||
payment_amount = d.amount
|
||||
|
||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||
|
||||
if d.against_voucher:
|
||||
if d.against_voucher_no:
|
||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||
|
||||
row = [
|
||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
||||
d.party_type,
|
||||
d.party,
|
||||
d.posting_date,
|
||||
d.against_voucher,
|
||||
d.against_voucher_no,
|
||||
invoice.posting_date,
|
||||
invoice.due_date,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.amount,
|
||||
d.remarks,
|
||||
d.age,
|
||||
d.range1,
|
||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = []
|
||||
|
||||
if not filters.party_type:
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
filters.party_type = "Supplier"
|
||||
else:
|
||||
filters.party_type = "Customer"
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("party_type=%(party_type)s")
|
||||
conditions.append(ple.delinked.eq(0))
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
conditions.append(ple.party_type.eq("Supplier"))
|
||||
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||
else:
|
||||
conditions.append(ple.party_type.eq("Customer"))
|
||||
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||
|
||||
if filters.party:
|
||||
conditions.append("party=%(party)s")
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("against_voucher_type=%(reference_type)s")
|
||||
filters["reference_type"] = (
|
||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
||||
)
|
||||
conditions.append(ple.party.eq(filters.party))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append("posting_date <= %(to_date)s")
|
||||
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||
|
||||
return "and " + " and ".join(conditions) if conditions else ""
|
||||
if filters.get("company"):
|
||||
conditions.append(ple.company.eq(filters.get("company")))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
||||
""".format(get_conditions(filters)),
|
||||
filters,
|
||||
as_dict=1,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
Abs(ple.amount).as_("amount"),
|
||||
ple.remarks,
|
||||
ple.against_voucher_no,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
|
||||
|
||||
def get_invoice_posting_date_map(filters):
|
||||
invoice_details = {}
|
||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
||||
dt = (
|
||||
qb.DocType("Sales Invoice")
|
||||
if filters.get("payment_type") == _("Incoming")
|
||||
else qb.DocType("Purchase Invoice")
|
||||
)
|
||||
res = (
|
||||
qb.from_(dt)
|
||||
.select(dt.name, dt.posting_date, dt.due_date)
|
||||
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||
.run(as_dict=1)
|
||||
)
|
||||
for t in res:
|
||||
invoice_details[t.name] = t
|
||||
|
||||
return invoice_details
|
||||
|
||||
@@ -12,7 +12,7 @@ def execute(filters=None):
|
||||
else:
|
||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
filters.update({"naming_series": party_naming_by})
|
||||
filters["naming_series"] = party_naming_by
|
||||
|
||||
validate_filters(filters)
|
||||
(
|
||||
@@ -63,21 +63,23 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
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)
|
||||
tax_withholding_category = tax_category_map.get((voucher_type, 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 net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
)
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
@@ -97,7 +99,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
row["party_name"] = party_map.get(party, {}).get(party_name)
|
||||
|
||||
row.update(
|
||||
{
|
||||
@@ -279,7 +281,6 @@ def get_tds_docs(filters):
|
||||
journal_entries = []
|
||||
tax_category_map = frappe._dict()
|
||||
net_total_map = frappe._dict()
|
||||
frappe._dict()
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
@@ -412,7 +413,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
|
||||
if doctype == "Purchase Invoice":
|
||||
value = [
|
||||
entry.base_tax_withholding_net_total,
|
||||
@@ -427,7 +428,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
net_total_map.update({entry.name: value})
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
@@ -56,7 +56,7 @@ def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||
):
|
||||
if isinstance(boolean, str):
|
||||
boolean = frappe.json.loads(boolean)
|
||||
boolean = loads(boolean)
|
||||
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
||||
@@ -739,6 +739,46 @@ def cancel_exchange_gain_loss_journal(
|
||||
gain_loss_je.cancel()
|
||||
|
||||
|
||||
def cancel_common_party_journal(self):
|
||||
if self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"):
|
||||
return
|
||||
|
||||
party_link = self.get_common_party_link()
|
||||
if not party_link:
|
||||
return
|
||||
|
||||
journal_entry = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="parent",
|
||||
)
|
||||
|
||||
if not journal_entry:
|
||||
return
|
||||
|
||||
common_party_journal = frappe.db.get_value(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"name": journal_entry,
|
||||
"is_system_generated": True,
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if not common_party_journal:
|
||||
return
|
||||
|
||||
common_party_je = frappe.get_doc("Journal Entry", common_party_journal)
|
||||
common_party_je.cancel()
|
||||
|
||||
|
||||
def update_accounting_ledgers_after_reference_removal(
|
||||
ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None
|
||||
):
|
||||
@@ -1571,6 +1611,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None:
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_monthly() -> None:
|
||||
"""
|
||||
Executed by background job
|
||||
"""
|
||||
companies = frappe.db.get_all(
|
||||
"Company",
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
ple_map = []
|
||||
if gl_entries:
|
||||
|
||||
@@ -78,7 +78,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.events.make_schedules_editable(frm);
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
|
||||
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(
|
||||
__("Transfer Asset"),
|
||||
function () {
|
||||
@@ -280,7 +280,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (v.journal_entry) {
|
||||
asset_values.push(asset_value);
|
||||
} else {
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
|
||||
asset_values.push(null);
|
||||
} else {
|
||||
asset_values.push(asset_value);
|
||||
@@ -312,7 +312,7 @@ frappe.ui.form.on("Asset", {
|
||||
});
|
||||
}
|
||||
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
|
||||
x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: "Date" }));
|
||||
asset_values.push(0);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import (
|
||||
add_months,
|
||||
add_years,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
get_datetime,
|
||||
@@ -361,9 +362,16 @@ class Asset(AccountsController):
|
||||
final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
|
||||
self.number_of_depreciations_booked
|
||||
)
|
||||
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
if has_pro_rata:
|
||||
for_income_tax = 0
|
||||
if frappe.db.has_column("Finance Book", "for_income_tax"):
|
||||
for_income_tax = frappe.db.get_value("Finance Book", finance_book.finance_book, "for_income_tax")
|
||||
has_pro_rata = False
|
||||
if not for_income_tax:
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
depr_already_booked = any(
|
||||
[d.journal_entry for d in self.get("schedules") if d.finance_book == finance_book.finance_book]
|
||||
)
|
||||
if has_pro_rata and not depr_already_booked and not for_income_tax:
|
||||
final_number_of_depreciations += 1
|
||||
|
||||
has_wdv_or_dd_non_yearly_pro_rata = False
|
||||
@@ -514,10 +522,13 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if (
|
||||
n == cint(final_number_of_depreciations) - 1
|
||||
and flt(value_after_depreciation) != flt(finance_book.expected_value_after_useful_life)
|
||||
) or flt(value_after_depreciation) < flt(finance_book.expected_value_after_useful_life):
|
||||
if not for_income_tax and (
|
||||
(
|
||||
n == cint(final_number_of_depreciations) - 1
|
||||
and flt(value_after_depreciation) != flt(finance_book.expected_value_after_useful_life)
|
||||
)
|
||||
or flt(value_after_depreciation) < flt(finance_book.expected_value_after_useful_life)
|
||||
):
|
||||
depreciation_amount += flt(value_after_depreciation) - flt(
|
||||
finance_book.expected_value_after_useful_life
|
||||
)
|
||||
@@ -543,7 +554,7 @@ class Asset(AccountsController):
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
"finance_book_id": cstr(finance_book.idx),
|
||||
"shift": shift,
|
||||
},
|
||||
)
|
||||
@@ -749,7 +760,6 @@ class Asset(AccountsController):
|
||||
):
|
||||
straight_line_idx = []
|
||||
finance_books = []
|
||||
|
||||
for i, d in enumerate(self.get("schedules")):
|
||||
if ignore_booked_entry and d.journal_entry:
|
||||
continue
|
||||
@@ -771,7 +781,10 @@ class Asset(AccountsController):
|
||||
finance_books.append(int(d.finance_book_id))
|
||||
|
||||
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
if not d.journal_entry:
|
||||
value_after_depreciation = flt(
|
||||
flt(value_after_depreciation) - depreciation_amount, d.precision("depreciation_amount")
|
||||
)
|
||||
|
||||
# for the last row, if depreciation method = Straight Line
|
||||
if (
|
||||
@@ -783,10 +796,13 @@ class Asset(AccountsController):
|
||||
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
||||
|
||||
if not book.shift_based:
|
||||
depreciation_amount += flt(
|
||||
adjustment_amount = flt(
|
||||
value_after_depreciation - flt(book.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
depreciation_amount = flt(
|
||||
depreciation_amount + adjustment_amount, d.precision("depreciation_amount")
|
||||
)
|
||||
|
||||
d.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation += d.depreciation_amount
|
||||
@@ -1433,7 +1449,7 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx, number_of_
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
number_of_pending_depreciations
|
||||
)
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
|
||||
@@ -1355,9 +1355,9 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, 1)
|
||||
self.assertEqual(schedule.finance_book_id, "1")
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, 2)
|
||||
self.assertEqual(schedule.finance_book_id, "2")
|
||||
|
||||
def test_depreciation_entry_cancellation(self):
|
||||
asset = create_asset(
|
||||
@@ -1689,12 +1689,12 @@ def create_asset(**args):
|
||||
return asset
|
||||
|
||||
|
||||
def create_asset_category():
|
||||
def create_asset_category(enable_cwip=1):
|
||||
asset_category = frappe.new_doc("Asset Category")
|
||||
asset_category.asset_category_name = "Computers"
|
||||
asset_category.total_number_of_depreciations = 3
|
||||
asset_category.frequency_of_depreciation = 3
|
||||
asset_category.enable_cwip_accounting = 1
|
||||
asset_category.enable_cwip_accounting = enable_cwip
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -103,12 +103,11 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
["2023-05-31", 9983.33, 45408.05],
|
||||
["2023-06-30", 9983.33, 55391.38],
|
||||
["2023-07-31", 9983.33, 65374.71],
|
||||
["2023-08-31", 8300.0, 73674.71],
|
||||
["2023-09-30", 8300.0, 81974.71],
|
||||
["2023-10-31", 8300.0, 90274.71],
|
||||
["2023-11-30", 8300.0, 98574.71],
|
||||
["2023-12-31", 8300.0, 106874.71],
|
||||
["2024-01-15", 8300.0, 115174.71],
|
||||
["2023-08-31", 9960.0, 75334.71],
|
||||
["2023-09-30", 9960.0, 85294.71],
|
||||
["2023-10-31", 9960.0, 95254.71],
|
||||
["2023-11-30", 9960.0, 105214.71],
|
||||
["2023-12-15", 9960.0, 115174.71],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
|
||||
@@ -125,9 +125,10 @@ def get_data(filters):
|
||||
if assets_linked_to_fb and asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(
|
||||
asset.asset_id, finance_book
|
||||
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||
depreciation_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount - asset.opening_accumulated_depreciation - depreciation_amount
|
||||
)
|
||||
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
@@ -139,7 +140,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
|
||||
"depreciated_amount": depreciation_amount,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
|
||||
@@ -6,8 +6,8 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
{% include 'erpnext/public/js/controllers/buying.js' %};
|
||||
|
||||
frappe.ui.form.on("Purchase Order", {
|
||||
setup: function(frm) {
|
||||
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function() {
|
||||
return {
|
||||
@@ -180,7 +180,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
this.frm.fields_dict.items_section.wrapper.removeClass("hide-border");
|
||||
}
|
||||
|
||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
||||
if(!["Closed", "Delivered"].includes(doc.status)) {
|
||||
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
|
||||
// Don't add Update Items button if the PO is following the new subcontracting flow.
|
||||
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||
@@ -211,7 +211,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
||||
}
|
||||
} else if(in_list(["Closed", "Delivered"], doc.status)) {
|
||||
} else if(["Closed", "Delivered"].includes(doc.status)) {
|
||||
if (this.frm.has_perm("submit")) {
|
||||
this.frm.add_custom_button(__('Re-open'), () => this.unclose_purchase_order(), __("Status"));
|
||||
}
|
||||
|
||||
@@ -345,7 +345,13 @@ class PurchaseOrder(BuyingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
|
||||
super().on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"fieldname": "supplier_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Supplier Type",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ def get_data():
|
||||
),
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"dynamic_links": {"party": ["Supplier", "party_type"]},
|
||||
"transactions": [
|
||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
|
||||
@@ -175,7 +175,7 @@ def get_data(filters):
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"actual_cost": flt(pi_records.get(po.name)) or flt(po.amount),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
|
||||
@@ -43,9 +43,10 @@ def get_data(filters):
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
.from_(po_item)
|
||||
.inner_join(po_item)
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on(pi_item.po_detail == po_item.name)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
|
||||
@@ -14,6 +14,9 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_
|
||||
def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
"""updates last_purchase_rate in item table for each item"""
|
||||
|
||||
if doc.get("is_internal_supplier"):
|
||||
return
|
||||
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
|
||||
@@ -84,7 +84,6 @@ force_item_fields = (
|
||||
"brand",
|
||||
"stock_uom",
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"pricing_rules",
|
||||
"weight_per_unit",
|
||||
"weight_uom",
|
||||
@@ -707,7 +706,6 @@ class AccountsController(TransactionBase):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
if item.get(fieldname) is None or fieldname in force_item_fields:
|
||||
@@ -717,7 +715,10 @@ class AccountsController(TransactionBase):
|
||||
fieldname
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "item_tax_rate" and not (
|
||||
self.get("is_return") and self.get("return_against")
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
elif fieldname == "serial_no":
|
||||
# Ensure that serial numbers are matched against Stock UOM
|
||||
item_conversion_factor = item.get("conversion_factor") or 1.0
|
||||
@@ -742,6 +743,9 @@ class AccountsController(TransactionBase):
|
||||
# reset pricing rule fields if pricing_rule_removed
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||
item.expense_account = value
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||
"is_fixed_asset"
|
||||
):
|
||||
@@ -1189,6 +1193,12 @@ class AccountsController(TransactionBase):
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if self.docstatus == 1:
|
||||
if dimensions_dict is None:
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
if self.get("doctype") == "Journal Entry":
|
||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
||||
@@ -1453,6 +1463,7 @@ class AccountsController(TransactionBase):
|
||||
remove_from_bank_transaction,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_common_party_journal,
|
||||
cancel_exchange_gain_loss_journal,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
)
|
||||
@@ -1464,6 +1475,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||
cancel_exchange_gain_loss_journal(self)
|
||||
cancel_common_party_journal(self)
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
@@ -2286,12 +2298,15 @@ class AccountsController(TransactionBase):
|
||||
|
||||
primary_account = get_party_account(primary_party_type, primary_party, self.company)
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
jv.posting_date = self.posting_date
|
||||
jv.company = self.company
|
||||
jv.remark = f"Adjustment for {self.doctype} {self.name}"
|
||||
jv.is_system_generated = True
|
||||
|
||||
reconcilation_entry = frappe._dict()
|
||||
advance_entry = frappe._dict()
|
||||
@@ -2309,6 +2324,15 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
@@ -2316,6 +2340,10 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||
jv.multi_currency = 1
|
||||
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
@@ -2373,16 +2401,12 @@ class AccountsController(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, cstr, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -305,6 +306,8 @@ class BuyingController(SubcontractingController):
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
return
|
||||
@@ -894,3 +897,8 @@ def validate_item_type(doc, fieldname, message):
|
||||
).format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user