mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-17 12:09:19 +00:00
Compare commits
381 Commits
coderabbit
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ae191478 | ||
|
|
6cacead726 | ||
|
|
2d8513de4e | ||
|
|
992027fe89 | ||
|
|
4eb045d927 | ||
|
|
1717a7c983 | ||
|
|
2de4b2ea56 | ||
|
|
b08e0014f7 | ||
|
|
f912c8419a | ||
|
|
f757adc7f7 | ||
|
|
df65fbbc4a | ||
|
|
59bd35c64d | ||
|
|
bd03bcdcb2 | ||
|
|
7fcf277055 | ||
|
|
1e2bcde0f5 | ||
|
|
f23d6911f3 | ||
|
|
12c1b8a910 | ||
|
|
f4c37f1f20 | ||
|
|
a799af7f9f | ||
|
|
f697679b37 | ||
|
|
f14b3ed723 | ||
|
|
fab7f9ee53 | ||
|
|
6e07aac5b7 | ||
|
|
a4fe0fb809 | ||
|
|
533257c4f3 | ||
|
|
33110951b3 | ||
|
|
12ebab1657 | ||
|
|
50eb6786bf | ||
|
|
590207419a | ||
|
|
92f69ae484 | ||
|
|
610dcbb974 | ||
|
|
e3ca318e93 | ||
|
|
98838b1dd5 | ||
|
|
abc7bf2fd6 | ||
|
|
8a19dc4a20 | ||
|
|
301b294da9 | ||
|
|
bf6c331ac4 | ||
|
|
780d3f5ba4 | ||
|
|
dbf9faa87c | ||
|
|
6494fc42c6 | ||
|
|
3abdfcb269 | ||
|
|
0d58dfd0fa | ||
|
|
f6ebf2d0b3 | ||
|
|
842a3645dc | ||
|
|
a6d92e5ec7 | ||
|
|
ce19514a2c | ||
|
|
e223731924 | ||
|
|
39b6aab714 | ||
|
|
6703610596 | ||
|
|
f4f2d11fa4 | ||
|
|
5c4f778223 | ||
|
|
5f97bec2b3 | ||
|
|
f1a2e1b725 | ||
|
|
17397ae652 | ||
|
|
6b83309750 | ||
|
|
e08f82909c | ||
|
|
4cc2afbd83 | ||
|
|
c780796284 | ||
|
|
ddf4a83cf8 | ||
|
|
b5c739d1cc | ||
|
|
2bc19783cb | ||
|
|
1adbf90d8c | ||
|
|
7386270fce | ||
|
|
156dda8157 | ||
|
|
af974fbccd | ||
|
|
e85238383f | ||
|
|
b380b60486 | ||
|
|
7fa800b874 | ||
|
|
8b6e58d02a | ||
|
|
58203a89f1 | ||
|
|
eb5899c786 | ||
|
|
4605051903 | ||
|
|
15397b17f3 | ||
|
|
b63566681b | ||
|
|
a5e49ea8a1 | ||
|
|
98f186b0e0 | ||
|
|
eac6e6a7dd | ||
|
|
2de3f63478 | ||
|
|
334bb609f0 | ||
|
|
b4cf6a1fb9 | ||
|
|
4a26810871 | ||
|
|
96cd8cdb38 | ||
|
|
912ffc2d64 | ||
|
|
f1f61ff61b | ||
|
|
aaca906a0f | ||
|
|
94b75e80b9 | ||
|
|
67f7341721 | ||
|
|
b11d064a2a | ||
|
|
959c311795 | ||
|
|
1d57bbca11 | ||
|
|
c9d98eb4f0 | ||
|
|
fc7a33ebf8 | ||
|
|
b48bff2029 | ||
|
|
a82c0c20f0 | ||
|
|
83d575206b | ||
|
|
e2d4ce74d9 | ||
|
|
13ce7279a8 | ||
|
|
4672c2c383 | ||
|
|
946073cfd9 | ||
|
|
1d97b7cc2b | ||
|
|
128e243945 | ||
|
|
a6a04e8245 | ||
|
|
6c8e909599 | ||
|
|
aab6271b14 | ||
|
|
ff2faf36a7 | ||
|
|
231356a005 | ||
|
|
630d873214 | ||
|
|
6cc421eec6 | ||
|
|
67427264d3 | ||
|
|
5d0958c5b1 | ||
|
|
1d7a8dda26 | ||
|
|
323d8eaccd | ||
|
|
d5301d3111 | ||
|
|
ef41654fcf | ||
|
|
aedefc867e | ||
|
|
4a4c2188ec | ||
|
|
22e4c7446e | ||
|
|
6cf24feffc | ||
|
|
b6e9b532aa | ||
|
|
8f43b41cad | ||
|
|
bc17d778a6 | ||
|
|
de6e787087 | ||
|
|
58eda49549 | ||
|
|
f00a63b69d | ||
|
|
0678638106 | ||
|
|
d610d1dccd | ||
|
|
3c70cbbaf8 | ||
|
|
6a8bd0ae9e | ||
|
|
47c0b47722 | ||
|
|
dbab718aaa | ||
|
|
ff0969ace6 | ||
|
|
6836b8830a | ||
|
|
8e2d4b2b77 | ||
|
|
d652fbeb01 | ||
|
|
06702ffae2 | ||
|
|
083a28d3b4 | ||
|
|
870181de87 | ||
|
|
67170d0a27 | ||
|
|
6026e9b3d4 | ||
|
|
da59db357e | ||
|
|
b2da214346 | ||
|
|
70117d3b06 | ||
|
|
0168639125 | ||
|
|
c848c2dba8 | ||
|
|
a60f7eaf3a | ||
|
|
cb952285b0 | ||
|
|
c25a85199c | ||
|
|
3773f56b0b | ||
|
|
235acd4713 | ||
|
|
acb3ef78a7 | ||
|
|
2ac2e02b2f | ||
|
|
ab4b47c0af | ||
|
|
2322a26916 | ||
|
|
05f2b43344 | ||
|
|
bd928e0d56 | ||
|
|
4cfd186aec | ||
|
|
85737327a3 | ||
|
|
09bedef9e1 | ||
|
|
1edd030e60 | ||
|
|
d22f4682b1 | ||
|
|
c021cf01fc | ||
|
|
58abcdf0c9 | ||
|
|
dd281b6375 | ||
|
|
0a186328e4 | ||
|
|
ed7c021900 | ||
|
|
c3c1b1f830 | ||
|
|
6e1fcfd210 | ||
|
|
2bc097a82c | ||
|
|
c6c1ab458c | ||
|
|
72efd21c47 | ||
|
|
c7290ce4a7 | ||
|
|
126fe8c974 | ||
|
|
cf492c3eb7 | ||
|
|
a1c74679da | ||
|
|
59f5fb6494 | ||
|
|
c75fbbd8f4 | ||
|
|
c261a436ac | ||
|
|
b85817d9c1 | ||
|
|
86b30c422b | ||
|
|
3fcab6e727 | ||
|
|
770297fd43 | ||
|
|
7d9bd48a4f | ||
|
|
a5a3f52c64 | ||
|
|
f9cafcc282 | ||
|
|
5fe8692a8d | ||
|
|
69cb2ca839 | ||
|
|
72b4aa1aac | ||
|
|
e77144414a | ||
|
|
b452e06b82 | ||
|
|
dffa8010c1 | ||
|
|
dcbcc596f2 | ||
|
|
c0c2e2367c | ||
|
|
95b9870de1 | ||
|
|
374e89ab33 | ||
|
|
523a5d0a49 | ||
|
|
25cafa6044 | ||
|
|
3ed8a99603 | ||
|
|
cdeeb36fe4 | ||
|
|
8598ca9a9d | ||
|
|
bdc04bf531 | ||
|
|
88097e78d2 | ||
|
|
ee65ceebad | ||
|
|
21c0fc5db6 | ||
|
|
3bbca629c6 | ||
|
|
be820ffe59 | ||
|
|
c253fb8902 | ||
|
|
8756f91857 | ||
|
|
da716b824f | ||
|
|
039f5e6143 | ||
|
|
44fd94c0d4 | ||
|
|
41d1703e7c | ||
|
|
4f503ac7f6 | ||
|
|
0fef95bfbb | ||
|
|
8c82b86b42 | ||
|
|
a93eed0fb7 | ||
|
|
437d0eea77 | ||
|
|
0ec30a1cea | ||
|
|
7e51346946 | ||
|
|
6849149176 | ||
|
|
a5e29e3659 | ||
|
|
87cbed0911 | ||
|
|
ca3e3a7941 | ||
|
|
584f6c42f0 | ||
|
|
282d28fbce | ||
|
|
20e9706ec3 | ||
|
|
9c1be96990 | ||
|
|
25e5a623d6 | ||
|
|
35a8d02866 | ||
|
|
44ff6ed6a1 | ||
|
|
a403940612 | ||
|
|
bf5f24c0e0 | ||
|
|
35474d997d | ||
|
|
ad886b6389 | ||
|
|
6408975b61 | ||
|
|
877f5611b1 | ||
|
|
d65c715e11 | ||
|
|
a7a8ff2086 | ||
|
|
71a8df2189 | ||
|
|
181ad0bdcd | ||
|
|
1963e03264 | ||
|
|
d383c70020 | ||
|
|
27fac7a352 | ||
|
|
bccbfe97b3 | ||
|
|
0e8f8677b8 | ||
|
|
3ffd50c772 | ||
|
|
b527d38bfa | ||
|
|
526b850e61 | ||
|
|
4024d8846b | ||
|
|
2757368579 | ||
|
|
b593150521 | ||
|
|
14128a47e7 | ||
|
|
7592c0956c | ||
|
|
a2d907d8bc | ||
|
|
d864d166f9 | ||
|
|
4a01c53cca | ||
|
|
3057a47994 | ||
|
|
29cbddbc77 | ||
|
|
34d2c8d9c2 | ||
|
|
fed8236919 | ||
|
|
9b09dd063d | ||
|
|
f18385c35d | ||
|
|
8411e4c5b2 | ||
|
|
81614939ab | ||
|
|
ea4379e4f2 | ||
|
|
89a603f20c | ||
|
|
ea63bfc9af | ||
|
|
073f88892e | ||
|
|
3c03c94f1a | ||
|
|
d22434d31e | ||
|
|
dc14a629ff | ||
|
|
f746540420 | ||
|
|
7fcdebcbd1 | ||
|
|
6e46c8f7c7 | ||
|
|
3cc9fb92d8 | ||
|
|
d5c457b8c5 | ||
|
|
fb802bc26b | ||
|
|
324bdcb177 | ||
|
|
452eaaf44e | ||
|
|
e57e8aa708 | ||
|
|
acdfdb1389 | ||
|
|
3a1c12d49c | ||
|
|
875cf68df8 | ||
|
|
6bc0d71fc8 | ||
|
|
552c6eb9f5 | ||
|
|
8202d2ed47 | ||
|
|
3718ac0c33 | ||
|
|
a3937ed44e | ||
|
|
fb515c8ddc | ||
|
|
02c7006525 | ||
|
|
cd8d4af900 | ||
|
|
dc5fd40a0c | ||
|
|
e3fe298297 | ||
|
|
533af66057 | ||
|
|
dbda66a62f | ||
|
|
bebbfd8f94 | ||
|
|
82741fbbe7 | ||
|
|
b11a1ecb7a | ||
|
|
b11d5ab04d | ||
|
|
5cee8edbb4 | ||
|
|
2dd5e2abd0 | ||
|
|
4a771fe765 | ||
|
|
8d10759631 | ||
|
|
d5ab4c1d7d | ||
|
|
81ae03e1a5 | ||
|
|
9f9120451b | ||
|
|
76a27541f3 | ||
|
|
9889d23b8c | ||
|
|
3578ee1195 | ||
|
|
4f8b2e520a | ||
|
|
0dc2545fb9 | ||
|
|
6e597b9c42 | ||
|
|
48acbe6b50 | ||
|
|
75cf70c8f3 | ||
|
|
d10530ee47 | ||
|
|
cf4b395ee3 | ||
|
|
90f399d0fc | ||
|
|
689172ff22 | ||
|
|
b2e109318f | ||
|
|
23b1b7ee04 | ||
|
|
06177ffaff | ||
|
|
a664f3039b | ||
|
|
daf1d52fc9 | ||
|
|
23f9d4c600 | ||
|
|
096e74b1ee | ||
|
|
33ab24943c | ||
|
|
b75940bf0e | ||
|
|
5ffbf59d78 | ||
|
|
47055901c0 | ||
|
|
a4e291bb77 | ||
|
|
1b0fc0541b | ||
|
|
cee3813ced | ||
|
|
6bd36a137c | ||
|
|
f4b18f2ad7 | ||
|
|
62a8e4a561 | ||
|
|
99b7a9d15c | ||
|
|
9391c8911c | ||
|
|
d3d03e8d83 | ||
|
|
6730960f56 | ||
|
|
1f91dcb1bd | ||
|
|
320f0056a2 | ||
|
|
a7ec01bf21 | ||
|
|
36f923c540 | ||
|
|
8bc7fe7e55 | ||
|
|
ff78aaeb3b | ||
|
|
b10cf4a928 | ||
|
|
027a4ea1bf | ||
|
|
c21a713750 | ||
|
|
b98977dc75 | ||
|
|
1979879b07 | ||
|
|
f5057cfb66 | ||
|
|
5a26d593e4 | ||
|
|
866b252309 | ||
|
|
2065f2b117 | ||
|
|
b99d2e16c4 | ||
|
|
468d181a00 | ||
|
|
997d573dc0 | ||
|
|
2442be5773 | ||
|
|
381072170a | ||
|
|
e3ab0e7c67 | ||
|
|
a68cbb177c | ||
|
|
f8f47d0a73 | ||
|
|
e6ad752c99 | ||
|
|
d8756fc7de | ||
|
|
fc967fceb2 | ||
|
|
b7fbe31558 | ||
|
|
eef77291ad | ||
|
|
82285e236f | ||
|
|
2579402852 | ||
|
|
66712fa8b5 | ||
|
|
99a0ba0b45 | ||
|
|
790876ea5b | ||
|
|
f7a37d2812 | ||
|
|
ae8b34e03c | ||
|
|
1de0c46c51 | ||
|
|
2abb011816 | ||
|
|
eda1dae882 | ||
|
|
9164162a9e | ||
|
|
b7c6d8e2a6 | ||
|
|
4b7cb6bfad | ||
|
|
4c7a0a4e4c | ||
|
|
4527877bb5 |
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -46,7 +46,8 @@ def validate_service_stop_date(doc):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
||||
and item.service_stop_date
|
||||
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
|
||||
):
|
||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -49,6 +50,13 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -62,10 +70,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -105,8 +114,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
@@ -116,6 +117,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"use_new_budget_controller"
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -598,12 +598,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_new_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use New Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -651,6 +645,12 @@
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -659,7 +659,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_new_budget_controller: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified": "2025-10-13 15:11:58.300836",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
|
||||
@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["against_voucher_type", "against_voucher_no"],
|
||||
)
|
||||
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["voucher_type", "voucher_no"],
|
||||
)
|
||||
|
||||
@@ -409,7 +409,7 @@ def start_auto_reconcile(
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry"],
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -666,7 +666,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -854,11 +854,14 @@ def get_je_matching_query(
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -871,11 +874,11 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -889,6 +892,9 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -76,18 +76,6 @@ class BankStatementImport(DataImport):
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
"""
|
||||
Start a background import job for this Bank Statement Import.
|
||||
|
||||
Validates that the preview contains a "Bank Account" column and that the scheduler is active (unless running in test or developer mode). If validation passes and there is not already an enqueued job for this document, enqueue a background worker to perform the import.
|
||||
|
||||
Returns:
|
||||
str | None: The enqueued job_id when a new job was queued, otherwise None.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the preview is missing a "Bank Account" column.
|
||||
frappe.ValidationError: If the scheduler is inactive and import is not allowed to run immediately.
|
||||
"""
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
@@ -124,46 +112,24 @@ class BankStatementImport(DataImport):
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""
|
||||
Truncate overly long MT940 statement numbers found in `:28C:` tags to the last 5 digits.
|
||||
|
||||
This function fixes MT940 files where banks supply statement numbers longer than the MT940-expected maximum (5 digits),
|
||||
which can break parsers. It only processes lines that start with the `:28C:` tag and:
|
||||
- leaves content unchanged if no `:28C:` tag is present,
|
||||
- truncates numeric statement numbers longer than 5 digits to their last 5 digits,
|
||||
- preserves any `/sequence` suffix and trailing whitespace on the same line.
|
||||
|
||||
Parameters:
|
||||
content (str): Raw MT940 file content.
|
||||
|
||||
Returns:
|
||||
str: The processed content with corrected `:28C:` statement numbers.
|
||||
"""Preprocess MT940 content to fix statement number format issues.
|
||||
|
||||
The MT940 standard expects statement numbers to be maximum 5 digits,
|
||||
but some banks provide longer statement numbers that cause parsing errors.
|
||||
This function truncates statement numbers longer than 5 digits to the last 5 digits.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r'(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$')
|
||||
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
|
||||
|
||||
def replace_statement_number(match):
|
||||
"""
|
||||
Replace a matched MT940 :28C: statement number by truncating it to the last five digits if it is longer.
|
||||
|
||||
Parameters:
|
||||
match (re.Match): A regex match with groups:
|
||||
1: prefix (e.g., ':28C:')
|
||||
2: numeric statement number
|
||||
3: optional sequence part (e.g., '/1')
|
||||
4: optional trailing whitespace
|
||||
|
||||
Returns:
|
||||
str: Reconstructed replacement string preserving prefix, (possibly truncated) statement number, sequence part, and trailing whitespace.
|
||||
"""
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or '' # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or '' # Preserve trailing whitespace
|
||||
sequence_part = match.group(3) or "" # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or "" # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
@@ -178,27 +144,9 @@ def preprocess_mt940_content(content: str) -> str:
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
"""
|
||||
Convert an MT940 file to a CSV and save it to the Frappe File Manager, returning the saved file URL.
|
||||
|
||||
This function:
|
||||
- Loads the specified MT940 file, verifies it is MT940 format, preprocesses content to fix statement number formatting, and parses transactions.
|
||||
- Writes parsed transactions to an in-memory CSV with headers: Date, Deposit, Withdrawal, Description, Reference Number, Bank Account, Currency.
|
||||
- Saves the CSV as a private attachment on the Bank Statement Import document and returns the file URL.
|
||||
|
||||
Parameters:
|
||||
data_import (str): Name (docname) of the Bank Statement Import document to attach the converted CSV to.
|
||||
mt940_file_path (str): File path or file identifier pointing to the uploaded MT940 file to convert.
|
||||
|
||||
Returns:
|
||||
str: URL of the saved CSV file in the File Manager.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the file is not MT940, MT940 import is not enabled on the document, parsing fails, or no transactions are found.
|
||||
"""
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
@@ -335,20 +283,7 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""
|
||||
Update a Bank document's transaction field mappings to match the provided template options.
|
||||
|
||||
This replaces all existing entries in the Bank.bank_transaction_mapping child table with mappings from
|
||||
the JSON-encoded template_options. The expected template_options JSON contains a "column_to_field_map"
|
||||
object mapping file column names (keys) to bank transaction field names (values).
|
||||
|
||||
Parameters:
|
||||
bank (str | frappe.model.document.Document): Bank name/docname or a Bank document.
|
||||
template_options (str): JSON string containing a "column_to_field_map" mapping of file column -> bank field.
|
||||
|
||||
Side effects:
|
||||
Overwrites the Bank.bank_transaction_mapping entries and saves the Bank document.
|
||||
"""
|
||||
"""Update bank transaction mapping database with template options."""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -360,17 +295,7 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""
|
||||
Ensure every data row contains the given bank account value.
|
||||
|
||||
Assumes `data` is a list of rows where data[0] is the header row. If the header row does not contain "Bank Account",
|
||||
this function appends that header and appends the `bank_account` value to each subsequent row. If the header exists,
|
||||
it sets the `bank_account` value into the existing "Bank Account" column for every data row. Mutates `data` in place.
|
||||
|
||||
Parameters:
|
||||
data (list[list]): Table-like data with the first row as headers.
|
||||
bank_account (str): Bank account value to set for each data row.
|
||||
"""
|
||||
"""Add bank account information to data rows."""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -387,21 +312,7 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""
|
||||
Write processed tabular data back to the original import file path (CSV or Excel).
|
||||
|
||||
This function overwrites the file referenced by import_file.file_doc.get_full_path().
|
||||
- If the file extension is "csv", writes rows using the csv writer (expects `data` as an iterable of row iterables).
|
||||
- If the extension is "xlsx" or "xls", writes to an Excel workbook using write_xlsx with sheet name "trans".
|
||||
|
||||
Parameters:
|
||||
import_file: object
|
||||
File wrapper whose `.file_doc.get_full_path()` and `.file_doc.get_extension()` are used to determine the target path and extension.
|
||||
data: Iterable[Iterable]
|
||||
Sequence of rows (each row is an iterable of cell values) to be written.
|
||||
|
||||
No return value.
|
||||
"""
|
||||
"""Write processed data to CSV or Excel files."""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -416,21 +327,7 @@ def write_files(import_file, data):
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""
|
||||
Write rows of data to an Excel worksheet and save the workbook.
|
||||
|
||||
Creates a sheet named `sheet_name` in the provided openpyxl workbook (or a new write-only workbook if `wb` is None), applies optional column widths, converts HTML in string cells (except for sheets named "Data Import Template" or "Data Export"), strips characters illegal in Excel, and saves the workbook to `file_path`.
|
||||
|
||||
Parameters:
|
||||
data (Iterable[Sequence]): Iterable of rows, where each row is a sequence of cell values.
|
||||
sheet_name (str): Name of the worksheet to create.
|
||||
wb (openpyxl.Workbook, optional): Workbook to append the sheet to. If not provided, a new write-only Workbook is created.
|
||||
column_widths (Sequence[Number], optional): Sequence of column widths; indexes correspond to columns starting at 1.
|
||||
file_path (str): File path where the workbook will be saved.
|
||||
|
||||
Returns:
|
||||
bool: True on successful save.
|
||||
"""
|
||||
"""Write data to Excel file with formatting."""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import unittest
|
||||
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
preprocess_mt940_content,
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,14 +108,7 @@ class TestBankStatementImport(unittest.TestCase):
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""
|
||||
Verify preprocessing handles statement-number length boundaries in `:28C:` tags.
|
||||
|
||||
Checks that:
|
||||
- A 6-digit statement number is truncated to its last 5 digits.
|
||||
- A 5-digit statement number remains unchanged.
|
||||
- A very long statement number is reduced to its last 5 digits.
|
||||
"""
|
||||
"""Test boundary conditions for statement number length"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
@@ -134,11 +127,7 @@ class TestBankStatementImport(unittest.TestCase):
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""
|
||||
Verify preprocessing of a real-world MT940 document: truncate 6-digit `:28C:` statement numbers to their last 5 digits and preserve all other content.
|
||||
|
||||
Uses a sanitized, production-failing MT940 sample where `:28C:167619/1` must become `:28C:67619/1`. Asserts the entire document matches the expected transformed output, that the truncated tag is present and the original is absent, and that unrelated fields (e.g., `:20:` reference and UPI details) remain unchanged.
|
||||
"""
|
||||
"""Test with real-world MT940 content that was failing in production"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
@@ -239,7 +239,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:53:45.908169",
|
||||
"modified": "2025-09-26 17:06:29.207673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.Data | None
|
||||
reference_number: DF.SmallText | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
|
||||
@@ -23,8 +23,8 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
|
||||
if (!value) {
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
@@ -137,8 +137,8 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
# skipping validation for payroll entry creation in case party is not required
|
||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||
|
||||
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
@@ -256,7 +256,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -577,6 +578,14 @@
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -591,7 +600,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -193,8 +194,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -645,10 +646,10 @@ class JournalEntry(AccountsController):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
# skipping validation for payroll entry creation
|
||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party) and not skip_validation:
|
||||
if (
|
||||
not (d.party_type and d.party) and not self.party_not_required
|
||||
): # skipping validation if party_not_required is passed via payroll entry
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -1240,6 +1241,11 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
@@ -1267,6 +1273,7 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
frappe.flags.party_not_required = False
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(IntegrationTestCase):
|
||||
@@ -591,6 +592,15 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified": "2025-09-29 13:01:48.916517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -585,6 +585,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
}
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -607,6 +608,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.received_amount(frm);
|
||||
}
|
||||
}
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -129,7 +129,13 @@ class PaymentRequest(Document):
|
||||
|
||||
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
if (
|
||||
flt(
|
||||
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
|
||||
get_currency_precision(),
|
||||
)
|
||||
> ref_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"invoice_portion",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"section_break_6",
|
||||
"discount_type",
|
||||
"discount_date",
|
||||
"column_break_9",
|
||||
"discount",
|
||||
"discount_type",
|
||||
"column_break_9",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
@@ -172,12 +177,50 @@
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
|
||||
"fieldname": "credit_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Days",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
|
||||
"fieldname": "credit_months",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Months",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount",
|
||||
"fieldname": "discount_validity_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Discount Validity Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount_validity_based_on",
|
||||
"fieldname": "discount_validity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discount Validity",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-11 11:06:51.792982",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -189,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,27 @@ class PaymentSchedule(Document):
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
credit_days: DF.Int
|
||||
credit_months: DF.Int
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
discount_date: DF.Date | None
|
||||
discount_type: DF.Literal["Percentage", "Amount"]
|
||||
discount_validity: DF.Int
|
||||
discount_validity_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
discounted_amount: DF.Currency
|
||||
due_date: DF.Date
|
||||
due_date_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
invoice_portion: DF.Percent
|
||||
mode_of_payment: DF.Link | None
|
||||
outstanding: DF.Currency
|
||||
|
||||
@@ -162,4 +162,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if pricing_rule.get("coupon_code_based") == 1:
|
||||
if not args.coupon_code:
|
||||
continue
|
||||
coupon_code = frappe.db.get_value(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
|
||||
<div>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
@@ -34,11 +34,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
<th style="width: 15%">{{ _("Reference") }}</th>
|
||||
<th style="width: 25%">{{ _("Remarks") }}</th>
|
||||
<th style="width: 20%">{{ _("Reference") }}</th>
|
||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||
{% if filters.show_remarks %}
|
||||
<th style="width: 20%">{{ _("Remarks") }}</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -47,36 +49,51 @@
|
||||
{% if(row.posting_date) %}
|
||||
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
|
||||
<td>{{ row.voucher_type }}
|
||||
<br>{{ row.voucher_no }}</td>
|
||||
<td>
|
||||
{% if not (filters.party or filters.account) %}
|
||||
<br>{{ row.voucher_no }}
|
||||
{% if not (filters.party or filters.account) %}
|
||||
{{ row.party or row.account }}
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<br>{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% if filters.show_remarks %}
|
||||
<td>
|
||||
{% if row.remarks %}
|
||||
{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
||||
<td>
|
||||
<b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% if filters.show_remarks %}
|
||||
<td>
|
||||
{% if row.remarks %}
|
||||
{{ _("Remarks:") }} {{ row.remarks }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-05-22 16:46:18.712954",
|
||||
"doctype": "DocType",
|
||||
@@ -69,7 +70,7 @@
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Weekly\nMonthly\nQuarterly"
|
||||
"options": "Daily\nWeekly\nBiweekly\nMonthly\nQuarterly"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@@ -416,7 +417,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-09-03 14:24:43.608565",
|
||||
"modified": "2025-10-07 12:19:20.719898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_months, format_date, getdate, today
|
||||
from frappe.utils import add_days, add_months, add_to_date, format_date, getdate, today
|
||||
from frappe.utils.jinja import validate_template
|
||||
from frappe.utils.pdf import get_pdf
|
||||
from frappe.www.printview import get_print_style
|
||||
@@ -55,7 +55,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
enable_auto_email: DF.Check
|
||||
filter_duration: DF.Int
|
||||
finance_book: DF.Link | None
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
frequency: DF.Literal["Daily", "Weekly", "Biweekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
@@ -555,8 +555,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
new_to_date = getdate(posting_date or today())
|
||||
if doc.frequency == "Weekly":
|
||||
new_to_date = add_days(new_to_date, 7)
|
||||
if doc.frequency in ("Daily", "Weekly", "Biweekly"):
|
||||
frequency = {"Daily": 1, "Weekly": 7, "Biweekly": 14}
|
||||
new_to_date = add_days(new_to_date, frequency[doc.frequency])
|
||||
else:
|
||||
new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3)
|
||||
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||
<h2 class="text-center" style="margin-top:0">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
|
||||
<h4 class="text-center">
|
||||
{{ filters.customer_name }}
|
||||
</h4>
|
||||
@@ -159,7 +159,7 @@
|
||||
{% else %}
|
||||
<th style="width: 24%">{{ _("Reference") }}</th>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
{% if not(filters.show_future_payments) and filters.show_remarks %}
|
||||
<th style="width: 20%">
|
||||
{% if (filters.customer or filters.supplier or filters.customer_name) %}
|
||||
{{ _("Remarks") }}
|
||||
@@ -228,7 +228,7 @@
|
||||
<td>{{ data[i]["sales_person"] }}</td>
|
||||
{% endif %}
|
||||
|
||||
{% if not (filters.show_future_payments) %}
|
||||
{% if not (filters.show_future_payments) and filters.show_remarks %}
|
||||
<td>
|
||||
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
|
||||
{{ data[i]["party"] }}
|
||||
@@ -327,7 +327,9 @@
|
||||
{% endfor %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
{% if (filters.show_future_payments) or filters.show_remarks %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
|
||||
@@ -81,6 +81,7 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||
)
|
||||
|
||||
send_emails(process_soa.name, from_scheduler=True)
|
||||
process_soa.load_from_db()
|
||||
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
|
||||
@@ -884,6 +884,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
self.set_gl_entry_for_purchase_expense(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
@@ -1228,7 +1229,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||
self.update_net_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
@@ -1290,7 +1291,7 @@ class PurchaseInvoice(BuyingController):
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
def update_net_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={
|
||||
@@ -1306,7 +1307,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2147,19 +2147,16 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -2185,7 +2182,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -2633,6 +2629,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1}
|
||||
)
|
||||
def test_pr_status_rate_adjusted_from_pi(self):
|
||||
pr = make_purchase_receipt(qty=5, rate=100)
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Inital check
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.items[0].rate = 80
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Test 1 : Adjustment amount is negative
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.items[0].rate = 120
|
||||
pi.submit()
|
||||
pr.reload()
|
||||
|
||||
# Test 2 : Adjustment amount is positive
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
|
||||
@@ -232,7 +232,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
default_expense_account = company.service_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
|
||||
@@ -279,6 +279,59 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def before_print(self, settings=None):
|
||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
||||
|
||||
super().before_print(settings)
|
||||
|
||||
company_details = frappe.get_value(
|
||||
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
||||
)
|
||||
|
||||
required_fields = [
|
||||
company_details.get("company_logo"),
|
||||
company_details.get("phone_no"),
|
||||
company_details.get("email"),
|
||||
]
|
||||
|
||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
address_display_list = get_address_display_list("Company", self.company)
|
||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
||||
|
||||
required_fields.append(self.company_address)
|
||||
required_fields.append(address_line)
|
||||
|
||||
if not all(required_fields):
|
||||
frappe.publish_realtime(
|
||||
"sales_invoice_before_print",
|
||||
{
|
||||
"company_logo": company_details.get("company_logo"),
|
||||
"website": company_details.get("website"),
|
||||
"phone_no": company_details.get("phone_no"),
|
||||
"email": company_details.get("email"),
|
||||
"address_line": address_line,
|
||||
"company": self.company,
|
||||
"company_address": self.company_address,
|
||||
"name": self.name,
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
@@ -2802,6 +2855,59 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_company_master_details(name, company, details):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if isinstance(details, str):
|
||||
details = frappe.parse_json(details)
|
||||
|
||||
if details.get("email"):
|
||||
validate_email_address(details.get("email"), throw=True)
|
||||
|
||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
||||
|
||||
if company_fields_to_update:
|
||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
||||
|
||||
company_address = details.get("company_address")
|
||||
if details.get("address_line1"):
|
||||
address_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": details.get("address_title"),
|
||||
"address_type": details.get("address_type"),
|
||||
"address_line1": details.get("address_line1"),
|
||||
"address_line2": details.get("address_line2"),
|
||||
"city": details.get("city"),
|
||||
"state": details.get("state"),
|
||||
"pincode": details.get("pincode"),
|
||||
"country": details.get("country"),
|
||||
"is_your_company_address": 1,
|
||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
||||
}
|
||||
)
|
||||
address_doc.insert()
|
||||
company_address = address_doc.name
|
||||
|
||||
if company_address:
|
||||
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
|
||||
if not company_address_display or details.get("address_line1"):
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
|
||||
(
|
||||
frappe.qb.update(SalesInvoice)
|
||||
.set(SalesInvoice.company_address, company_address)
|
||||
.set(SalesInvoice.company_address_display, get_address_display(company_address))
|
||||
.where(SalesInvoice.name == name)
|
||||
).run()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -483,18 +483,23 @@ class Subscription(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]:
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
|
||||
"""
|
||||
Returns the `Item`s linked to `Subscription Plan`
|
||||
"""
|
||||
if prorate is None:
|
||||
prorate = False
|
||||
|
||||
prorate_factor = 1
|
||||
if prorate:
|
||||
prorate_factor = get_prorata_factor(
|
||||
self.current_invoice_end,
|
||||
self.current_invoice_start,
|
||||
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
|
||||
cint(
|
||||
self.generate_invoice_at
|
||||
in [
|
||||
"Beginning of the current subscription period",
|
||||
"Days before the current subscription period",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
items = []
|
||||
@@ -511,33 +516,19 @@ class Subscription(Document):
|
||||
|
||||
deferred = frappe.db.get_value("Item", item_code, deferred_field)
|
||||
|
||||
if not prorate:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
else:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
|
||||
if deferred:
|
||||
item.update(
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
add_years,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
@@ -555,6 +556,33 @@ class TestSubscription(IntegrationTestCase):
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
def test_invoice_generation_days_before_subscription_period_with_prorate(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
settings.prorate = 1
|
||||
settings.save()
|
||||
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 5",
|
||||
cost=1000,
|
||||
billing_interval="Year",
|
||||
billing_interval_count=1,
|
||||
currency="INR",
|
||||
)
|
||||
|
||||
start_date = add_days(nowdate(), 2)
|
||||
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
generate_invoice_at="Days before the current subscription period",
|
||||
generate_new_invoices_past_due_date=1,
|
||||
number_of_days=2,
|
||||
plans=[{"plan": "_Test Plan Name 5", "qty": 1}],
|
||||
)
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -35,7 +35,7 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if (
|
||||
frappe.get_single_value("Accounts Settings", "use_new_budget_controller")
|
||||
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
||||
and gl_map[0].voucher_type != "Period Closing Voucher"
|
||||
):
|
||||
bud_val = BudgetValidation(gl_map=gl_map)
|
||||
@@ -159,6 +159,7 @@ def validate_accounting_period(gl_map):
|
||||
WHERE
|
||||
ap.name = cd.parent
|
||||
AND ap.company = %(company)s
|
||||
AND ap.disabled = 0
|
||||
AND cd.closed = 1
|
||||
AND cd.document_type = %(voucher_type)s
|
||||
AND %(date)s between ap.start_date and ap.end_date
|
||||
|
||||
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<style>
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
padding-right: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.letter-head td{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.invoice-header {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.logo-container {
|
||||
width: 90px;
|
||||
display: block;
|
||||
}
|
||||
.logo-container img {
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.company-details {
|
||||
width: 40%;
|
||||
align-content: center;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #171717;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invoice-info-cell {
|
||||
float: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
.invoice-info {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.invoice-label {
|
||||
color: #7C7C7C;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="invoice-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-cell" style="vertical-align: middle !important;">
|
||||
<div class="logo-container">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
|
||||
{% if company_logo %}
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="company-details">
|
||||
<div class="company-name">
|
||||
{{ doc.company }}
|
||||
</div>
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
|
||||
{{ company_address.get("address_line1") or "" }}<br>
|
||||
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
|
||||
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="invoice-info-cell">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Invoice:") }}</span>
|
||||
<span>{{ doc.name }}</span>
|
||||
</div>
|
||||
{% if company_details.website %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||
<span>{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||
<span>{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||
<span>{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<style>
|
||||
.print-format-preview {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
background: #f8f8f8;
|
||||
padding: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.letterhead-container {
|
||||
width: 100%;
|
||||
}
|
||||
.letterhead-container .other-details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.logo-address {
|
||||
width: 65%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.letter-head .logo {
|
||||
width: 90px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.letter-head .logo img {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: #171717;
|
||||
font-weight: bold;
|
||||
line-height: 23px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.company-address {
|
||||
color: #171717;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invoice-number {
|
||||
color: #7c7c7c;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
color: #7c7c7c;
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
color: #171717;
|
||||
display: inline-block;
|
||||
}
|
||||
.letterhead-container td {
|
||||
padding: 0px !important;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="letterhead-container">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-address">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
|
||||
company_logo %}
|
||||
<div class="logo">
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="company-name">{{ doc.company }}</div>
|
||||
<div class="company-address">
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
{{ company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
|
||||
{{ company_address.city or "" }}, {{ company_address.state or "" }}
|
||||
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="vertical-align: top">
|
||||
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
||||
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
|
||||
<div class="invoice-number">{{ doc.name }}</div>
|
||||
<br />
|
||||
</div>
|
||||
<div style="text-align: left; float: right" class="other-details">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
{% if company_details.website %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Website:") }}</span
|
||||
><span class="contact-value">{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Email:") }}</span
|
||||
><span class="contact-value">{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Contact:") }}</span
|
||||
><span class="contact-value">{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -164,6 +164,12 @@
|
||||
{% } %}
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<tbody>
|
||||
{% for(var i=0, l=data.length; i<l; i++) { %}
|
||||
<tr>
|
||||
|
||||
@@ -1272,7 +1272,7 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
ranges = [*self.ranges, "Above"]
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
|
||||
@@ -171,7 +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")
|
||||
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")
|
||||
|
||||
@@ -103,7 +103,7 @@ def get_data(filters):
|
||||
"depreciation_amount": d.debit,
|
||||
"depreciation_date": d.posting_date,
|
||||
"value_after_depreciation": (
|
||||
flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
flt(row.net_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
),
|
||||
"depreciation_entry": d.voucher_no,
|
||||
}
|
||||
@@ -119,7 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
"status",
|
||||
@@ -151,7 +151,7 @@ def get_columns():
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -95,7 +95,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -103,7 +103,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -114,7 +114,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -125,7 +125,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -354,10 +354,10 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT a.name,
|
||||
SELECT a.name, a.asset_name,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -365,7 +365,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -373,7 +373,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -384,7 +384,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -395,7 +395,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -583,6 +583,14 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
}
|
||||
)
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Asset Name"),
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
}
|
||||
)
|
||||
|
||||
columns += [
|
||||
{
|
||||
|
||||
@@ -5,30 +5,35 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{% include "accounts/report/financial_statements.html" %}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Trial Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
on_change: function (query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
{
|
||||
fieldname: "presentation_currency",
|
||||
label: __("Currency"),
|
||||
fieldtype: "Select",
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_opening",
|
||||
label: __("With Period Closing Entry For Opening Balances"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_current_period",
|
||||
label: __("Period Closing Entry For Current Period"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_unclosed_fy_pl_balances",
|
||||
label: __("Show unclosed fiscal year's P&L balances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_group_accounts",
|
||||
label: __("Show Group Accounts"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
formatter: erpnext.financial_statements.formatter,
|
||||
tree: true,
|
||||
name_field: "account",
|
||||
parent_field: "parent_account",
|
||||
initial_depth: 3,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2025-09-03 00:53:22.230646",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-09-03 00:53:22.230646",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Trial Balance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Consolidated Trial Balance",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, getdate, now_datetime, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.account import get_root_company
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
accumulate_values_into_parents,
|
||||
calculate_values,
|
||||
get_opening_balances,
|
||||
hide_group_accounts,
|
||||
prepare_opening_closing,
|
||||
value_fields,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
validate_filters as tb_validate_filters,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_rate_as_at
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
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=filters)
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_companies(filters)
|
||||
filters.show_net_values = True
|
||||
tb_validate_filters(filters)
|
||||
|
||||
|
||||
def validate_companies(filters):
|
||||
if not filters.company:
|
||||
return
|
||||
|
||||
root_company = get_root_company(filters.company[0])
|
||||
root_company = root_company[0] if root_company else filters.company[0]
|
||||
|
||||
lft, rgt = frappe.db.get_value("Company", root_company, fieldname=["lft", "rgt"])
|
||||
|
||||
company_subtree = frappe.db.get_all(
|
||||
"Company",
|
||||
{"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
"name",
|
||||
order_by="lft",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for company in filters.company:
|
||||
if company not in company_subtree:
|
||||
frappe.throw(
|
||||
_("Consolidated Trial Balance can be generated for Companies having same root Company.")
|
||||
)
|
||||
|
||||
sort_companies(filters)
|
||||
|
||||
|
||||
def sort_companies(filters):
|
||||
companies = frappe.db.get_all(
|
||||
"Company", {"name": ["in", filters.company]}, "name", order_by="lft", pluck="name"
|
||||
)
|
||||
filters.company = companies
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
data = []
|
||||
if filters.company:
|
||||
reporting_currency, ignore_reporting_currency = get_reporting_currency(filters)
|
||||
else:
|
||||
return data
|
||||
|
||||
for company in filters.company:
|
||||
company_filter = frappe._dict(filters)
|
||||
company_filter.company = company
|
||||
|
||||
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
|
||||
consolidate_trial_balance_data(data, tb_data)
|
||||
|
||||
for d in data:
|
||||
prepare_opening_closing(d)
|
||||
|
||||
total_row = calculate_total_row(data, reporting_currency)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
update_to_presentation_currency(
|
||||
data,
|
||||
reporting_currency,
|
||||
filters.get("presentation_currency"),
|
||||
filters.get("to_date"),
|
||||
ignore_reporting_currency,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
default_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
opening_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
filters.get("from_date"),
|
||||
)
|
||||
current_date = (
|
||||
filters.get("to_date") if getdate(filters.get("to_date")) <= now_datetime().date() else nowdate()
|
||||
)
|
||||
closing_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
current_date,
|
||||
)
|
||||
|
||||
if not (opening_exchange_rate and closing_exchange_rate):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}.",
|
||||
).format(default_currency, reporting_currency, current_date)
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(
|
||||
filters,
|
||||
ignore_is_opening,
|
||||
exchange_rate=opening_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
group_by_account=True,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
exchange_rate=closing_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
# Prepare opening closing for group account
|
||||
if parent_children_map.get(d.account) and filters.get("show_net_values"):
|
||||
prepare_opening_closing(d)
|
||||
|
||||
has_value = False
|
||||
row = {
|
||||
"account": d.name,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": reporting_currency,
|
||||
"is_group_account": d.is_group,
|
||||
"acc_name": d.account_name,
|
||||
"acc_number": d.account_number,
|
||||
"account_name": (
|
||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||
),
|
||||
"root_type": d.root_type,
|
||||
"account_type": d.account_type,
|
||||
}
|
||||
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= get_zero_cutoff(reporting_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, reporting_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": reporting_currency,
|
||||
}
|
||||
|
||||
for d in data:
|
||||
if not d.get("parent_account"):
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
calculate_foreign_currency_translation_reserve(total_row, data)
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def calculate_foreign_currency_translation_reserve(total_row, data):
|
||||
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
|
||||
dr_cr_diff = total_row["debit"] - total_row["credit"]
|
||||
|
||||
idx = get_fctr_root_row_index(data)
|
||||
|
||||
fctr_row = {
|
||||
"account": _("Foreign Currency Translation Reserve"),
|
||||
"account_name": _("Foreign Currency Translation Reserve"),
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": abs(opening_dr_cr_diff) if opening_dr_cr_diff < 0 else 0.0,
|
||||
"opening_credit": abs(opening_dr_cr_diff) if opening_dr_cr_diff > 0 else 0.0,
|
||||
"debit": abs(dr_cr_diff) if dr_cr_diff < 0 else 0.0,
|
||||
"credit": abs(dr_cr_diff) if dr_cr_diff > 0 else 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"root_type": data[idx].get("root_type"),
|
||||
"account_type": "Equity",
|
||||
"parent_account": data[idx].get("account"),
|
||||
"indent": data[idx].get("indent") + 1,
|
||||
"has_value": True,
|
||||
"currency": total_row.get("currency"),
|
||||
}
|
||||
|
||||
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
||||
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
||||
|
||||
prepare_opening_closing(fctr_row)
|
||||
|
||||
data.insert(idx + 1, fctr_row)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += fctr_row[field]
|
||||
|
||||
|
||||
def get_fctr_root_row_index(data):
|
||||
"""
|
||||
Returns: index, root_type, parent_account
|
||||
"""
|
||||
liabilities_idx, equity_idx, tmp_idx = -1, -1, 0
|
||||
for d in data:
|
||||
if liabilities_idx == -1 and d.get("root_type") == "Liability":
|
||||
liabilities_idx = tmp_idx
|
||||
|
||||
if equity_idx == -1 and d.get("root_type") == "Equity":
|
||||
equity_idx = tmp_idx
|
||||
|
||||
tmp_idx += 1
|
||||
|
||||
if equity_idx == -1:
|
||||
return liabilities_idx
|
||||
|
||||
return equity_idx
|
||||
|
||||
|
||||
def consolidate_trial_balance_data(data, tb_data):
|
||||
if not data:
|
||||
data.extend(list(tb_data))
|
||||
return
|
||||
|
||||
for entry in tb_data:
|
||||
if entry:
|
||||
consolidate_gle_data(data, entry, tb_data)
|
||||
|
||||
|
||||
def get_reporting_currency(filters):
|
||||
reporting_currency = frappe.get_cached_value("Company", filters.company[0], "reporting_currency")
|
||||
default_currency = None
|
||||
for company in filters.company:
|
||||
company_default_currency = erpnext.get_company_currency(company)
|
||||
if not default_currency:
|
||||
default_currency = company_default_currency
|
||||
|
||||
if company_default_currency != default_currency:
|
||||
return (reporting_currency, False)
|
||||
|
||||
return (default_currency, True)
|
||||
|
||||
|
||||
def consolidate_gle_data(data, entry, tb_data):
|
||||
entry_gle_exists = False
|
||||
for gle in data:
|
||||
if gle and gle["account_name"] == entry["account_name"]:
|
||||
entry_gle_exists = True
|
||||
gle["closing_credit"] += entry["closing_credit"]
|
||||
gle["closing_debit"] += entry["closing_debit"]
|
||||
gle["credit"] += entry["credit"]
|
||||
gle["debit"] += entry["debit"]
|
||||
gle["opening_credit"] += entry["opening_credit"]
|
||||
gle["opening_debit"] += entry["opening_debit"]
|
||||
gle["has_value"] = 1
|
||||
|
||||
if not entry_gle_exists:
|
||||
entry_parent_account = next(
|
||||
(d for d in tb_data if d.get("account") == entry.get("parent_account")), None
|
||||
)
|
||||
parent_account_in_data = None
|
||||
if entry_parent_account:
|
||||
parent_account_in_data = next(
|
||||
(d for d in data if d and d.get("account_name") == entry_parent_account.get("account_name")),
|
||||
None,
|
||||
)
|
||||
if parent_account_in_data:
|
||||
entry["parent_account"] = parent_account_in_data.get("account")
|
||||
entry["indent"] = (parent_account_in_data.get("indent") or 0) + 1
|
||||
data.insert(data.index(parent_account_in_data) + 1, entry)
|
||||
else:
|
||||
entry["parent_account"] = None
|
||||
entry["indent"] = 0
|
||||
data.append(entry)
|
||||
|
||||
|
||||
def update_to_presentation_currency(data, from_currency, to_currency, date, ignore_reporting_currency):
|
||||
if from_currency == to_currency:
|
||||
return
|
||||
|
||||
exchange_rate = get_rate_as_at(date, from_currency, to_currency)
|
||||
|
||||
for d in data:
|
||||
if not ignore_reporting_currency:
|
||||
for field in value_fields:
|
||||
if d.get(field):
|
||||
d[field] = d[field] * flt(exchange_rate)
|
||||
d.update(currency=to_currency)
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Data",
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_name",
|
||||
"label": _("Account Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_number",
|
||||
"label": _("Account Number"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_debit",
|
||||
"label": _("Opening (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_credit",
|
||||
"label": _("Opening (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_debit",
|
||||
"label": _("Closing (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_credit",
|
||||
"label": _("Closing (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.consolidated_trial_balance.consolidated_trial_balance import execute
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class ForeignCurrencyTranslationReserveNotFoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TestConsolidatedTrialBalance(IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import create_company
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
# Group Company
|
||||
create_company(company_name="Parent Group Company India", is_group=1)
|
||||
|
||||
create_company(company_name="Child Company India", parent_company="Parent Group Company India")
|
||||
|
||||
# Child Company with different currency
|
||||
create_company(
|
||||
company_name="Child Company US",
|
||||
country="United States",
|
||||
currency="USD",
|
||||
parent_company="Parent Group Company India",
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Parent Group Company India",
|
||||
acc1="Marketing Expenses - PGCI",
|
||||
acc2="Cash - PGCI",
|
||||
amount=100000,
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company India", acc1="Cash - CCI", acc2="Secured Loans - CCI", amount=50000
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company US", acc1="Marketing Expenses - CCU", acc2="Cash - CCU", amount=1000
|
||||
)
|
||||
|
||||
cls.fiscal_year = get_fiscal_year(today(), company="Parent Group Company India")[0]
|
||||
|
||||
def test_single_company_report(self):
|
||||
filters = frappe._dict({"company": ["Parent Group Company India"], "fiscal_year": self.fiscal_year})
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertEqual(total_row["closing_credit"], 100000)
|
||||
|
||||
def test_child_company_report_with_same_default_currency_as_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company India"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
|
||||
def test_child_company_with_different_default_currency_from_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company US"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR")
|
||||
|
||||
fctr = [d for d in report[1] if d.get("account") == _("Foreign Currency Translation Reserve")]
|
||||
|
||||
if not fctr:
|
||||
raise ForeignCurrencyTranslationReserveNotFoundError
|
||||
|
||||
ccu_total_credit = 1000 * flt(exchange_rate)
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertNotEqual(total_row["closing_credit"], ccu_total_credit)
|
||||
|
||||
self.assertEqual(total_row["closing_credit"], flt(100000 + ccu_total_credit))
|
||||
|
||||
|
||||
def create_journal_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = args.posting_date or today()
|
||||
je.company = args.company
|
||||
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": args.acc1,
|
||||
"debit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": args.acc2,
|
||||
"credit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
je.save()
|
||||
je.submit()
|
||||
@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
|
||||
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
|
||||
|
||||
if (heading_ratios.includes(value)) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
|
||||
column.fieldtype = "Data";
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
|
||||
|
||||
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Liquidity Ratios"})
|
||||
data.append({"ratio": _("Liquidity Ratios")})
|
||||
|
||||
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||||
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
|
||||
|
||||
for d in ratio_data:
|
||||
row = {
|
||||
@@ -165,13 +165,13 @@ def add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Solvency Ratios"})
|
||||
data.append({"ratio": _("Solvency Ratios")})
|
||||
|
||||
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||||
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||||
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||||
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
|
||||
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
|
||||
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
|
||||
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
|
||||
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
@@ -195,7 +195,7 @@ def add_solvency_ratios(
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Turnover Ratios"})
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||||
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||||
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||||
["Inventory Turnover Ratio", cogs, avg_stock],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
]
|
||||
for ratio in ratio_data:
|
||||
row = {
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
</h5>
|
||||
{% } %}
|
||||
<hr>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -212,7 +212,7 @@ def get_data(
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out and total:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
@@ -325,18 +325,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
|
||||
def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
|
||||
def get_all_parents(account, parent_children_map):
|
||||
for parent, children in parent_children_map.items():
|
||||
for child in children:
|
||||
if child["name"] == account and parent:
|
||||
accounts_to_show.add(parent)
|
||||
get_all_parents(parent, parent_children_map)
|
||||
|
||||
data_with_value = []
|
||||
accounts_to_show = set()
|
||||
|
||||
for d in data:
|
||||
if show_zero_values or d.get("has_value"):
|
||||
accounts_to_show.add(d.get("account"))
|
||||
get_all_parents(d.get("account"), parent_children_map)
|
||||
|
||||
for d in data:
|
||||
if d.get("account") in accounts_to_show:
|
||||
data_with_value.append(d)
|
||||
else:
|
||||
# show group with zero balance, if there are balances against child
|
||||
children = [child.name for child in parent_children_map.get(d.get("account")) or []]
|
||||
if children:
|
||||
for row in data:
|
||||
if row.get("account") in children and row.get("has_value"):
|
||||
data_with_value.append(d)
|
||||
break
|
||||
|
||||
return data_with_value
|
||||
|
||||
@@ -437,6 +443,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
@@ -467,6 +474,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
@@ -482,9 +490,10 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
for entry in gl_entries:
|
||||
@@ -505,6 +514,7 @@ def get_accounting_entries(
|
||||
period_closing_voucher=None,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
gl_entry = frappe.qb.DocType(doctype)
|
||||
query = (
|
||||
@@ -524,6 +534,16 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
if not ignore_reporting_currency:
|
||||
query = query.select(
|
||||
gl_entry.debit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.debit_in_reporting_currency).as_("debit_in_reporting_currency"),
|
||||
gl_entry.credit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.credit_in_reporting_currency).as_("credit_in_reporting_currency"),
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
if doctype == "GL Entry":
|
||||
|
||||
@@ -75,6 +75,12 @@
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="show-filters">
|
||||
{% if subtitle %}
|
||||
{{ subtitle }}
|
||||
<hr>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table style="width:100%; font-size: 11px">
|
||||
<thead>
|
||||
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
|
||||
|
||||
@@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name":
|
||||
del columns[4:6]
|
||||
else:
|
||||
del columns[5:7]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
@@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
"width": 120,
|
||||
},
|
||||
"posting_time": {
|
||||
"label": _("Posting Time"),
|
||||
@@ -677,7 +682,9 @@ class GrossProfitGenerator:
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
@@ -395,7 +395,6 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
@@ -642,3 +641,42 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 0.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
@@ -5,31 +5,36 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -75,6 +75,8 @@ def create_company(**args):
|
||||
"company_name": args.company_name or "Trial Balance Company",
|
||||
"country": args.country or "India",
|
||||
"default_currency": args.currency or "INR",
|
||||
"parent_company": args.get("parent_company"),
|
||||
"is_group": args.get("is_group"),
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -135,15 +135,21 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters, ignore_is_opening):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
|
||||
def get_opening_balances(filters, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(
|
||||
filters, "Balance Sheet", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
pl_opening = get_rootwise_opening_balances(
|
||||
filters, "Profit and Loss", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
|
||||
balance_sheet_opening.update(pl_opening)
|
||||
return balance_sheet_opening
|
||||
|
||||
|
||||
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
def get_rootwise_opening_balances(
|
||||
filters, report_type, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True
|
||||
):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = ""
|
||||
@@ -168,6 +174,7 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
@@ -180,24 +187,41 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
start_date=start_date,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
else:
|
||||
gle = get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
|
||||
"GL Entry",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening.setdefault(
|
||||
d.account,
|
||||
{
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
},
|
||||
)
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
opening_dr_cr = {
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
}
|
||||
|
||||
opening.setdefault(d.account, opening_dr_cr)
|
||||
|
||||
if ignore_reporting_currency:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
|
||||
else:
|
||||
if d.get("report_type") == "Balance Sheet" and not (
|
||||
d.get("root_type") == "Equity" or d.get("account_type") == "Equity"
|
||||
):
|
||||
opening[d.account]["opening_debit"] += flt(d.debit) * flt(exchange_rate)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit) * flt(exchange_rate)
|
||||
else:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit_in_reporting_currency)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit_in_reporting_currency)
|
||||
|
||||
return opening
|
||||
|
||||
@@ -210,9 +234,10 @@ def get_opening_balance(
|
||||
period_closing_voucher=None,
|
||||
start_date=None,
|
||||
ignore_is_opening=0,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
closing_balance = frappe.qb.DocType(doctype)
|
||||
account = frappe.qb.DocType("Account")
|
||||
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
|
||||
|
||||
opening_balance = (
|
||||
frappe.qb.from_(closing_balance)
|
||||
@@ -224,17 +249,16 @@ def get_opening_balance(
|
||||
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
)
|
||||
.where(
|
||||
(closing_balance.company == filters.company)
|
||||
& (
|
||||
closing_balance.account.isin(
|
||||
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
|
||||
)
|
||||
)
|
||||
)
|
||||
.where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts)))
|
||||
.groupby(closing_balance.account)
|
||||
)
|
||||
|
||||
if not ignore_reporting_currency:
|
||||
opening_balance = opening_balance.select(
|
||||
Sum(closing_balance.debit_in_reporting_currency).as_("debit_in_reporting_currency"),
|
||||
Sum(closing_balance.credit_in_reporting_currency).as_("credit_in_reporting_currency"),
|
||||
)
|
||||
|
||||
if period_closing_voucher:
|
||||
opening_balance = opening_balance.where(
|
||||
closing_balance.period_closing_voucher == period_closing_voucher
|
||||
@@ -286,21 +310,24 @@ def get_opening_balance(
|
||||
if filters.project:
|
||||
opening_balance = opening_balance.where(closing_balance.project == filters.project)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
if frappe.db.count("Finance Book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
|
||||
)
|
||||
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
@@ -319,13 +346,21 @@ def get_opening_balance(
|
||||
|
||||
gle = opening_balance.run(as_dict=1)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gle, get_currency(filters))
|
||||
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
|
||||
def calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
show_net_values,
|
||||
ignore_is_opening=0,
|
||||
exchange_rate=None,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -344,8 +379,18 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
|
||||
for entry in gl_entries_by_account.get(d.name, []):
|
||||
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
if ignore_reporting_currency:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
else:
|
||||
if d.report_type == "Balance Sheet" and not (
|
||||
d.root_type == "Equity" or d.account_type == "Equity"
|
||||
):
|
||||
d["debit"] += flt(entry.debit) * flt(exchange_rate)
|
||||
d["credit"] += flt(entry.credit) * flt(exchange_rate)
|
||||
else:
|
||||
d["debit"] += flt(entry.debit_in_reporting_currency)
|
||||
d["credit"] += flt(entry.credit_in_reporting_currency)
|
||||
|
||||
d["closing_debit"] = d["opening_debit"] + d["debit"]
|
||||
d["closing_credit"] = d["opening_credit"] + d["credit"]
|
||||
|
||||
@@ -964,19 +964,28 @@ def update_accounting_ledgers_after_reference_removal(
|
||||
adv_ple.run()
|
||||
|
||||
|
||||
def remove_ref_from_advance_section(ref_doc: object = None):
|
||||
def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None):
|
||||
# TODO: this might need some testing
|
||||
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
ref_doc.set("advances", [])
|
||||
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
|
||||
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
|
||||
row_names = []
|
||||
for adv in ref_doc.get("advances") or []:
|
||||
if adv.get("reference_name", None) == payment_name:
|
||||
row_names.append(adv.name)
|
||||
|
||||
if not row_names:
|
||||
return
|
||||
|
||||
child_table = (
|
||||
"Sales Invoice Advance" if ref_doc.doctype == "Sales Invoice" else "Purchase Invoice Advance"
|
||||
)
|
||||
frappe.db.delete(child_table, {"name": ("in", row_names)})
|
||||
|
||||
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_from_advance_section(ref_doc)
|
||||
remove_ref_from_advance_section(ref_doc, payment_name)
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_jv(
|
||||
@@ -1043,7 +1052,6 @@ def remove_ref_doc_link_from_pe(
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
|
||||
var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: "Date" })];
|
||||
var asset_values = [frm.doc.gross_purchase_amount];
|
||||
var asset_values = [frm.doc.net_purchase_amount];
|
||||
|
||||
if (frm.doc.calculate_depreciation) {
|
||||
if (frm.doc.opening_accumulated_depreciation) {
|
||||
@@ -351,8 +351,8 @@ frappe.ui.form.on("Asset", {
|
||||
x_intervals.push(frappe.format(depreciation_date, { fieldtype: "Date" }));
|
||||
asset_values.push(
|
||||
flt(
|
||||
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("net_purchase_amount")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -371,8 +371,8 @@ frappe.ui.form.on("Asset", {
|
||||
$.each(asset_depr_schedule_doc.depreciation_schedule || [], function (i, v) {
|
||||
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: "Date" }));
|
||||
var asset_value = flt(
|
||||
frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - v.accumulated_depreciation_amount,
|
||||
precision("net_purchase_amount")
|
||||
);
|
||||
if (v.journal_entry) {
|
||||
asset_values.push(asset_value);
|
||||
@@ -392,8 +392,8 @@ frappe.ui.form.on("Asset", {
|
||||
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: "Date" }));
|
||||
asset_values.push(
|
||||
flt(
|
||||
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("net_purchase_amount")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -408,7 +408,7 @@ frappe.ui.form.on("Asset", {
|
||||
$.each(depr_entries || [], function (i, v) {
|
||||
x_intervals.push(frappe.format(v.posting_date, { fieldtype: "Date" }));
|
||||
let last_asset_value = asset_values[asset_values.length - 1];
|
||||
asset_values.push(flt(last_asset_value - v.value, precision("gross_purchase_amount")));
|
||||
asset_values.push(flt(last_asset_value - v.value, precision("net_purchase_amount")));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
item_code: function (frm) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.net_purchase_amount) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
@@ -447,7 +447,7 @@ frappe.ui.form.on("Asset", {
|
||||
args: {
|
||||
item_code: frm.doc.item_code,
|
||||
asset_category: frm.doc.asset_category,
|
||||
gross_purchase_amount: frm.doc.gross_purchase_amount,
|
||||
net_purchase_amount: frm.doc.net_purchase_amount,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
@@ -463,10 +463,10 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
is_composite_asset: function (frm) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
frm.set_value("gross_purchase_amount", 0);
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", 1);
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 1);
|
||||
} else {
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", 0);
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
}
|
||||
|
||||
frm.trigger("toggle_reference_doc");
|
||||
@@ -592,14 +592,14 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
calculate_depreciation: function (frm) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.net_purchase_amount) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
}
|
||||
},
|
||||
|
||||
gross_purchase_amount: function (frm) {
|
||||
net_purchase_amount: function (frm) {
|
||||
if (frm.doc.finance_books) {
|
||||
frm.doc.finance_books.forEach((d) => {
|
||||
frm.events.set_depreciation_rate(frm, d);
|
||||
@@ -650,8 +650,8 @@ frappe.ui.form.on("Asset", {
|
||||
let data = r.message;
|
||||
frm.set_value("company", data.company);
|
||||
frm.set_value("purchase_date", data.purchase_date);
|
||||
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("net_purchase_amount", data.net_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.net_purchase_amount);
|
||||
frm.set_value("asset_quantity", data.asset_quantity);
|
||||
frm.set_value("cost_center", data.cost_center);
|
||||
if (data.asset_location) {
|
||||
@@ -702,7 +702,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (expected_value_after_useful_life_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_salvage_value_percentage = flt(
|
||||
(row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount,
|
||||
(row.expected_value_after_useful_life * 100) / frm.doc.net_purchase_amount,
|
||||
precision("salvage_value_percentage", row)
|
||||
);
|
||||
frappe.model.set_value(
|
||||
@@ -715,8 +715,8 @@ frappe.ui.form.on("Asset", {
|
||||
} else if (salvage_value_percentage_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_expected_value_after_useful_life = flt(
|
||||
frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100),
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount * (row.salvage_value_percentage / 100),
|
||||
precision("net_purchase_amount")
|
||||
);
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"purchase_amount",
|
||||
"asset_quantity",
|
||||
"additional_asset_cost",
|
||||
@@ -226,13 +226,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
"fieldtype": "Date",
|
||||
@@ -244,7 +237,7 @@
|
||||
"fieldname": "calculate_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate Depreciation",
|
||||
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.gross_purchase_amount) || doc.is_composite_component"
|
||||
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -558,6 +551,13 @@
|
||||
"fieldname": "is_composite_component",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Composite Component"
|
||||
},
|
||||
{
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -601,7 +601,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-20 13:44:06.229177",
|
||||
"modified": "2025-05-23 00:53:54.249309",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -70,7 +70,6 @@ class Asset(AccountsController):
|
||||
disposal_date: DF.Date | None
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
frequency_of_depreciation: DF.Int
|
||||
gross_purchase_amount: DF.Currency
|
||||
image: DF.AttachImage | None
|
||||
insurance_end_date: DF.Date | None
|
||||
insurance_start_date: DF.Date | None
|
||||
@@ -86,6 +85,7 @@ class Asset(AccountsController):
|
||||
location: DF.Link
|
||||
maintenance_required: DF.Check
|
||||
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
||||
net_purchase_amount: DF.Currency
|
||||
next_depreciation_date: DF.Date | None
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
@@ -129,7 +129,7 @@ class Asset(AccountsController):
|
||||
self.set_missing_values()
|
||||
self.validate_gross_and_purchase_amount()
|
||||
self.validate_finance_books()
|
||||
self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
|
||||
self.total_asset_cost = self.net_purchase_amount + self.additional_asset_cost
|
||||
self.status = self.get_status()
|
||||
|
||||
def create_asset_depreciation_schedule(self):
|
||||
@@ -159,7 +159,7 @@ class Asset(AccountsController):
|
||||
return
|
||||
|
||||
self.value_after_depreciation = (
|
||||
flt(self.gross_purchase_amount)
|
||||
flt(self.net_purchase_amount)
|
||||
- flt(self.opening_accumulated_depreciation)
|
||||
+ flt(self.additional_asset_cost)
|
||||
)
|
||||
@@ -224,7 +224,7 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset or self.is_composite_asset:
|
||||
return
|
||||
|
||||
self.purchase_amount = self.gross_purchase_amount
|
||||
self.purchase_amount = self.net_purchase_amount
|
||||
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
purchase_doc = self.purchase_receipt or self.purchase_invoice
|
||||
|
||||
@@ -244,12 +244,12 @@ class Asset(AccountsController):
|
||||
|
||||
for item in purchase_doc.items:
|
||||
if self.asset_quantity > 1:
|
||||
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
if item.base_net_amount == self.net_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
elif item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
if item.base_net_rate == self.net_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
@@ -327,7 +327,7 @@ class Asset(AccountsController):
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if self.item_code and not self.get("finance_books"):
|
||||
finance_books = get_item_details(self.item_code, self.asset_category, self.gross_purchase_amount)
|
||||
finance_books = get_item_details(self.item_code, self.asset_category, self.net_purchase_amount)
|
||||
self.set("finance_books", finance_books)
|
||||
|
||||
if self.asset_owner == "Company" and not self.asset_owner_company:
|
||||
@@ -366,10 +366,8 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(
|
||||
self.gross_purchase_amount, self.precision("gross_purchase_amount")
|
||||
)
|
||||
if self.net_purchase_amount:
|
||||
self.net_purchase_amount = flt(self.net_purchase_amount, self.precision("net_purchase_amount"))
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
@@ -380,8 +378,8 @@ class Asset(AccountsController):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
|
||||
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
if not flt(self.net_purchase_amount) and not self.is_composite_asset:
|
||||
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
|
||||
if is_cwip_accounting_enabled(self.asset_category):
|
||||
if (
|
||||
@@ -440,13 +438,13 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
|
||||
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
|
||||
error_message = _(
|
||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
"Net Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
)
|
||||
error_message += "<br>"
|
||||
error_message += _("Please do not book expense of multiple assets against one single Asset.")
|
||||
frappe.throw(error_message, title=_("Invalid Gross Purchase Amount"))
|
||||
frappe.throw(error_message, title=_("Invalid Net Purchase Amount"))
|
||||
|
||||
def make_asset_movement(self):
|
||||
reference_doctype = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
@@ -486,11 +484,11 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_asset_finance_books(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
row.expected_value_after_useful_life, self.precision("net_purchase_amount")
|
||||
)
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.net_purchase_amount):
|
||||
frappe.throw(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Net Purchase Amount").format(
|
||||
row.idx
|
||||
)
|
||||
)
|
||||
@@ -507,11 +505,11 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_opening_depreciation_values(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
row.expected_value_after_useful_life, self.precision("net_purchase_amount")
|
||||
)
|
||||
depreciable_amount = flt(
|
||||
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("gross_purchase_amount"),
|
||||
flt(self.net_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("net_purchase_amount"),
|
||||
)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
@@ -576,8 +574,8 @@ class Asset(AccountsController):
|
||||
|
||||
if accumulated_depreciation_after_full_schedule:
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision("gross_purchase_amount"),
|
||||
flt(self.net_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -631,7 +629,7 @@ class Asset(AccountsController):
|
||||
|
||||
self.db_set(
|
||||
"value_after_depreciation",
|
||||
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
(flt(self.net_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
)
|
||||
|
||||
def set_status(self, status=None):
|
||||
@@ -668,7 +666,7 @@ class Asset(AccountsController):
|
||||
or self.is_fully_depreciated
|
||||
):
|
||||
status = "Fully Depreciated"
|
||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
@@ -676,16 +674,16 @@ class Asset(AccountsController):
|
||||
|
||||
def get_value_after_depreciation(self, finance_book=None):
|
||||
if not self.calculate_depreciation:
|
||||
return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
return flt(self.value_after_depreciation, self.precision("net_purchase_amount"))
|
||||
|
||||
if not finance_book:
|
||||
return flt(
|
||||
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
|
||||
self.get("finance_books")[0].value_after_depreciation, self.precision("net_purchase_amount")
|
||||
)
|
||||
|
||||
for row in self.get("finance_books"):
|
||||
if finance_book == row.finance_book:
|
||||
return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
return flt(row.value_after_depreciation, self.precision("net_purchase_amount"))
|
||||
|
||||
def get_default_finance_book_idx(self):
|
||||
if not self.get("default_finance_book") and self.company:
|
||||
@@ -889,7 +887,7 @@ class Asset(AccountsController):
|
||||
if flt(args.get("value_after_depreciation")):
|
||||
current_asset_value = flt(args.get("value_after_depreciation"))
|
||||
else:
|
||||
current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
|
||||
current_asset_value = flt(self.net_purchase_amount) - flt(self.opening_accumulated_depreciation)
|
||||
|
||||
value = flt(args.get("expected_value_after_useful_life")) / current_asset_value
|
||||
|
||||
@@ -1058,7 +1056,7 @@ def transfer_asset(args):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
def get_item_details(item_code, asset_category, net_purchase_amount):
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
@@ -1071,7 +1069,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
"daily_prorata_based": d.daily_prorata_based,
|
||||
"shift_based": d.shift_based,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
"expected_value_after_useful_life": flt(net_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
"depreciation_start_date": d.depreciation_start_date or nowdate(),
|
||||
"rate_of_depreciation": d.rate_of_depreciation,
|
||||
@@ -1211,7 +1209,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"net_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
@@ -1266,10 +1264,10 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
|
||||
|
||||
|
||||
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
||||
asset_doc.gross_purchase_amount = existing_asset.gross_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.gross_purchase_amount
|
||||
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.net_purchase_amount
|
||||
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
||||
asset_doc.total_asset_cost = asset_doc.gross_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.opening_accumulated_depreciation = (
|
||||
existing_asset.opening_accumulated_depreciation * scaling_factor
|
||||
)
|
||||
|
||||
@@ -589,8 +589,8 @@ def get_gl_entries_on_asset_regain(
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"debit_in_account_currency": asset.net_purchase_amount,
|
||||
"debit": asset.net_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
@@ -642,8 +642,8 @@ def get_gl_entries_on_asset_disposal(
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"credit_in_account_currency": asset.net_purchase_amount,
|
||||
"credit": asset.net_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
@@ -681,7 +681,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
accumulated_depr_amount = flt(asset.net_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
@@ -792,7 +792,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
||||
validate_disposal_date(asset_doc.available_for_use_date, getdate(disposal_date), "available for use")
|
||||
|
||||
if asset_doc.available_for_use_date == getdate(disposal_date):
|
||||
return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
|
||||
return flt(asset_doc.net_purchase_amount - asset_doc.opening_accumulated_depreciation)
|
||||
|
||||
if not asset_doc.calculate_depreciation:
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
@@ -813,8 +813,8 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
||||
].accumulated_depreciation_amount
|
||||
|
||||
return flt(
|
||||
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
flt(asset_doc.net_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(asset.asset_category, "Computers")
|
||||
|
||||
def test_gross_purchase_amount_is_mandatory(self):
|
||||
def test_net_purchase_amount_is_mandatory(self):
|
||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.net_purchase_amount = 0
|
||||
|
||||
self.assertRaises(frappe.MandatoryError, asset.save)
|
||||
|
||||
@@ -213,8 +213,8 @@ class TestAsset(AssetSetup):
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
self.assertEqual(accumulated_depr_amount, 18000.0)
|
||||
|
||||
@@ -252,8 +252,8 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
second_asset_depr_schedule.depreciation_amount = 9006.17
|
||||
@@ -266,10 +266,10 @@ class TestAsset(AssetSetup):
|
||||
date,
|
||||
original_schedule_date=get_last_day(date),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("net_purchase_amount"))
|
||||
self.assertEqual(
|
||||
accumulated_depr_amount,
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Scrapped")
|
||||
@@ -278,13 +278,13 @@ class TestAsset(AssetSetup):
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
)
|
||||
@@ -304,8 +304,8 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0
|
||||
|
||||
@@ -347,21 +347,21 @@ class TestAsset(AssetSetup):
|
||||
|
||||
asset.load_from_db()
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount = flt(accumulated_depr_amount - 18000)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(accumulated_depr_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(accumulated_depr_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
@@ -385,7 +385,7 @@ class TestAsset(AssetSetup):
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
net_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
@@ -483,7 +483,7 @@ class TestAsset(AssetSetup):
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2024-03-31",
|
||||
opening_accumulated_depreciation=493.15,
|
||||
gross_purchase_amount=12000,
|
||||
net_purchase_amount=12000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
@@ -493,7 +493,7 @@ class TestAsset(AssetSetup):
|
||||
post_depreciation_entries(date="2024-03-31")
|
||||
|
||||
self.assertEqual(asset.asset_quantity, 10)
|
||||
self.assertEqual(asset.gross_purchase_amount, 12000)
|
||||
self.assertEqual(asset.net_purchase_amount, 12000)
|
||||
self.assertEqual(asset.opening_accumulated_depreciation, 493.15)
|
||||
|
||||
new_asset = split_asset(asset.name, 2)
|
||||
@@ -510,14 +510,14 @@ class TestAsset(AssetSetup):
|
||||
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
|
||||
|
||||
self.assertEqual(new_asset.asset_quantity, 2)
|
||||
self.assertEqual(new_asset.gross_purchase_amount, 2400)
|
||||
self.assertEqual(new_asset.net_purchase_amount, 2400)
|
||||
self.assertEqual(new_asset.opening_accumulated_depreciation, 98.63)
|
||||
self.assertEqual(new_asset.split_from, asset.name)
|
||||
self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 400)
|
||||
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 400)
|
||||
|
||||
self.assertEqual(asset.asset_quantity, 8)
|
||||
self.assertEqual(asset.gross_purchase_amount, 9600)
|
||||
self.assertEqual(asset.net_purchase_amount, 9600)
|
||||
self.assertEqual(asset.opening_accumulated_depreciation, 394.52)
|
||||
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 1600)
|
||||
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 1600)
|
||||
@@ -603,7 +603,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = (
|
||||
nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
|
||||
)
|
||||
self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
|
||||
self.assertEqual(asset_doc.net_purchase_amount, 5250.0)
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
@@ -732,7 +732,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=12000,
|
||||
net_purchase_amount=12000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
@@ -935,7 +935,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2022-02-15",
|
||||
purchase_date="2022-02-15",
|
||||
depreciation_method="Written Down Value",
|
||||
gross_purchase_amount=10000,
|
||||
net_purchase_amount=10000,
|
||||
expected_value_after_useful_life=5000,
|
||||
depreciation_start_date="2022-02-28",
|
||||
total_number_of_depreciations=5,
|
||||
@@ -1123,7 +1123,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertTrue(depr_schedule_doc.has_pro_rata)
|
||||
|
||||
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):
|
||||
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000)."""
|
||||
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > net_purchase_amount(100,000)."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1151,7 +1151,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_opening_accumulated_depreciation(self):
|
||||
"""Tests if an error is raised when opening_accumulated_depreciation > (gross_purchase_amount - expected_value_after_useful_life)."""
|
||||
"""Tests if an error is raised when opening_accumulated_depreciation > (net_purchase_amount - expected_value_after_useful_life)."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1489,7 +1489,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft")
|
||||
)
|
||||
|
||||
asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt(
|
||||
asset_value_after_full_schedule = flt(asset.net_purchase_amount) - flt(
|
||||
accumulated_depreciation_after_full_schedule
|
||||
)
|
||||
|
||||
@@ -1739,7 +1739,7 @@ def create_asset(**args):
|
||||
"calculate_depreciation": args.calculate_depreciation or 0,
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"net_purchase_amount": args.net_purchase_amount or 100000,
|
||||
"purchase_amount": args.purchase_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
@@ -1771,7 +1771,7 @@ def create_asset(**args):
|
||||
)
|
||||
|
||||
if asset.is_composite_asset:
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.net_purchase_amount = 0
|
||||
asset.purchase_amount = 0
|
||||
|
||||
if not args.do_not_save:
|
||||
|
||||
@@ -569,14 +569,14 @@ class AssetCapitalization(StockController):
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
if self.docstatus == 2:
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
|
||||
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
||||
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value)
|
||||
else:
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
|
||||
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
||||
|
||||
asset_doc.db_set("gross_purchase_amount", gross_purchase_amount)
|
||||
asset_doc.db_set("net_purchase_amount", net_purchase_amount)
|
||||
asset_doc.db_set("purchase_amount", purchase_amount)
|
||||
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
@@ -273,7 +273,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
@@ -333,7 +333,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
@@ -528,8 +528,8 @@ def create_depreciation_asset(**args):
|
||||
asset.purchase_date = args.purchase_date or "2020-01-01"
|
||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||
|
||||
asset.gross_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_amount = asset.gross_purchase_amount
|
||||
asset.net_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_amount = asset.net_purchase_amount
|
||||
|
||||
finance_book = asset.append("finance_books")
|
||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"naming_series",
|
||||
"company",
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"finance_book",
|
||||
@@ -163,15 +163,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Gross Purchase Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
@@ -210,12 +201,21 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Value After Depreciation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Net Purchase Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-02 17:54:20.635668",
|
||||
"modified": "2025-05-23 01:17:16.708004",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
@@ -252,7 +252,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
|
||||
finance_book: DF.Link | None
|
||||
finance_book_id: DF.Int
|
||||
frequency_of_depreciation: DF.Int
|
||||
gross_purchase_amount: DF.Currency
|
||||
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
||||
net_purchase_amount: DF.Currency
|
||||
notes: DF.SmallText | None
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
@@ -149,7 +149,7 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
|
||||
self.opening_number_of_booked_depreciations = (
|
||||
self.asset_doc.opening_number_of_booked_depreciations or 0
|
||||
)
|
||||
self.gross_purchase_amount = self.asset_doc.gross_purchase_amount
|
||||
self.net_purchase_amount = self.asset_doc.net_purchase_amount
|
||||
self.depreciation_method = self.fb_row.depreciation_method
|
||||
self.total_number_of_depreciations = self.fb_row.total_number_of_depreciations
|
||||
self.frequency_of_depreciation = self.fb_row.frequency_of_depreciation
|
||||
|
||||
@@ -82,19 +82,19 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
self.set_depreciation_amount_for_last_row(row_idx)
|
||||
|
||||
self.depreciation_amount = flt(
|
||||
self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
|
||||
self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")
|
||||
)
|
||||
if not self.depreciation_amount:
|
||||
break
|
||||
|
||||
self.pending_depreciation_amount = flt(
|
||||
self.pending_depreciation_amount - self.depreciation_amount,
|
||||
self.asset_doc.precision("gross_purchase_amount"),
|
||||
self.asset_doc.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
self.adjust_depr_amount_for_salvage_value(row_idx)
|
||||
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")) > 0:
|
||||
self.add_depr_schedule_row(row_idx)
|
||||
|
||||
def initialize_variables(self):
|
||||
@@ -310,7 +310,7 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
)
|
||||
|
||||
self.depreciation_amount = flt(
|
||||
self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
|
||||
self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")
|
||||
)
|
||||
if self.depreciation_amount > 0:
|
||||
self.schedule_date = self.disposal_date
|
||||
@@ -380,13 +380,13 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
|
||||
def validate_depreciation_amount_for_low_value_assets(self):
|
||||
"""
|
||||
If gross purchase amount is too low, then depreciation amount
|
||||
If net purchase amount is too low, then depreciation amount
|
||||
can come zero sometimes based on the frequency and number of depreciations.
|
||||
"""
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")) <= 0:
|
||||
frappe.throw(
|
||||
_("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
|
||||
frappe.bold(self.asset_doc.gross_purchase_amount),
|
||||
_("Net Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
|
||||
frappe.bold(self.asset_doc.net_purchase_amount),
|
||||
frappe.bold(self.fb_row.total_number_of_depreciations),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
daily_prorata_based=1,
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
@@ -171,7 +171,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-12-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=3,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
@@ -199,7 +199,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
daily_prorata_based=1,
|
||||
gross_purchase_amount=1096,
|
||||
net_purchase_amount=1096,
|
||||
available_for_use_date="2020-01-15",
|
||||
depreciation_start_date="2020-01-31",
|
||||
frequency_of_depreciation=1,
|
||||
@@ -377,7 +377,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -457,7 +457,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair_for_6_months_frequency(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -522,7 +522,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-15",
|
||||
@@ -601,7 +601,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_wdv_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
available_for_use_date="2023-04-01",
|
||||
@@ -662,7 +662,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -742,7 +742,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_value_adjustent(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=1000,
|
||||
net_purchase_amount=1000,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -844,7 +844,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_value_adjustent_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-15",
|
||||
@@ -918,7 +918,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_for_parallel_adjustment_and_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=600,
|
||||
net_purchase_amount=600,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2021-01-01",
|
||||
@@ -1007,7 +1007,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_sale_of_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=600,
|
||||
net_purchase_amount=600,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2021-01-01",
|
||||
@@ -1085,7 +1085,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_sale_of_asset_wdv_method(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
available_for_use_date="2021-01-01",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form
|
||||
from frappe.utils import cstr, get_link_to_form
|
||||
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
|
||||
@@ -143,8 +143,8 @@ class AssetMovement(Document):
|
||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||
asset = frappe.get_doc("Asset", asset_id)
|
||||
|
||||
if employee and employee != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", employee)
|
||||
if cstr(employee) != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee))
|
||||
if location and location != asset.location:
|
||||
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestAssetShiftAllocation(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=120000,
|
||||
net_purchase_amount=120000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_data(filters):
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
@@ -87,7 +87,7 @@ def get_data(filters):
|
||||
depreciation_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
revaluation_amount = revaluation_amount_map.get(asset.asset_id, 0.0)
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount
|
||||
asset.net_purchase_amount
|
||||
- asset.opening_accumulated_depreciation
|
||||
- depreciation_amount
|
||||
+ revaluation_amount
|
||||
@@ -101,7 +101,7 @@ def get_data(filters):
|
||||
"cost_center": asset.cost_center,
|
||||
"vendor_name": pr_supplier_map.get(asset.purchase_receipt)
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"net_purchase_amount": asset.net_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": depreciation_amount,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
@@ -268,6 +268,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
.where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account))
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.is_opening == "No")
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
)
|
||||
@@ -318,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
|
||||
.select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
|
||||
.where(gle.account == aca.fixed_asset_account)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.is_opening == "No")
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
)
|
||||
@@ -354,7 +356,7 @@ def get_group_by_data(
|
||||
fields = [
|
||||
group_by,
|
||||
"name",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"calculate_depreciation",
|
||||
]
|
||||
@@ -369,7 +371,7 @@ def get_group_by_data(
|
||||
a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0)
|
||||
a["revaluation_amount"] = revaluation_amount_map.get(a["name"], 0.0)
|
||||
a["asset_value"] = (
|
||||
a["gross_purchase_amount"]
|
||||
a["net_purchase_amount"]
|
||||
- a["opening_accumulated_depreciation"]
|
||||
- a["depreciated_amount"]
|
||||
+ a["revaluation_amount"]
|
||||
@@ -383,7 +385,7 @@ def get_group_by_data(
|
||||
data.append(a)
|
||||
else:
|
||||
for field in (
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"depreciated_amount",
|
||||
"asset_value",
|
||||
@@ -434,8 +436,8 @@ def get_columns(filters):
|
||||
"width": 216,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"label": _("Net Purchase Amount"),
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
@@ -495,8 +497,8 @@ def get_columns(filters):
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"label": _("Net Purchase Amount"),
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"get_items_from_open_material_requests",
|
||||
"mps",
|
||||
"column_break_7",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
@@ -1315,6 +1316,13 @@
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "mps",
|
||||
"fieldtype": "Link",
|
||||
"label": "MPS",
|
||||
"options": "Master Production Schedule",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1322,7 +1330,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-31 17:19:40.816883",
|
||||
"modified": "2025-08-28 11:00:56.635116",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -108,6 +108,7 @@ class PurchaseOrder(BuyingController):
|
||||
items: DF.Table[PurchaseOrderItem]
|
||||
language: DF.Data | None
|
||||
letter_head: DF.Link | None
|
||||
mps: DF.Link | None
|
||||
named_place: DF.Data | None
|
||||
naming_series: DF.Literal["PUR-ORD-.YYYY.-"]
|
||||
net_total: DF.Currency
|
||||
|
||||
@@ -540,12 +540,8 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_make_purchase_invoice_with_terms(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
|
||||
@@ -569,7 +565,6 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
|
||||
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
|
||||
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||
@@ -717,6 +712,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
)
|
||||
self.assertEqual(due_date, "2023-03-31")
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
|
||||
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
@@ -910,18 +906,16 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -935,8 +929,6 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
|
||||
@@ -109,22 +109,6 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Bank Account"),
|
||||
function () {
|
||||
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Pricing Rule"),
|
||||
function () {
|
||||
frm.trigger("make_pricing_rule");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
function () {
|
||||
|
||||
@@ -284,15 +284,15 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_message():
|
||||
return """<span class="indicator">
|
||||
Valid till :
|
||||
return f"""<span class="indicator">
|
||||
{_("Valid Till")}:
|
||||
</span>
|
||||
<span class="indicator orange">
|
||||
Expires in a week or less
|
||||
{_("Expires in a week or less")}
|
||||
</span>
|
||||
|
||||
<span class="indicator red">
|
||||
Expires today / Already Expired
|
||||
{_("Expires today or already expired")}
|
||||
</span>"""
|
||||
|
||||
|
||||
|
||||
@@ -228,6 +228,11 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_date_with_fiscal_year()
|
||||
self.validate_party_accounts()
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
# validate inter company transaction rate
|
||||
@@ -279,11 +284,6 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
@@ -2570,6 +2570,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.payment_schedule = []
|
||||
self.payment_terms_template = po_or_so.payment_terms_template
|
||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
||||
|
||||
for schedule in po_or_so.payment_schedule:
|
||||
payment_schedule = {
|
||||
@@ -2582,6 +2583,17 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
if schedule.due_date_based_on:
|
||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||
|
||||
if schedule.discount_validity_based_on:
|
||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
@@ -3384,14 +3396,27 @@ def get_payment_term_details(
|
||||
term = frappe.get_doc("Payment Term", term)
|
||||
else:
|
||||
term_details.payment_term = term.payment_term
|
||||
term_details.description = term.description
|
||||
term_details.invoice_portion = term.invoice_portion
|
||||
|
||||
fields_to_copy = [
|
||||
"description",
|
||||
"invoice_portion",
|
||||
"discount_type",
|
||||
"discount",
|
||||
"mode_of_payment",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
]
|
||||
|
||||
for field in fields_to_copy:
|
||||
term_details[field] = term.get(field)
|
||||
|
||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||
term_details.discount_type = term.discount_type
|
||||
term_details.discount = term.discount
|
||||
term_details.outstanding = term_details.payment_amount
|
||||
term_details.mode_of_payment = term.mode_of_payment
|
||||
term_details.base_outstanding = term_details.base_payment_amount
|
||||
|
||||
if bill_date:
|
||||
term_details.due_date = get_due_date(term, bill_date)
|
||||
@@ -3410,11 +3435,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
|
||||
due_date = None
|
||||
date = bill_date or posting_date
|
||||
if term.due_date_based_on == "Day(s) after invoice date":
|
||||
due_date = add_days(date, term.credit_days)
|
||||
due_date = add_days(date, cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = add_days(get_last_day(date), term.credit_days)
|
||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||
due_date = get_last_day(add_months(date, term.credit_months))
|
||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
@@ -3422,11 +3447,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
|
||||
discount_validity = None
|
||||
date = bill_date or posting_date
|
||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||
discount_validity = add_days(date, term.discount_validity)
|
||||
discount_validity = add_days(date, cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||
discount_validity = add_days(get_last_day(date), term.discount_validity)
|
||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||
discount_validity = get_last_day(add_months(date, term.discount_validity))
|
||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||
return discount_validity
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.get_item_details import get_conversion_factor, get_item_defaults
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -307,6 +307,60 @@ class BuyingController(SubcontractingController):
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def set_gl_entry_for_purchase_expense(self, gl_entries):
|
||||
if self.doctype == "Purchase Invoice" and not self.update_stock:
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
details = get_purchase_expense_account(row.item_code, self.company)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details.purchase_expense_account = frappe.get_cached_value(
|
||||
"Company", self.company, "purchase_expense_account"
|
||||
)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
return
|
||||
|
||||
if not details.purchase_expense_contra_account:
|
||||
details.purchase_expense_contra_account = frappe.get_cached_value(
|
||||
"Company", self.company, "purchase_expense_contra_account"
|
||||
)
|
||||
|
||||
if not details.purchase_expense_contra_account:
|
||||
frappe.throw(
|
||||
_("Please set Purchase Expense Contra Account in Company {0}").format(self.company)
|
||||
)
|
||||
|
||||
amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount"))
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=details.purchase_expense_account,
|
||||
cost_center=row.cost_center,
|
||||
debit=amount,
|
||||
credit=0.0,
|
||||
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
|
||||
against_account=details.purchase_expense_contra_account,
|
||||
account_currency=frappe.get_cached_value(
|
||||
"Account", details.purchase_expense_account, "account_currency"
|
||||
),
|
||||
item=row,
|
||||
)
|
||||
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=details.purchase_expense_contra_account,
|
||||
cost_center=row.cost_center,
|
||||
debit=0.0,
|
||||
credit=amount,
|
||||
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
|
||||
against_account=details.purchase_expense_account,
|
||||
account_currency=frappe.get_cached_value(
|
||||
"Account", details.purchase_expense_contra_account, "account_currency"
|
||||
),
|
||||
item=row,
|
||||
)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
||||
@@ -875,7 +929,7 @@ class BuyingController(SubcontractingController):
|
||||
self.update_fixed_asset(field, delete_asset=True)
|
||||
|
||||
def validate_budget(self):
|
||||
if frappe.get_single_value("Accounts Settings", "use_new_budget_controller"):
|
||||
if not frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"):
|
||||
from erpnext.controllers.budget_controller import BudgetValidation
|
||||
|
||||
val = BudgetValidation(doc=self)
|
||||
@@ -998,7 +1052,7 @@ class BuyingController(SubcontractingController):
|
||||
"purchase_date": self.posting_date,
|
||||
"calculate_depreciation": 0,
|
||||
"purchase_amount": purchase_amount,
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
@@ -1171,3 +1225,33 @@ def validate_item_type(doc, fieldname, message):
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_purchase_expense_account(item_code, company):
|
||||
defaults = get_item_defaults(item_code, company)
|
||||
|
||||
details = frappe._dict(
|
||||
{
|
||||
"purchase_expense_account": defaults.get("purchase_expense_account"),
|
||||
"purchase_expense_contra_account": defaults.get("purchase_expense_contra_account"),
|
||||
}
|
||||
)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": defaults.item_group, "company": company},
|
||||
["purchase_expense_account", "purchase_expense_contra_account"],
|
||||
as_dict=1,
|
||||
) or frappe._dict({})
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": defaults.brand, "company": company},
|
||||
["purchase_expense_account", "purchase_expense_contra_account"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return details or frappe._dict({})
|
||||
|
||||
@@ -318,7 +318,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
|
||||
@@ -13,7 +13,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@@ -202,7 +202,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
current_stock_qty = args.get(column)
|
||||
elif args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
current_stock_qty = (
|
||||
args.get(column) * args.get("conversion_factor", 1.0)
|
||||
if column != "stock_qty"
|
||||
else args.get(column)
|
||||
)
|
||||
else:
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
@@ -850,13 +854,14 @@ def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
@@ -868,6 +873,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
"`tabSerial and Batch Bundle`.`item_code`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
@@ -881,6 +887,16 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if doctype == "Packed Item":
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
if row.voucher_type == "Delivery Note":
|
||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||
elif row.voucher_type == "Sales Invoice":
|
||||
key = frappe.get_cached_value("Sales Invoice Item", key, "sales_invoice_item")
|
||||
|
||||
key = (row.item_code, key)
|
||||
|
||||
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
row.qty = -1 * row.qty
|
||||
|
||||
@@ -909,6 +925,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
if doctype == "Packed Item":
|
||||
filters = get_filters_for_packed_item(field, reference_ids)
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
@@ -922,10 +940,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if _bundle_ids and doctype == "Packed Item":
|
||||
return _bundle_ids
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
if "name" in filters:
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
@@ -968,10 +990,29 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def get_filters_for_packed_item(field, reference_ids):
|
||||
names = []
|
||||
filters = {"docstatus": 1, "dn_detail": ("in", reference_ids)}
|
||||
if dns := frappe.get_all("Delivery Note Item", filters=filters, pluck="name"):
|
||||
names.extend(dns)
|
||||
|
||||
filters = {"docstatus": 1, "sales_invoice_item": ("in", reference_ids)}
|
||||
if sis := frappe.get_all("Sales Invoice Item", filters=filters, pluck="name"):
|
||||
names.extend(sis)
|
||||
|
||||
if names:
|
||||
reference_ids.extend(names)
|
||||
|
||||
return {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "stock_qty"
|
||||
|
||||
if not hasattr(row, qty_field):
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
@@ -1061,6 +1102,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
if not qty_field:
|
||||
qty_field = "stock_qty"
|
||||
|
||||
if not hasattr(child_doc, qty_field):
|
||||
qty_field = "qty"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
@@ -1082,8 +1126,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"batches": data.get("batches"),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -519,8 +519,15 @@ class SellingController(StockController):
|
||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||
continue
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not self.get("return_against") or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
get_valuation_method(d.item_code) == "Moving Average"
|
||||
and self.get("is_return")
|
||||
and not item_details.has_serial_no
|
||||
and not item_details.has_batch_no
|
||||
):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
@@ -999,6 +1006,9 @@ def set_default_income_account_for_item(obj):
|
||||
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if parent.get("is_return") and parent.get("packed_items"):
|
||||
return
|
||||
|
||||
if child.get("use_serial_batch_fields"):
|
||||
return
|
||||
|
||||
@@ -1017,8 +1027,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
"voucher_type": parent.doctype,
|
||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
|
||||
"posting_date": parent.posting_date,
|
||||
"posting_time": parent.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time),
|
||||
"qty": child.qty,
|
||||
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
|
||||
"company": parent.company,
|
||||
|
||||
@@ -265,6 +265,8 @@ class StatusUpdater(Document):
|
||||
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
|
||||
continue
|
||||
|
||||
items_to_validate = []
|
||||
|
||||
# get unique transactions to update
|
||||
for d in self.get_all_children():
|
||||
if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"):
|
||||
@@ -286,31 +288,63 @@ class StatusUpdater(Document):
|
||||
)
|
||||
|
||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||
args["name"] = d.get(args["join_field"])
|
||||
|
||||
is_from_pp = (
|
||||
hasattr(d, "production_plan_sub_assembly_item")
|
||||
and frappe.db.get_value(
|
||||
"Production Plan Sub Assembly Item",
|
||||
d.production_plan_sub_assembly_item,
|
||||
"type_of_manufacturing",
|
||||
items_to_validate.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"name": d.get(args["join_field"]),
|
||||
"production_plan_sub_assembly_item": d.get(
|
||||
"production_plan_sub_assembly_item"
|
||||
),
|
||||
"idx": d.idx,
|
||||
"child_doc": d,
|
||||
}
|
||||
)
|
||||
== "Subcontract"
|
||||
)
|
||||
args["item_code"] = "production_item" if is_from_pp else "item_code"
|
||||
|
||||
# get all qty where qty > target_field
|
||||
item = frappe.db.sql(
|
||||
"""select `{item_code}` as item_code, `{target_ref_field}`,
|
||||
`{target_field}`, parenttype, parent from `tab{target_dt}`
|
||||
where `{target_ref_field}` < `{target_field}`
|
||||
and name=%s and docstatus=1""".format(**args),
|
||||
args["name"],
|
||||
as_dict=1,
|
||||
if items_to_validate:
|
||||
pp_sub_assembly_items = [
|
||||
item.production_plan_sub_assembly_item
|
||||
for item in items_to_validate
|
||||
if item.production_plan_sub_assembly_item
|
||||
]
|
||||
|
||||
pp_subcontract_items = []
|
||||
if pp_sub_assembly_items:
|
||||
pp_subcontract_items = frappe.db.get_all(
|
||||
"Production Plan Sub Assembly Item",
|
||||
filters={
|
||||
"name": ("in", pp_sub_assembly_items),
|
||||
"type_of_manufacturing": "Subcontract",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
regular_items = []
|
||||
pp_items = []
|
||||
|
||||
for item in items_to_validate:
|
||||
if item.production_plan_sub_assembly_item in pp_subcontract_items:
|
||||
pp_items.append(item.name)
|
||||
else:
|
||||
regular_items.append(item.name)
|
||||
|
||||
item_details = []
|
||||
|
||||
# Query regular items with item_code field
|
||||
if regular_items:
|
||||
item_details.extend(self.fetch_items_with_pending_qty(args, "item_code", regular_items))
|
||||
|
||||
# Query production plan items with production_item field
|
||||
if pp_items:
|
||||
item_details.extend(self.fetch_items_with_pending_qty(args, "production_item", pp_items))
|
||||
|
||||
item_lookup = {item.name: item for item in item_details}
|
||||
|
||||
for child_item in items_to_validate:
|
||||
item = item_lookup.get(child_item.name)
|
||||
|
||||
if item:
|
||||
item = item[0]
|
||||
item["idx"] = d.idx
|
||||
item["idx"] = child_item.idx
|
||||
item["target_ref_field"] = args["target_ref_field"].replace("_", " ")
|
||||
|
||||
# if not item[args['target_ref_field']]:
|
||||
@@ -323,6 +357,28 @@ class StatusUpdater(Document):
|
||||
elif item[args["target_ref_field"]]:
|
||||
self.check_overflow_with_allowance(item, args)
|
||||
|
||||
def fetch_items_with_pending_qty(self, args, item_field, items):
|
||||
doctype = frappe.qb.DocType(args["target_dt"])
|
||||
item_field = doctype[item_field]
|
||||
target_ref_field = doctype[args["target_ref_field"]]
|
||||
target_field = doctype[args["target_field"]]
|
||||
|
||||
return (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(
|
||||
doctype.name,
|
||||
item_field.as_("item_code"),
|
||||
target_ref_field,
|
||||
target_field,
|
||||
doctype.parenttype,
|
||||
doctype.parent,
|
||||
)
|
||||
.where(target_ref_field < target_field)
|
||||
.where(doctype.name.isin(items))
|
||||
.where(doctype.docstatus == 1)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def check_overflow_with_allowance(self, item, args):
|
||||
"""
|
||||
Checks if there is overflow condering a relaxation allowance
|
||||
|
||||
@@ -27,6 +27,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_type_of_transaction,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
@@ -266,8 +267,7 @@ class StockController(AccountsController):
|
||||
):
|
||||
bundle_details = {
|
||||
"item_code": row.get("rm_item_code") or row.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
@@ -360,10 +360,20 @@ class StockController(AccountsController):
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
if table_name == "packed_items":
|
||||
field = "parent_detail_docname"
|
||||
child_doctype = "Packed Item"
|
||||
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
value = row.get(field)
|
||||
if table_name == "packed_items" and row.get("parent_detail_docname"):
|
||||
value = self.get_value_for_packed_item(row)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if data := available_dict.get(value):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
@@ -379,6 +389,14 @@ class StockController(AccountsController):
|
||||
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||
)
|
||||
|
||||
def get_value_for_packed_item(self, row):
|
||||
parent_items = self.get("items", {"name": row.parent_detail_docname})
|
||||
if parent_items:
|
||||
ref = parent_items[0].get("dn_detail")
|
||||
return (row.item_code, ref)
|
||||
|
||||
return None
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
@@ -413,6 +431,12 @@ class StockController(AccountsController):
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
if table_name == "packed_items" and row.get("parent_detail_docname"):
|
||||
parent_rows = self.get("items", {"name": row.parent_detail_docname}) or []
|
||||
for d in parent_rows:
|
||||
if d.get(field) and not d.get(bundle_field):
|
||||
reference_ids.append(d.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
|
||||
@@ -12,6 +12,9 @@ from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
get_available_serial_nos,
|
||||
get_voucher_wise_serial_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -52,9 +55,42 @@ class SubcontractingController(StockController):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
self.validate_items()
|
||||
self.create_raw_materials_supplied()
|
||||
self.set_valuation_rate_for_rm()
|
||||
else:
|
||||
super().validate()
|
||||
|
||||
def set_valuation_rate_for_rm(self):
|
||||
rate_changed = False
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
for row in self.supplied_items:
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": row.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": flt(row.consumed_qty) * (-1 if not self.is_return else 1),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||
"voucher_detail_no": row.name,
|
||||
"batch_no": row.batch_no,
|
||||
"serial_no": row.serial_no,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
}
|
||||
)
|
||||
|
||||
rate = get_incoming_rate(kwargs)
|
||||
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
|
||||
if flt(rate, precision) != flt(row.rate, precision):
|
||||
row.rate = rate
|
||||
row.amount = flt(row.consumed_qty) * flt(rate)
|
||||
rate_changed = True
|
||||
|
||||
if rate_changed:
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
def validate_rejected_warehouse(self):
|
||||
for item in self.get("items"):
|
||||
if flt(item.rejected_qty) and not item.rejected_warehouse:
|
||||
@@ -166,6 +202,9 @@ class SubcontractingController(StockController):
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
if not self.get(self.raw_material_table):
|
||||
return
|
||||
|
||||
item_dict = self.__get_data_before_save()
|
||||
if not item_dict:
|
||||
return True
|
||||
@@ -537,8 +576,7 @@ class SubcontractingController(StockController):
|
||||
"qty": qty,
|
||||
"serial_nos": serial_nos,
|
||||
"batches": batches,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"do_not_submit": True,
|
||||
"type_of_transaction": "Outward" if qty > 0 else "Inward",
|
||||
@@ -616,6 +654,67 @@ class SubcontractingController(StockController):
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
self.set_batch_for_supplied_items()
|
||||
|
||||
def set_batch_for_supplied_items(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
|
||||
from erpnext.stock.get_item_details import get_filtered_serial_nos
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
for row in self.supplied_items:
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
continue
|
||||
|
||||
if not row.use_serial_batch_fields:
|
||||
continue
|
||||
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": row.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": flt(row.consumed_qty),
|
||||
}
|
||||
)
|
||||
|
||||
if item_details.has_serial_no and not row.serial_and_batch_bundle and not row.serial_no:
|
||||
serial_nos = get_available_serial_nos(kwargs)
|
||||
if serial_nos:
|
||||
serial_nos = [sn.get("serial_no") for sn in serial_nos]
|
||||
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
|
||||
row.serial_no = "\n".join(serial_nos)
|
||||
|
||||
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
if batches:
|
||||
consumed_qty = row.consumed_qty
|
||||
for index, d in enumerate(batches):
|
||||
if consumed_qty <= 0:
|
||||
break
|
||||
|
||||
if index == 0:
|
||||
row.batch_no = d.get("batch_no")
|
||||
row.consumed_qty = d.get("qty")
|
||||
consumed_qty -= d.get("qty")
|
||||
else:
|
||||
new_row = self.append("supplied_items", {})
|
||||
new_row.update(frappe.copy_doc(row).as_dict())
|
||||
new_row.update(
|
||||
{
|
||||
"consumed_qty": d.get("qty"),
|
||||
"batch_no": d.get("batch_no"),
|
||||
"rate": row.rate,
|
||||
"amount": flt(d.get("qty")) * flt(row.rate),
|
||||
}
|
||||
)
|
||||
consumed_qty -= d.get("qty")
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
|
||||
@@ -208,12 +208,18 @@ class calculate_taxes_and_totals:
|
||||
if item.discount_amount and not item.discount_percentage:
|
||||
item.rate = item.rate_with_margin - item.discount_amount
|
||||
else:
|
||||
item.discount_amount = item.rate_with_margin - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.rate_with_margin - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
|
||||
elif flt(item.price_list_rate) > 0:
|
||||
item.discount_amount = item.price_list_rate - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.price_list_rate - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
|
||||
item.discount_amount = item.price_list_rate - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.price_list_rate - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
|
||||
item.net_rate = item.rate
|
||||
|
||||
|
||||
@@ -1308,6 +1308,7 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
"Subcontracted Item SA9": {"stock_uom": "Litre"},
|
||||
"Subcontracted Item SA10": {},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
@@ -1329,6 +1330,7 @@ def make_raw_materials():
|
||||
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
|
||||
"Subcontracted SRM Item 8": {},
|
||||
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
|
||||
"Subcontracted SRM Item 10": {},
|
||||
}
|
||||
|
||||
for item, properties in raw_materials.items():
|
||||
@@ -1357,6 +1359,7 @@ def make_service_items():
|
||||
"Subcontracted Service Item 7": {},
|
||||
"Subcontracted Service Item 8": {},
|
||||
"Subcontracted Service Item 9": {},
|
||||
"Subcontracted Service Item 10": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
@@ -1381,6 +1384,7 @@ def make_bom_for_subcontracted_items():
|
||||
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
|
||||
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
|
||||
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
|
||||
"Subcontracted Item SA10": ["Subcontracted SRM Item 10"],
|
||||
}
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"canonical_uri",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
@@ -71,10 +72,17 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "code_list.canonical_uri",
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Canonical URI"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified": "2025-10-04 17:22:28.176155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
@@ -94,10 +102,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user