mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-30 15:54:35 +00:00
Compare commits
421 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f95e42bec | ||
|
|
578ddb9be4 | ||
|
|
31e0bb477e | ||
|
|
d9d86dae35 | ||
|
|
d61f38b8ed | ||
|
|
8c8dc241e5 | ||
|
|
c9f49caecc | ||
|
|
cd57e009dd | ||
|
|
f11e298984 | ||
|
|
2eec0c057a | ||
|
|
50b4257a6f | ||
|
|
a23e8b13be | ||
|
|
9f09bf14cb | ||
|
|
5413372aeb | ||
|
|
710d30074d | ||
|
|
14e30d12b4 | ||
|
|
5929d50c72 | ||
|
|
d2923bae85 | ||
|
|
9e72a844f7 | ||
|
|
208bd2b8ff | ||
|
|
5110975c6d | ||
|
|
82764af09e | ||
|
|
3fd9df0d2e | ||
|
|
cf81202d94 | ||
|
|
0c0f103b83 | ||
|
|
ea4f7365ea | ||
|
|
346c06977c | ||
|
|
2dddd7906b | ||
|
|
026c2d7590 | ||
|
|
8e5252d6f8 | ||
|
|
ffc119a8a4 | ||
|
|
8f4dc8048d | ||
|
|
03e3374a8b | ||
|
|
ea86bc2235 | ||
|
|
60b81a2a59 | ||
|
|
354c34e4d8 | ||
|
|
a9bd11f59a | ||
|
|
33174b1ba2 | ||
|
|
47b216373d | ||
|
|
d17baddb0d | ||
|
|
80b5c16a2e | ||
|
|
a9b142eccd | ||
|
|
a66d1c30ae | ||
|
|
c379b783b1 | ||
|
|
f45b1db1a4 | ||
|
|
a69623c131 | ||
|
|
2c1f72e44c | ||
|
|
3915018400 | ||
|
|
b832b60b28 | ||
|
|
b70eb46222 | ||
|
|
829660e7f3 | ||
|
|
4649cf0a25 | ||
|
|
0bc947f30d | ||
|
|
8447bf34f0 | ||
|
|
6fde07da0e | ||
|
|
6bd95a6d17 | ||
|
|
6c74180e1c | ||
|
|
9bd3d7a020 | ||
|
|
84b0fa38d5 | ||
|
|
75cb29890d | ||
|
|
c31b4e6b4b | ||
|
|
cb64f90d7d | ||
|
|
7f261f3448 | ||
|
|
efdc2173b2 | ||
|
|
99828d945f | ||
|
|
11a6ebaeef | ||
|
|
d2b2002664 | ||
|
|
56dad7d365 | ||
|
|
6869e5dde9 | ||
|
|
5d5ec2ab7c | ||
|
|
f5a4ec129b | ||
|
|
e185a06a15 | ||
|
|
193d7981ea | ||
|
|
957eabf53e | ||
|
|
3bb186736d | ||
|
|
1121c6663f | ||
|
|
944479313c | ||
|
|
daa75eea00 | ||
|
|
a2b7fc18ab | ||
|
|
7d6984c873 | ||
|
|
64cbf446bd | ||
|
|
b99cdb5be7 | ||
|
|
d6de50634f | ||
|
|
4d7c0c004a | ||
|
|
f0e3fb466a | ||
|
|
8337439589 | ||
|
|
88e5ed7998 | ||
|
|
fee2255661 | ||
|
|
23d91145d0 | ||
|
|
6c8e0fd1fb | ||
|
|
512a171ad5 | ||
|
|
30f034555b | ||
|
|
a35ce12d60 | ||
|
|
8cf057849e | ||
|
|
f3c60ea0a7 | ||
|
|
c724573a18 | ||
|
|
53e1b57354 | ||
|
|
42e7725442 | ||
|
|
18b7af977c | ||
|
|
db746a4def | ||
|
|
3d5fb5fc90 | ||
|
|
a833010d2b | ||
|
|
0d6148f218 | ||
|
|
6d51d14dfd | ||
|
|
cb2cb4447a | ||
|
|
b3a8fe9391 | ||
|
|
2a820a85ed | ||
|
|
8d8dd0cd2b | ||
|
|
3a5f19853a | ||
|
|
c0dd794e15 | ||
|
|
f56ee58e81 | ||
|
|
2c99075899 | ||
|
|
01cd509113 | ||
|
|
5563bea789 | ||
|
|
a0a932a235 | ||
|
|
6f75a3c617 | ||
|
|
a4d3934f75 | ||
|
|
b4171e4bd9 | ||
|
|
76d32ab07a | ||
|
|
3d469db47b | ||
|
|
80244bafa4 | ||
|
|
c54e97b89a | ||
|
|
14202fae06 | ||
|
|
9fc0ac1a92 | ||
|
|
5bdd2989c6 | ||
|
|
42dccadff1 | ||
|
|
0650c22b53 | ||
|
|
ec26c92263 | ||
|
|
26248924b6 | ||
|
|
8d29dc6a81 | ||
|
|
6eb50fa300 | ||
|
|
8c350d43b2 | ||
|
|
caa6ca1d0b | ||
|
|
47f1714be4 | ||
|
|
ecb0506dba | ||
|
|
42382b3945 | ||
|
|
993114942e | ||
|
|
5a28a1728e | ||
|
|
80a5df0e96 | ||
|
|
25193c5e92 | ||
|
|
1ee14ac135 | ||
|
|
a3e5ffe915 | ||
|
|
ff868a9290 | ||
|
|
247ae7a965 | ||
|
|
3a149b3c9b | ||
|
|
08bed618f6 | ||
|
|
d9ca680a29 | ||
|
|
7401dc4015 | ||
|
|
eb7e063d5c | ||
|
|
0dbe79645c | ||
|
|
995773088a | ||
|
|
be736cf641 | ||
|
|
85089d3d64 | ||
|
|
eed6d2b81c | ||
|
|
edf53f1ab7 | ||
|
|
758282739e | ||
|
|
49d995c3ac | ||
|
|
72ca2ec9a5 | ||
|
|
4297895bd9 | ||
|
|
7428df8778 | ||
|
|
767c79663c | ||
|
|
fa85482662 | ||
|
|
831e2aaf18 | ||
|
|
9ac665b4bd | ||
|
|
8d8d84bae4 | ||
|
|
9f4cb98de6 | ||
|
|
72c16097d6 | ||
|
|
2203ea9301 | ||
|
|
28c9f2adab | ||
|
|
d04f7ffe87 | ||
|
|
21e5c01f11 | ||
|
|
ae6c1a30ac | ||
|
|
1040198ce1 | ||
|
|
69878b7847 | ||
|
|
ec0201cb85 | ||
|
|
5d7fb1d945 | ||
|
|
33542cb909 | ||
|
|
549dc286d0 | ||
|
|
f547befeb9 | ||
|
|
a144059c7c | ||
|
|
b96361e837 | ||
|
|
cd5994017c | ||
|
|
431fb62803 | ||
|
|
9e9de4c99e | ||
|
|
d3369368db | ||
|
|
b24de3e35b | ||
|
|
0344442d42 | ||
|
|
0decd0955b | ||
|
|
6f7fdbefac | ||
|
|
dbd466b6b2 | ||
|
|
4914481105 | ||
|
|
8514c01a91 | ||
|
|
751c20984f | ||
|
|
81b1cbd367 | ||
|
|
a72383da0e | ||
|
|
eb8213c4e7 | ||
|
|
4d9f522f22 | ||
|
|
528f42c713 | ||
|
|
7cc7179b05 | ||
|
|
cfaad685a4 | ||
|
|
e420fa9779 | ||
|
|
fbf1160357 | ||
|
|
a3e3585e50 | ||
|
|
0f9849e672 | ||
|
|
3be037221a | ||
|
|
b2e108afcc | ||
|
|
114a5f8cca | ||
|
|
aba54ba18f | ||
|
|
6a7768367e | ||
|
|
3a1ad6e844 | ||
|
|
40abd82e2d | ||
|
|
2933a86458 | ||
|
|
ecf0d0b388 | ||
|
|
b84ca04975 | ||
|
|
abb88662c1 | ||
|
|
73af5be1c3 | ||
|
|
81e3d269dc | ||
|
|
3dafa98eef | ||
|
|
3699b96adf | ||
|
|
f782af8ab3 | ||
|
|
2af48e40a1 | ||
|
|
389227bce8 | ||
|
|
8cd1952da3 | ||
|
|
f128fcd86c | ||
|
|
d890d02b5c | ||
|
|
defd5541b0 | ||
|
|
8624a0abce | ||
|
|
13b79c6781 | ||
|
|
2926915a06 | ||
|
|
536dc47eb0 | ||
|
|
225843265e | ||
|
|
a1f98603a6 | ||
|
|
ab59b33606 | ||
|
|
1f1e934675 | ||
|
|
582fffca93 | ||
|
|
e9b3a24221 | ||
|
|
0d5ac078f6 | ||
|
|
f109164f88 | ||
|
|
765c1104c4 | ||
|
|
a8de8aecf5 | ||
|
|
20c1bcd654 | ||
|
|
6eaad90535 | ||
|
|
91a5c562d8 | ||
|
|
70d5593ace | ||
|
|
f3401243be | ||
|
|
19dfeca96d | ||
|
|
774db56762 | ||
|
|
d15c535f86 | ||
|
|
a429f2f626 | ||
|
|
dfbc7e8b33 | ||
|
|
b41f10c1b9 | ||
|
|
44745a757d | ||
|
|
47bf2d408b | ||
|
|
c71f06be9e | ||
|
|
50b1fa5deb | ||
|
|
a99057754d | ||
|
|
6f16ae3e00 | ||
|
|
38d80aab99 | ||
|
|
d8939e0bb0 | ||
|
|
f376abf0fe | ||
|
|
64e75a8e08 | ||
|
|
67a210f722 | ||
|
|
a2df276880 | ||
|
|
151d293aaf | ||
|
|
13f6f3259d | ||
|
|
da6eea7743 | ||
|
|
b28fa8b994 | ||
|
|
7ed7c22469 | ||
|
|
6a4f06cf4a | ||
|
|
dfe0079573 | ||
|
|
a983d65404 | ||
|
|
2aa90fdc14 | ||
|
|
a81bc28afb | ||
|
|
70e40183bb | ||
|
|
bdfc1ddfc3 | ||
|
|
fb770f5633 | ||
|
|
9789648175 | ||
|
|
94f4c92a03 | ||
|
|
3cac4a598f | ||
|
|
654fea8a3b | ||
|
|
838d341d91 | ||
|
|
fd4143e686 | ||
|
|
2e3b2db03b | ||
|
|
51fa336350 | ||
|
|
0a3c0ecea2 | ||
|
|
2d644ac066 | ||
|
|
d8768c5377 | ||
|
|
65c1387a04 | ||
|
|
3ad1f2d0d0 | ||
|
|
af31f3b291 | ||
|
|
2f81c99143 | ||
|
|
e679b336ef | ||
|
|
82c17632f7 | ||
|
|
59b9b7dc91 | ||
|
|
d242d5330c | ||
|
|
05c606b65d | ||
|
|
52578b03b4 | ||
|
|
8978a54479 | ||
|
|
b44598cb7a | ||
|
|
48e05b6110 | ||
|
|
3640bc7f88 | ||
|
|
531410820f | ||
|
|
d08eba7308 | ||
|
|
c1bfa0a31a | ||
|
|
155ca14b19 | ||
|
|
3ee5991896 | ||
|
|
7ec7e11634 | ||
|
|
17a6224a3f | ||
|
|
e9bcb1bbb2 | ||
|
|
d8080910c6 | ||
|
|
02a31caa05 | ||
|
|
830b95bdcb | ||
|
|
2de86eb0f4 | ||
|
|
9616c59ed8 | ||
|
|
f27e9f3089 | ||
|
|
9321408154 | ||
|
|
703b9accb8 | ||
|
|
aca0662a64 | ||
|
|
bf0e2b3b52 | ||
|
|
97cc3082e1 | ||
|
|
b7b4de7c8d | ||
|
|
dc9cf74be8 | ||
|
|
4e826fb3e9 | ||
|
|
553513c4c6 | ||
|
|
5cfa2d0692 | ||
|
|
3da7071327 | ||
|
|
c3293d110c | ||
|
|
78eb443614 | ||
|
|
97cadfe5d3 | ||
|
|
87d8603d1d | ||
|
|
7dc68cabcc | ||
|
|
7c8d13c51a | ||
|
|
14f9aef55c | ||
|
|
da3eddeb26 | ||
|
|
43dadc763c | ||
|
|
64c185f097 | ||
|
|
1390c86fc4 | ||
|
|
9d2ef4d3e8 | ||
|
|
6840f6cb26 | ||
|
|
61280e6072 | ||
|
|
b7d70ac928 | ||
|
|
826577c88f | ||
|
|
5fa185d480 | ||
|
|
75271ca1a0 | ||
|
|
f03e58f5f6 | ||
|
|
596110dd65 | ||
|
|
76018f5b9c | ||
|
|
1019f6d158 | ||
|
|
fc0db1941a | ||
|
|
f25b38caf5 | ||
|
|
edf1fcb742 | ||
|
|
4b66fcad64 | ||
|
|
334c4d0676 | ||
|
|
ac2ef21896 | ||
|
|
b529dc7dd1 | ||
|
|
4b24f8bc04 | ||
|
|
0c2e9480cb | ||
|
|
6e833cce6a | ||
|
|
901f83edd5 | ||
|
|
80d4dc2016 | ||
|
|
3a279db06b | ||
|
|
3053254db7 | ||
|
|
d135193f6c | ||
|
|
183ac41550 | ||
|
|
322fbe92ee | ||
|
|
367b7eeeba | ||
|
|
dd8fff6d43 | ||
|
|
3e3bdf7491 | ||
|
|
9e99eda3c3 | ||
|
|
05b9432f6d | ||
|
|
422824b9e7 | ||
|
|
2d2140aad0 | ||
|
|
a24fe951ed | ||
|
|
2de69e2b12 | ||
|
|
4bae4194ab | ||
|
|
7b3a78e04e | ||
|
|
219310e817 | ||
|
|
298a5699f1 | ||
|
|
bf34c94483 | ||
|
|
a7d8202b4e | ||
|
|
2ae94b2af2 | ||
|
|
e607f3c78d | ||
|
|
d69361b1c9 | ||
|
|
949d7f4b53 | ||
|
|
01dfea3ffa | ||
|
|
8b9860902c | ||
|
|
3d8eac9b5a | ||
|
|
a8fe0e89a8 | ||
|
|
9b52d89e03 | ||
|
|
e28c1e9c4b | ||
|
|
1e16a987dd | ||
|
|
c045c9a6dd | ||
|
|
976abf7b3c | ||
|
|
8141c6504e | ||
|
|
ffacf4222b | ||
|
|
aea8271f7e | ||
|
|
72bc539ffd | ||
|
|
926fd41a2b | ||
|
|
f2bc064da2 | ||
|
|
555be2be11 | ||
|
|
8da28dcfb2 | ||
|
|
5741f32080 | ||
|
|
22b17de2b4 | ||
|
|
96062dec10 | ||
|
|
29fc975fb8 | ||
|
|
0052ca9173 | ||
|
|
54791e938b | ||
|
|
709be13e82 | ||
|
|
7893a957cb | ||
|
|
a9146efc17 | ||
|
|
aee2cc2e03 | ||
|
|
25fe08eb74 | ||
|
|
307dcea097 | ||
|
|
0dae0a05d4 | ||
|
|
886256c86b | ||
|
|
372a7e905c | ||
|
|
3f820734b6 | ||
|
|
0a41ccda99 | ||
|
|
18500b8e3a | ||
|
|
bebd70d752 | ||
|
|
527781a588 |
@@ -2,8 +2,9 @@ import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.30.0"
|
||||
__version__ = "15.35.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
def check_app_permission():
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
if is_website_user():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -202,7 +202,7 @@ class Account(NestedSet):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"Utility Expenses": {},
|
||||
"Write Off": {},
|
||||
"Exchange Gain/Loss": {},
|
||||
"Gain/Loss on Asset Disposal": {}
|
||||
"Gain/Loss on Asset Disposal": {},
|
||||
"Impairment": {}
|
||||
},
|
||||
"root_type": "Expense"
|
||||
},
|
||||
@@ -132,7 +133,8 @@
|
||||
"Source of Funds (Liabilities)": {
|
||||
"Capital Account": {
|
||||
"Reserves and Surplus": {},
|
||||
"Shareholders Funds": {}
|
||||
"Shareholders Funds": {},
|
||||
"Revaluation Surplus": {}
|
||||
},
|
||||
"Current Liabilities": {
|
||||
"Accounts Payable": {
|
||||
|
||||
@@ -72,6 +72,7 @@ def get():
|
||||
_("Write Off"): {},
|
||||
_("Exchange Gain/Loss"): {},
|
||||
_("Gain/Loss on Asset Disposal"): {},
|
||||
_("Impairment"): {},
|
||||
},
|
||||
"root_type": "Expense",
|
||||
},
|
||||
@@ -104,6 +105,7 @@ def get():
|
||||
_("Dividends Paid"): {"account_type": "Equity"},
|
||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||
_("Retained Earnings"): {"account_type": "Equity"},
|
||||
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||
"root_type": "Equity",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
"receivable_payable_remarks_length",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -475,6 +477,18 @@
|
||||
"fieldname": "calculate_depr_using_total_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -482,7 +496,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-12 00:24:20.957726",
|
||||
"modified": "2024-07-26 06:48:52.714630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -35,6 +35,7 @@ class AccountsSettings(Document):
|
||||
book_tax_discount_loss: DF.Check
|
||||
calculate_depr_using_total_days: DF.Check
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
|
||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
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 +50,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 +59,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
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -96,8 +96,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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -47,9 +47,11 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 8:
|
||||
if no_of_columns != 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
_(
|
||||
"Columns are not according to template. Please compare the uploaded file with standard template"
|
||||
),
|
||||
title=(_("Wrong Template")),
|
||||
)
|
||||
|
||||
|
||||
@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
},
|
||||
|
||||
make_jv: function (frm) {
|
||||
let revaluation_journal = null;
|
||||
let zero_balance_journal = null;
|
||||
frappe.call({
|
||||
method: "make_jv_entries",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: "Making Journal Entries...",
|
||||
freeze_message: __("Creating Journal Entries..."),
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let response = r.message;
|
||||
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
|
||||
frappe.msgprint(__("Journals have been created"));
|
||||
frappe.msgprint(__("Journal entries have been created"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,30 +25,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
refresh: function (frm) {
|
||||
erpnext.toggle_naming_series();
|
||||
|
||||
if (frm.doc.repost_required && frm.doc.docstatus === 1) {
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
frm.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted."));
|
||||
frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Ledger"),
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"stock_entry",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
"repost_required"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -544,15 +543,6 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -567,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",
|
||||
@@ -618,4 +608,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ class JournalEntry(AccountsController):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import (
|
||||
JournalEntryAccount,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -197,14 +195,10 @@ class JournalEntry(AccountsController):
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"accounts": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
|
||||
@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
frm.refresh_fields();
|
||||
frm.page.clear_indicator();
|
||||
frm.dashboard.hide_progress();
|
||||
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
|
||||
|
||||
if (frm.doc.invoice_type == "Sales") {
|
||||
frappe.msgprint(__("Opening Sales Invoices have been created."));
|
||||
} else {
|
||||
frappe.msgprint(__("Opening Purchase Invoices have been created."));
|
||||
}
|
||||
},
|
||||
1500,
|
||||
data.title
|
||||
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||
frm.page.set_primary_action(__("Create Invoices"), () => {
|
||||
let btn_primary = frm.page.btn_primary.get(0);
|
||||
let freeze_message;
|
||||
if (frm.doc.invoice_type == "Sales") {
|
||||
freeze_message = __("Creating Sales Invoices ...");
|
||||
} else {
|
||||
freeze_message = __("Creating Purchase Invoices ...");
|
||||
}
|
||||
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
freeze_message: freeze_message,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
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],
|
||||
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
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],
|
||||
@@ -165,8 +173,25 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
@@ -213,7 +238,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
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.toggle_display(
|
||||
@@ -280,7 +305,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
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.set_currency_labels(
|
||||
@@ -395,6 +420,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +658,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -37,7 +37,6 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -1119,90 +1118,82 @@ class PaymentEntry(AccountsController):
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1749,7 +1740,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
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"),
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,6 +267,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}%"))
|
||||
|
||||
@@ -1805,6 +1805,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
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):
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"transaction_date",
|
||||
"column_break_2",
|
||||
"naming_series",
|
||||
"company",
|
||||
"mode_of_payment",
|
||||
"party_details",
|
||||
"party_type",
|
||||
@@ -144,6 +145,7 @@
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
@@ -389,13 +391,20 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-27 09:51:42.277638",
|
||||
"modified": "2024-08-07 16:39:54.288002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -433,4 +442,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,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.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -338,6 +339,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)})
|
||||
|
||||
@@ -480,6 +492,7 @@ def make_payment_request(**args):
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
@@ -500,8 +513,11 @@ def make_payment_request(**args):
|
||||
if args.order_type == "Shopping Cart" or args.mute_email:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
pr.insert(ignore_permissions=True)
|
||||
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
if pr.get("__unsaved"):
|
||||
pr.insert(ignore_permissions=True)
|
||||
pr.submit()
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
@@ -540,7 +556,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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
|
||||
@@ -17,6 +17,10 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.selling.page.point_of_sale.point_of_sale import get_items
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
@@ -179,6 +183,94 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
accounting_dimension_department.save()
|
||||
disable_dimension()
|
||||
|
||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||
frappe.flags.print_message = False
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
item_doc = make_item(
|
||||
"_Test Item With Batch FOR POS Merge Test",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-PM-POS-MERGE-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
)
|
||||
|
||||
item_code = item_doc.name
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
use_serial_batch_fields=0,
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
pcv_doc.submit()
|
||||
|
||||
piv_merge = frappe.db.get_value("POS Invoice Merge Log", {"pos_closing_entry": pcv_doc.name}, "name")
|
||||
|
||||
self.assertTrue(piv_merge)
|
||||
piv_merge_doc = frappe.get_doc("POS Invoice Merge Log", piv_merge)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[0].pos_invoice)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[1].pos_invoice)
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(pos_inv.consolidated_invoice)
|
||||
pos_inv2.load_from_db()
|
||||
self.assertTrue(pos_inv2.consolidated_invoice)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 0.0)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
frappe.flags.print_message = True
|
||||
|
||||
pcv_doc.reload()
|
||||
pcv_doc.cancel()
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pos_inv.reload()
|
||||
pos_inv2.reload()
|
||||
|
||||
pos_inv.cancel()
|
||||
pos_inv2.cancel()
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 10.0)
|
||||
|
||||
|
||||
def init_user_and_profile(**args):
|
||||
user = "test@example.com"
|
||||
|
||||
@@ -188,7 +188,7 @@ class POSInvoice(SalesInvoice):
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
)
|
||||
|
||||
# run on validate method of selling controller
|
||||
@@ -229,7 +229,9 @@ class POSInvoice(SalesInvoice):
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
for table_name in ["items", "packed_items"]:
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.submit_serial_batch_bundle(table_name)
|
||||
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
@@ -283,10 +285,11 @@ class POSInvoice(SalesInvoice):
|
||||
{"is_cancelled": 1, "voucher_no": ""},
|
||||
)
|
||||
|
||||
frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
|
||||
def submit_serial_batch_bundle(self):
|
||||
for item in self.items:
|
||||
def submit_serial_batch_bundle(self, table_name):
|
||||
for item in self.get(table_name):
|
||||
if item.serial_and_batch_bundle:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
|
||||
@@ -355,10 +358,16 @@ class POSInvoice(SalesInvoice):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
error_msg = ""
|
||||
if d.get("has_serial_no") and not d.serial_and_batch_bundle:
|
||||
if d.get("has_serial_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.serial_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
|
||||
|
||||
elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
|
||||
elif d.get("has_batch_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.batch_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
|
||||
|
||||
if error_msg:
|
||||
|
||||
@@ -780,8 +780,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1.submit()
|
||||
pos_inv1.reload()
|
||||
|
||||
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
@@ -957,7 +955,7 @@ def create_pos_invoice(**args):
|
||||
pos_inv.set_missing_values()
|
||||
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
type_of_transaction = args.type_of_transaction or "Outward"
|
||||
|
||||
if pos_inv.is_return:
|
||||
@@ -998,6 +996,9 @@ def create_pos_invoice(**args):
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
"serial_no": args.serial_no if args.use_serial_batch_fields else None,
|
||||
"batch_no": args.batch_no if args.use_serial_batch_fields else None,
|
||||
}
|
||||
# append in pos invoice items without item_code by checking flag without_item_code
|
||||
if args.without_item_code:
|
||||
@@ -1023,6 +1024,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.insert()
|
||||
if not args.do_not_submit:
|
||||
pos_inv.submit()
|
||||
if args.use_serial_batch_fields:
|
||||
pos_inv.reload()
|
||||
else:
|
||||
pos_inv.payment_schedule = []
|
||||
else:
|
||||
|
||||
@@ -634,7 +634,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
@@ -655,7 +654,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
@@ -827,7 +825,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.serial_and_batch_bundle",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -853,7 +851,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:50:17.140269",
|
||||
"modified": "2024-05-07 15:56:53.343317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
@@ -863,4 +861,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg = _(
|
||||
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||
msg += " "
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
"The original invoice should be consolidated before or along with the return invoice."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
||||
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
@@ -131,6 +130,7 @@ class POSInvoiceMergeLog(Document):
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.serial_and_batch_bundle_reference_for_pos_invoice()
|
||||
self.cancel_linked_invoices()
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
@@ -191,6 +191,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
@@ -312,6 +313,12 @@ class POSInvoiceMergeLog(Document):
|
||||
doc.set_status(update=True)
|
||||
doc.save()
|
||||
|
||||
def serial_and_batch_bundle_reference_for_pos_invoice(self):
|
||||
for d in self.pos_invoices:
|
||||
pos_invoice = frappe.get_doc("POS Invoice", d.pos_invoice)
|
||||
for table_name in ["items", "packed_items"]:
|
||||
pos_invoice.set_serial_and_batch_bundle(table_name)
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
if not si_name:
|
||||
|
||||
@@ -186,7 +186,8 @@ class PricingRule(Document):
|
||||
if not self.priority:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||
frappe.bold(_("Priority")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,7 +195,7 @@ class PricingRule(Document):
|
||||
throw(
|
||||
_(
|
||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
||||
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
|
||||
@@ -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
|
||||
@@ -1311,6 +1313,69 @@ class TestPricingRule(unittest.TestCase):
|
||||
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"]
|
||||
|
||||
|
||||
@@ -486,7 +486,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
||||
bank_cash_account: DF.Link | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
default_advance_account: DF.Link
|
||||
error_log: DF.LongText | None
|
||||
from_invoice_date: DF.Date | None
|
||||
from_payment_date: DF.Date | None
|
||||
@@ -101,6 +102,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",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"ignore_cr_dr_notes",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -383,10 +384,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",
|
||||
|
||||
@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
@@ -133,6 +134,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)
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Promotional Scheme", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("for_price_list", "price_discount_slabs", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
selling: doc.selling,
|
||||
buying: doc.buying,
|
||||
currency: doc.currency,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.trigger("toggle_reqd_apply_on");
|
||||
|
||||
@@ -51,6 +51,7 @@ price_discount_fields = [
|
||||
"discount_percentage",
|
||||
"validate_applied_rule",
|
||||
"apply_multiple_pricing_rules",
|
||||
"for_price_list",
|
||||
]
|
||||
|
||||
product_discount_fields = [
|
||||
@@ -63,6 +64,7 @@ product_discount_fields = [
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
"round_free_qty",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"rate",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_11",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -120,6 +121,13 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.rate_or_discount!=\"Rate\"",
|
||||
"fieldname": "for_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -169,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-16 00:25:33.843996",
|
||||
"modified": "2024-07-23 12:33:46.574950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
@@ -177,4 +185,4 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class PromotionalSchemePriceDiscount(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
for_price_list: DF.Link | None
|
||||
max_amount: DF.Currency
|
||||
max_qty: DF.Float
|
||||
min_amount: DF.Currency
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"column_break_9",
|
||||
"free_item_uom",
|
||||
"free_item_rate",
|
||||
"round_free_qty",
|
||||
"section_break_12",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -181,12 +182,18 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-12 12:53:58.199108",
|
||||
"modified": "2024-07-22 17:25:07.880984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
@@ -195,4 +202,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
round_free_qty: DF.Check
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -77,31 +77,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"auto_repeat",
|
||||
@@ -364,7 +363,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",
|
||||
@@ -1603,15 +1603,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",
|
||||
@@ -1639,7 +1630,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-11 11:28:42.802211",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -159,7 +159,6 @@ class PurchaseInvoice(BuyingController):
|
||||
rejected_warehouse: DF.Link | None
|
||||
release_date: DF.Date | None
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -286,7 +285,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.create_remarks()
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
validate_inter_company_party(
|
||||
@@ -323,10 +321,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def create_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.bill_no and self.bill_date:
|
||||
self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
|
||||
self.bill_no, formatdate(self.bill_date)
|
||||
)
|
||||
if self.bill_no:
|
||||
self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no)
|
||||
if self.bill_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
@@ -378,16 +377,16 @@ class PurchaseInvoice(BuyingController):
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold("Credit To")),
|
||||
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold(_("Credit To"))),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
||||
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
@@ -635,7 +634,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Order Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
@@ -656,7 +655,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Receipt Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
@@ -748,6 +747,9 @@ class PurchaseInvoice(BuyingController):
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def before_submit(self):
|
||||
self.create_remarks()
|
||||
|
||||
def on_submit(self):
|
||||
super().on_submit()
|
||||
|
||||
@@ -796,19 +798,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",
|
||||
"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.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
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"
|
||||
@@ -1265,7 +1265,11 @@ class PurchaseInvoice(BuyingController):
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
@@ -1705,6 +1709,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
|
||||
|
||||
@@ -1746,8 +1753,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",
|
||||
@@ -1762,7 +1767,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)
|
||||
|
||||
@@ -2014,8 +2014,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
@change_settings("Buying Settings", {"supplier_group": None})
|
||||
def test_purchase_invoice_without_supplier_group(self):
|
||||
@@ -2238,6 +2236,62 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 6300
|
||||
pr.plc_conversion_rate = 1
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.conversion_rate, 6300)
|
||||
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=3000,
|
||||
distribute_charges_based_on="Qty",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 1.1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -958,12 +959,22 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -68,31 +68,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
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";
|
||||
}
|
||||
@@ -157,7 +132,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
const payment_is_overdue = doc.payment_schedule
|
||||
.map((row) => Date.parse(row.due_date) < Date.now())
|
||||
.reduce((prev, current) => prev || current);
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -502,11 +477,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
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",
|
||||
args: {
|
||||
for_validate: true,
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
|
||||
@@ -213,7 +213,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2125,15 +2124,6 @@
|
||||
"label": "Write Off",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "incoterm",
|
||||
"fieldtype": "Link",
|
||||
@@ -2188,7 +2178,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-06-07 16:49:32.458402",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -161,7 +161,6 @@ class SalesInvoice(SellingController):
|
||||
project: DF.Link | None
|
||||
redeem_loyalty_points: DF.Check
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -279,7 +278,6 @@ class SalesInvoice(SellingController):
|
||||
self.check_sales_order_on_hold_or_close("sales_order")
|
||||
self.validate_debit_to_acc()
|
||||
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
|
||||
self.add_remarks()
|
||||
self.validate_fixed_asset()
|
||||
self.set_income_account_for_fixed_assets()
|
||||
self.validate_item_cost_centers()
|
||||
@@ -423,6 +421,9 @@ class SalesInvoice(SellingController):
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
|
||||
def before_submit(self):
|
||||
self.add_remarks()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_pos_paid_amount()
|
||||
|
||||
@@ -515,7 +516,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
@@ -556,7 +557,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)
|
||||
@@ -706,25 +706,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",
|
||||
"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.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
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
|
||||
@@ -862,7 +860,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = (
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||
+ " "
|
||||
)
|
||||
msg += _(
|
||||
@@ -873,7 +871,7 @@ class SalesInvoice(SellingController):
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = (
|
||||
_("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
||||
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||
)
|
||||
+ " "
|
||||
)
|
||||
@@ -950,10 +948,11 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
self.remarks = _("Against Customer Order {0} dated {1}").format(
|
||||
self.po_no, formatdate(self.po_date)
|
||||
)
|
||||
if self.po_no:
|
||||
self.remarks = _("Against Customer Order {0}").format(self.po_no)
|
||||
if self.po_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
@@ -1213,6 +1212,8 @@ class SalesInvoice(SellingController):
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
@@ -1318,6 +1319,10 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
@@ -1376,37 +1381,33 @@ class SalesInvoice(SellingController):
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -1481,6 +1482,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(
|
||||
@@ -1494,7 +1499,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,
|
||||
},
|
||||
@@ -2226,6 +2231,11 @@ def make_inter_company_purchase_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def make_regional_gl_entries(gl_entries, doc):
|
||||
return gl_entries
|
||||
|
||||
|
||||
def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
|
||||
@@ -5,9 +5,10 @@ import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -2952,9 +2953,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.
|
||||
@@ -3086,6 +3084,128 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "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
|
||||
|
||||
@@ -3761,6 +3881,130 @@ 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 test_invoice_remarks(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.po_no = "Test PO"
|
||||
si.po_date = nowdate()
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}")
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -38,6 +38,12 @@ frappe.ui.form.on("Subscription", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Force-Fetch Subscription Updates"),
|
||||
() => frm.trigger("force_fetch_subscription_updates"),
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Cancel Subscription"),
|
||||
() => frm.trigger("cancel_this_subscription"),
|
||||
@@ -82,4 +88,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();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -717,6 +717,28 @@ class Subscription(Document):
|
||||
self.update_subscription_period(posting_date or nowdate())
|
||||
self.save()
|
||||
|
||||
@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 is_prorate() -> int:
|
||||
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
|
||||
|
||||
@@ -521,6 +521,18 @@ class TestSubscription(FrappeTestCase):
|
||||
subscription.process(posting_date="2023-01-22")
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
def test_future_subscription(self):
|
||||
"""Force-Fetch should not process future subscriptions"""
|
||||
subscription = create_subscription(
|
||||
start_date=add_months(nowdate(), 1),
|
||||
submit_invoice=0,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
party="_Test Subscription Customer John Doe",
|
||||
)
|
||||
subscription.force_fetch_subscription_updates()
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -268,6 +268,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,
|
||||
@@ -275,7 +280,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":
|
||||
@@ -369,12 +375,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,
|
||||
)
|
||||
@@ -387,6 +395,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
|
||||
@@ -477,6 +499,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)"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -61,32 +61,10 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "payment_terms_template",
|
||||
@@ -162,6 +140,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
label: __("In Party Currency"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "handle_employee_advances",
|
||||
label: __("Handle Employee Advances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -30,10 +30,7 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Supplier",
|
||||
"party": [self.supplier],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
|
||||
@@ -89,32 +89,10 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "customer_group",
|
||||
|
||||
@@ -50,6 +50,11 @@ class ReceivablePayableReport:
|
||||
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
|
||||
)
|
||||
|
||||
if not self.filters.range:
|
||||
self.filters.range = "30, 60, 90, 120"
|
||||
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
||||
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
self.set_defaults()
|
||||
@@ -112,6 +117,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 +148,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 +217,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 +275,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 +310,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
|
||||
@@ -716,37 +738,22 @@ class ReceivablePayableReport:
|
||||
|
||||
# ageing buckets should not have amounts if due date is not reached
|
||||
if getdate(entry_date) > getdate(self.filters.report_date):
|
||||
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
|
||||
row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
|
||||
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
|
||||
|
||||
def get_ageing_data(self, entry_date, row):
|
||||
# [0-30, 30-60, 60-90, 90-120, 120-above]
|
||||
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
|
||||
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
|
||||
|
||||
if not (self.age_as_on and entry_date):
|
||||
return
|
||||
|
||||
row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0
|
||||
index = None
|
||||
|
||||
if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4):
|
||||
self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = (
|
||||
30,
|
||||
60,
|
||||
90,
|
||||
120,
|
||||
)
|
||||
|
||||
for i, days in enumerate(
|
||||
[self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]
|
||||
):
|
||||
if cint(row.age) <= cint(days):
|
||||
index = i
|
||||
break
|
||||
|
||||
if index is None:
|
||||
index = 4
|
||||
index = next(
|
||||
(i for i, days in enumerate(self.ranges) if cint(row.age) <= cint(days)), len(self.ranges)
|
||||
)
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
@@ -1058,6 +1065,7 @@ class ReceivablePayableReport:
|
||||
self.add_column(_("Debit Note"), fieldname="credit_note")
|
||||
self.add_column(_("Outstanding Amount"), fieldname="outstanding")
|
||||
|
||||
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
|
||||
self.setup_ageing_columns()
|
||||
|
||||
self.add_column(
|
||||
@@ -1116,34 +1124,26 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
|
||||
ranges = [*self.ranges, "Above"]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
label = f"{prev_range_value}-{curr_range_value}"
|
||||
self.add_column(label=label, fieldname="range" + str(idx + 1))
|
||||
|
||||
for i, label in enumerate(
|
||||
[
|
||||
"0-{range1}".format(range1=self.filters["range1"]),
|
||||
"{range1}-{range2}".format(
|
||||
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
|
||||
),
|
||||
"{range2}-{range3}".format(
|
||||
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
|
||||
),
|
||||
"{range3}-{range4}".format(
|
||||
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
|
||||
),
|
||||
_("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1),
|
||||
]
|
||||
):
|
||||
self.add_column(label=label, fieldname="range" + str(i + 1))
|
||||
self.ageing_column_labels.append(label)
|
||||
|
||||
if curr_range_value.isdigit():
|
||||
prev_range_value = cint(curr_range_value) + 1
|
||||
|
||||
def get_chart_data(self):
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
rows = []
|
||||
for row in self.data:
|
||||
row = frappe._dict(row)
|
||||
if not cint(row.bold):
|
||||
values = [row.range1, row.range2, row.range3, row.range4, row.range5]
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
rows.append({"values": [flt(val, precision) for val in values]})
|
||||
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
|
||||
rows.append({"values": values})
|
||||
|
||||
self.chart = {
|
||||
"data": {"labels": self.ageing_column_labels, "datasets": rows},
|
||||
|
||||
@@ -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(
|
||||
@@ -81,10 +83,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party": [self.customer],
|
||||
"report_date": add_days(today(), 2),
|
||||
"based_on_payment_terms": 0,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": False,
|
||||
}
|
||||
|
||||
@@ -114,10 +113,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
@@ -170,10 +166,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
@@ -264,10 +257,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 0,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -326,10 +316,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
report = execute(filters)
|
||||
|
||||
@@ -395,10 +382,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
@@ -414,10 +398,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"group_by_party": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -491,10 +472,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_future_payments": True,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -553,10 +531,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"sales_person": sales_person.name,
|
||||
"show_sales_person": True,
|
||||
}
|
||||
@@ -573,10 +548,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -591,10 +563,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"customer_group": cus_group,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -616,10 +585,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"customer_group": cus_groups_list, # Use the list of customer groups
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -658,10 +624,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"party_account": self.debit_to,
|
||||
}
|
||||
report = execute(filters)[1]
|
||||
@@ -709,10 +672,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
@@ -752,10 +712,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer1, self.customer3],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
@@ -835,10 +792,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
report_ouput = execute(filters)[1]
|
||||
@@ -901,10 +855,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
{
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_future_payments": True,
|
||||
"in_party_currency": False,
|
||||
}
|
||||
@@ -955,3 +906,63 @@ 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(),
|
||||
"range": "30, 60, 90, 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(),
|
||||
"range": "30, 60, 90, 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])
|
||||
|
||||
@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range1",
|
||||
label: __("Ageing Range 1"),
|
||||
fieldtype: "Int",
|
||||
default: "30",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range2",
|
||||
label: __("Ageing Range 2"),
|
||||
fieldtype: "Int",
|
||||
default: "60",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range3",
|
||||
label: __("Ageing Range 3"),
|
||||
fieldtype: "Int",
|
||||
default: "90",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "range4",
|
||||
label: __("Ageing Range 4"),
|
||||
fieldtype: "Int",
|
||||
default: "120",
|
||||
reqd: 1,
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
fieldtype: "Data",
|
||||
default: "30, 60, 90, 120",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
|
||||
@@ -104,25 +104,23 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.set_party_details(d)
|
||||
|
||||
def init_party_total(self, row):
|
||||
default_dict = {
|
||||
"invoiced": 0.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 0.0,
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
for i in self.range_numbers:
|
||||
range_key = f"range{i}"
|
||||
default_dict[range_key] = 0.0
|
||||
|
||||
self.party_total.setdefault(
|
||||
row.party,
|
||||
frappe._dict(
|
||||
{
|
||||
"invoiced": 0.0,
|
||||
"paid": 0.0,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": 0.0,
|
||||
"range1": 0.0,
|
||||
"range2": 0.0,
|
||||
"range3": 0.0,
|
||||
"range4": 0.0,
|
||||
"range5": 0.0,
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
),
|
||||
frappe._dict(default_dict),
|
||||
)
|
||||
|
||||
def set_party_details(self, row):
|
||||
@@ -173,6 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_("Difference"), fieldname="diff")
|
||||
|
||||
self.setup_ageing_columns()
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
@@ -206,27 +205,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80
|
||||
)
|
||||
|
||||
def setup_ageing_columns(self):
|
||||
for i, label in enumerate(
|
||||
[
|
||||
"0-{range1}".format(range1=self.filters["range1"]),
|
||||
"{range1}-{range2}".format(
|
||||
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
|
||||
),
|
||||
"{range2}-{range3}".format(
|
||||
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
|
||||
),
|
||||
"{range3}-{range4}".format(
|
||||
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
|
||||
),
|
||||
"{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")),
|
||||
]
|
||||
):
|
||||
self.add_column(label=label, fieldname="range" + str(i + 1))
|
||||
|
||||
# Add column for total due amount
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
|
||||
|
||||
def get_gl_balance(report_date, company):
|
||||
return frappe._dict(
|
||||
|
||||
@@ -27,10 +27,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
@@ -121,10 +118,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -69,6 +69,9 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
@@ -119,6 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -128,6 +132,10 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and name = %(asset)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = `tabAsset`.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT name,
|
||||
@@ -176,6 +184,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -154,8 +154,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`
|
||||
|
||||
@@ -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},
|
||||
]
|
||||
@@ -104,7 +104,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,
|
||||
@@ -175,7 +175,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
|
||||
)
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@
|
||||
<br>
|
||||
{% } %}
|
||||
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% if(data[i].bill_no) { %}
|
||||
{% if(data[i].remarks) { %}
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% } else if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</span>
|
||||
|
||||
@@ -209,6 +209,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",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -236,6 +236,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")
|
||||
|
||||
|
||||
@@ -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_entries()
|
||||
|
||||
def clear_old_entries(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)
|
||||
|
||||
@@ -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
|
||||
@@ -315,8 +315,9 @@ def apply_conditions(query, pi, pii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
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)
|
||||
@@ -353,6 +354,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.where(pii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
|
||||
@@ -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)
|
||||
@@ -410,8 +410,9 @@ def apply_group_by_conditions(query, si, ii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
@@ -459,6 +460,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
|
||||
@@ -336,7 +336,7 @@ def get_tds_docs(filters):
|
||||
def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
if not tds_accounts:
|
||||
frappe.throw(
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
|
||||
title=_("Accounts Missing Error"),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -14,8 +14,8 @@ DEFAULT_FILTERS = {
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
|
||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||
("Accounts Payable", {"range": "30, 60, 90, 120"}),
|
||||
("Accounts Receivable", {"range": "30, 60, 90, 120"}),
|
||||
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
||||
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}),
|
||||
("Consolidated Financial Statement", {"report": "Cash Flow"}),
|
||||
|
||||
@@ -781,6 +781,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
|
||||
):
|
||||
@@ -1598,6 +1638,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:
|
||||
|
||||
@@ -188,11 +188,21 @@ frappe.ui.form.on("Asset", {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -496,6 +506,7 @@ frappe.ui.form.on("Asset", {
|
||||
create_asset_repair: function (frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
asset: frm.doc.name,
|
||||
asset_name: frm.doc.asset_name,
|
||||
},
|
||||
@@ -510,6 +521,7 @@ frappe.ui.form.on("Asset", {
|
||||
create_asset_capitalization: function (frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
asset: frm.doc.name,
|
||||
asset_name: frm.doc.asset_name,
|
||||
item_code: frm.doc.item_code,
|
||||
@@ -518,6 +530,7 @@ frappe.ui.form.on("Asset", {
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
$(".primary-action").prop("hidden", false);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -660,6 +673,11 @@ frappe.ui.form.on("Asset", {
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -775,11 +793,8 @@ frappe.ui.form.on("Asset Finance Book", {
|
||||
|
||||
depreciation_start_date: function (frm, cdt, cdn) {
|
||||
const book = locals[cdt][cdn];
|
||||
if (
|
||||
frm.doc.available_for_use_date &&
|
||||
book.depreciation_start_date == frm.doc.available_for_use_date
|
||||
) {
|
||||
frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date."));
|
||||
if (frm.doc.available_for_use_date && book.depreciation_start_date < frm.doc.available_for_use_date) {
|
||||
frappe.msgprint(__("Depreciation Posting Date cannot be before Available-for-use Date"));
|
||||
book.depreciation_start_date = "";
|
||||
frm.refresh_field("finance_books");
|
||||
}
|
||||
|
||||
@@ -33,14 +33,16 @@
|
||||
"dimension_col_break",
|
||||
"purchase_details_section",
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
"purchase_invoice",
|
||||
"purchase_invoice_item",
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"total_asset_cost",
|
||||
"additional_asset_cost",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
"purchase_date",
|
||||
"additional_asset_cost",
|
||||
"total_asset_cost",
|
||||
"section_break_23",
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
@@ -221,7 +223,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
@@ -537,6 +538,20 @@
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -580,7 +595,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-07 22:27:14.733839",
|
||||
"modified": "2024-08-26 23:28:29.095139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -94,7 +94,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -123,7 +125,6 @@ class Asset(AccountsController):
|
||||
self.validate_cost_center()
|
||||
self.set_missing_values()
|
||||
self.validate_gross_and_purchase_amount()
|
||||
self.validate_expected_value_after_useful_life()
|
||||
self.validate_finance_books()
|
||||
|
||||
if not self.split_from:
|
||||
@@ -144,6 +145,7 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
self.validate_expected_value_after_useful_life()
|
||||
self.set_total_booked_depreciations()
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
self.status = self.get_status()
|
||||
@@ -267,10 +269,10 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Available for use date is required"))
|
||||
|
||||
for d in self.finance_books:
|
||||
if d.depreciation_start_date == self.available_for_use_date:
|
||||
if getdate(d.depreciation_start_date) < getdate(self.available_for_use_date):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Depreciation Posting Date should not be equal to Available for Use Date."
|
||||
"Depreciation Row {0}: Depreciation Posting Date cannot be before Available-for-use Date"
|
||||
).format(d.idx),
|
||||
title=_("Incorrect Date"),
|
||||
)
|
||||
@@ -669,7 +671,7 @@ class Asset(AccountsController):
|
||||
if not fixed_asset_account:
|
||||
frappe.throw(
|
||||
_("Set {0} in asset category {1} for company {2}").format(
|
||||
frappe.bold("Fixed Asset Account"),
|
||||
frappe.bold(_("Fixed Asset Account")),
|
||||
frappe.bold(self.asset_category),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
@@ -691,12 +693,17 @@ class Asset(AccountsController):
|
||||
return cwip_account
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.check_asset_capitalization_gl_entries():
|
||||
return
|
||||
|
||||
gl_entries = []
|
||||
|
||||
purchase_document = self.get_purchase_document()
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
|
||||
self.available_for_use_date
|
||||
) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -733,6 +740,24 @@ class Asset(AccountsController):
|
||||
make_gl_entries(gl_entries)
|
||||
self.db_set("booked_fixed_asset", 1)
|
||||
|
||||
def check_asset_capitalization_gl_entries(self):
|
||||
if self.is_composite_asset:
|
||||
result = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1},
|
||||
["name", "target_fixed_asset_account"],
|
||||
)
|
||||
|
||||
if result:
|
||||
asset_capitalization, target_fixed_asset_account = result
|
||||
# Check GL entries for the retrieved Asset Capitalization and target fixed asset account
|
||||
return has_gl_entries(
|
||||
"Asset Capitalization", asset_capitalization, target_fixed_asset_account
|
||||
)
|
||||
# return if there are no submitted capitalization for given asset
|
||||
return True
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_depreciation_rate(self, args, on_validate=False):
|
||||
if isinstance(args, str):
|
||||
@@ -779,6 +804,22 @@ class Asset(AccountsController):
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
|
||||
def has_gl_entries(doctype, docname, target_account):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(gl_entry.account)
|
||||
.where(
|
||||
(gl_entry.voucher_type == doctype)
|
||||
& (gl_entry.voucher_no == docname)
|
||||
& (gl_entry.debit != 0)
|
||||
& (gl_entry.account == target_account)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return len(gl_entries) > 0
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
|
||||
@@ -854,18 +895,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_repair(asset, asset_name):
|
||||
def create_asset_repair(company, asset, asset_name):
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"asset": asset, "asset_name": asset_name})
|
||||
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
|
||||
return asset_repair
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_capitalization(asset, asset_name, item_code):
|
||||
def create_asset_capitalization(company, asset, asset_name, item_code):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"target_asset": asset,
|
||||
"company": company,
|
||||
"capitalization_method": "Choose a WIP composite asset",
|
||||
"target_asset_name": asset_name,
|
||||
"target_item_code": item_code,
|
||||
@@ -904,7 +946,7 @@ def transfer_asset(args):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
books.append(
|
||||
@@ -1035,6 +1077,14 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
return asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_active_capitalization(asset):
|
||||
active_capitalizations = frappe.db.count(
|
||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||
)
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -234,7 +234,7 @@ class TestAsset(AssetSetup):
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
add_days(get_last_day(add_months(purchase_date, 1)), 1),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
@@ -320,7 +320,7 @@ class TestAsset(AssetSetup):
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
add_days(get_last_day(add_months(purchase_date, 1)), 1),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
@@ -740,7 +740,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2030-06-06",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=47095.89,
|
||||
opening_accumulated_depreciation=47178.08,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2032-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
@@ -748,7 +748,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
|
||||
expected_schedules = [["2032-12-31", 30000.0, 77178.08], ["2033-06-06", 12821.92, 90000.0]]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -317,7 +317,16 @@ class AssetCapitalization(StockController):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
if self.capitalization_method == "Create a new composite asset" and not (
|
||||
self.get("stock_items") or self.get("asset_items")
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
|
||||
)
|
||||
)
|
||||
|
||||
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
@@ -460,13 +469,24 @@ class AssetCapitalization(StockController):
|
||||
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
return self.target_fixed_asset_account
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
|
||||
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
|
||||
if is_cwip_accounting_enabled(asset_category):
|
||||
target_account = get_asset_category_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
return target_account if target_account else self.target_fixed_asset_account
|
||||
else:
|
||||
return self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
|
||||
@@ -554,13 +574,13 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.target_fixed_asset_account,
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
|
||||
@@ -31,6 +31,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_perpetual_inventory(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
# Variables
|
||||
consumed_asset_value = 100000
|
||||
@@ -187,9 +193,10 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test General Ledger Entries
|
||||
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 3000,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000,
|
||||
default_expense_account: -2000,
|
||||
"_Test Fixed Asset - _TC": -100000.0,
|
||||
default_expense_account: -2000.0,
|
||||
"CWIP Account - _TC": 103000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
@@ -214,6 +221,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_wip_composite_asset(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
@@ -424,7 +437,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 1000.0,
|
||||
"CWIP Account - _TC": 1000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,9 @@ class AssetDepreciationSchedule(Document):
|
||||
if skip_row:
|
||||
continue
|
||||
|
||||
schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
|
||||
schedule_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
|
||||
)
|
||||
if not current_fiscal_year_end_date:
|
||||
current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
|
||||
elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
|
||||
@@ -330,8 +332,10 @@ class AssetDepreciationSchedule(Document):
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(from_date)
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
|
||||
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
@@ -353,9 +357,8 @@ class AssetDepreciationSchedule(Document):
|
||||
and not self.opening_accumulated_depreciation
|
||||
and not self.flags.wdv_it_act_applied
|
||||
):
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
from_date = asset_doc.available_for_use_date
|
||||
# needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
@@ -406,6 +409,8 @@ class AssetDepreciationSchedule(Document):
|
||||
(n + self.opening_number_of_booked_depreciations)
|
||||
* cint(row.frequency_of_depreciation),
|
||||
)
|
||||
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
asset_doc.to_date = get_last_day(asset_doc.to_date)
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
@@ -421,7 +426,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount_without_pro_rata, depreciation_amount
|
||||
)
|
||||
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
schedule_date = add_days(schedule_date, days - 1)
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
@@ -504,7 +509,10 @@ class AssetDepreciationSchedule(Document):
|
||||
continue
|
||||
|
||||
if not accumulated_depreciation:
|
||||
if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if i > 0 and (
|
||||
asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
|
||||
or asset_doc.flags.increase_in_asset_value_due_to_repair
|
||||
):
|
||||
accumulated_depreciation = self.get("depreciation_schedule")[
|
||||
i - 1
|
||||
].accumulated_depreciation_amount
|
||||
@@ -552,9 +560,20 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
prev_depreciation_start_date = get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
|
||||
)
|
||||
)
|
||||
from_date = asset_doc.available_for_use_date
|
||||
days = date_diff(prev_depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation)
|
||||
else:
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if days <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -601,7 +620,7 @@ def _get_pro_rata_amt(
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
original_schedule_date=None,
|
||||
):
|
||||
days = date_diff(to_date, from_date)
|
||||
days = date_diff(to_date, from_date) + 1
|
||||
months = month_diff(to_date, from_date)
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||
@@ -661,7 +680,7 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# 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:
|
||||
@@ -682,20 +701,15 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_prorata_based:
|
||||
amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
)
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
return depreciation_amount
|
||||
|
||||
|
||||
def get_daily_prorata_based_straight_line_depr(
|
||||
@@ -725,7 +739,16 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)),
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(
|
||||
row.frequency_of_depreciation
|
||||
* (asset.opening_number_of_booked_depreciations + 1)
|
||||
)
|
||||
* -1,
|
||||
),
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -744,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
depr_period_start_date = add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1
|
||||
)
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12)
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
|
||||
@@ -904,7 +931,7 @@ def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
|
||||
|
||||
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
|
||||
""" "
|
||||
"""
|
||||
Returns monthly depreciation amount when year changes
|
||||
1. Calculate per day depr based on new year
|
||||
2. Calculate monthly amount based on new per day amount
|
||||
@@ -1021,6 +1048,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
date_of_return=None,
|
||||
value_after_depreciation=None,
|
||||
ignore_booked_entry=False,
|
||||
difference_amount=None,
|
||||
):
|
||||
for row in asset_doc.get("finance_books"):
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
@@ -1035,6 +1063,8 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
)
|
||||
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
||||
value_after_depreciation = row.value_after_depreciation + difference_amount
|
||||
|
||||
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
||||
"Written Down Value",
|
||||
|
||||
@@ -75,6 +75,116 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_slm_for_existing_asset_daily_pro_rata_enabled(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1)
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265,
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
daily_prorata_based=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-07-31", 31.0, 296.0],
|
||||
["2024-08-31", 31.0, 327.0],
|
||||
["2024-09-30", 30.0, 357.0],
|
||||
["2024-10-31", 31.0, 388.0],
|
||||
["2024-11-30", 30.0, 418.0],
|
||||
["2024-12-31", 31.0, 449.0],
|
||||
["2025-01-31", 31.0, 480.0],
|
||||
["2025-02-28", 28.0, 508.0],
|
||||
["2025-03-31", 31.0, 539.0],
|
||||
["2025-04-30", 30.0, 569.0],
|
||||
["2025-05-31", 31.0, 600.0],
|
||||
["2025-06-30", 30.0, 630.0],
|
||||
["2025-07-31", 31.0, 661.0],
|
||||
["2025-08-31", 31.0, 692.0],
|
||||
["2025-09-30", 30.0, 722.0],
|
||||
["2025-10-10", 9.0, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0)
|
||||
|
||||
def test_schedule_for_slm_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265.30,
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-07-31", 30.46, 295.76],
|
||||
["2024-08-31", 30.46, 326.22],
|
||||
["2024-09-30", 30.46, 356.68],
|
||||
["2024-10-31", 30.46, 387.14],
|
||||
["2024-11-30", 30.46, 417.6],
|
||||
["2024-12-31", 30.46, 448.06],
|
||||
["2025-01-31", 30.46, 478.52],
|
||||
["2025-02-28", 30.46, 508.98],
|
||||
["2025-03-31", 30.46, 539.44],
|
||||
["2025-04-30", 30.46, 569.9],
|
||||
["2025-05-31", 30.46, 600.36],
|
||||
["2025-06-30", 30.46, 630.82],
|
||||
["2025-07-31", 30.46, 661.28],
|
||||
["2025-08-31", 30.46, 691.74],
|
||||
["2025-09-30", 30.46, 722.2],
|
||||
["2025-10-10", 8.8, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_sl_method_for_existing_asset_with_frequency_of_3_months(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-11-01",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=4,
|
||||
opening_accumulated_depreciation=223.15,
|
||||
depreciation_start_date="2024-12-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=3,
|
||||
gross_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-12-31", 60.92, 284.07],
|
||||
["2025-03-31", 60.92, 344.99],
|
||||
["2025-06-30", 60.92, 405.91],
|
||||
["2025-09-30", 60.92, 466.83],
|
||||
["2025-12-31", 60.92, 527.75],
|
||||
["2026-03-31", 60.92, 588.67],
|
||||
["2026-06-30", 60.92, 649.59],
|
||||
["2026-09-30", 60.92, 710.51],
|
||||
["2026-11-01", 20.49, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Enable Checkbox to Calculate depreciation using total days in depreciation period
|
||||
def test_daily_prorata_based_depr_after_enabling_configuration(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1)
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
frappe.ui.form.on("Asset Maintenance", {
|
||||
setup: (frm) => {
|
||||
frm.set_query("asset_name", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("assign_to", "asset_maintenance_tasks", function (doc) {
|
||||
return {
|
||||
query: "erpnext.assets.doctype.asset_maintenance.asset_maintenance.get_team_members",
|
||||
|
||||
@@ -29,6 +29,15 @@ frappe.ui.form.on("Asset Repair", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_invoice", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("warehouse", "stock_items", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user