Compare commits

..

1 Commits

Author SHA1 Message Date
Imesha Sudasingha
a28dfcd44c fix(item): avoid inheriting item defaults from identically named items (#49571)
(cherry picked from commit 9e58a56b5c)

# Conflicts:
#	erpnext/stock/doctype/item/item.py
2025-09-17 12:03:43 +00:00
275 changed files with 32847 additions and 194607 deletions

View File

@@ -85,7 +85,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -111,7 +111,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -109,7 +109,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -94,7 +94,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -46,8 +46,7 @@ def validate_service_stop_date(doc):
if (
old_stop_dates
and old_stop_dates.get(item.name)
and item.service_stop_date
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
and item.service_stop_date != old_stop_dates.get(item.name)
):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))

View File

@@ -11,7 +11,6 @@
"end_date",
"column_break_4",
"company",
"disabled",
"section_break_7",
"closed_documents"
],
@@ -50,13 +49,6 @@
"options": "Company",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
@@ -70,11 +62,10 @@
}
],
"links": [],
"modified": "2025-10-06 15:00:15.568067",
"modified": "2024-03-27 13:05:57.388109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -114,9 +105,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -28,7 +28,6 @@ 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
@@ -117,7 +116,6 @@ 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)

View File

@@ -98,7 +98,7 @@
"payment_request_settings",
"create_pr_in_draft_status",
"budget_settings",
"use_legacy_budget_controller"
"use_new_budget_controller"
],
"fields": [
{
@@ -598,6 +598,12 @@
"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",
@@ -645,12 +651,6 @@
"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-09-24 16:08:08.515254",
"modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -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_legacy_budget_controller: DF.Check
use_new_budget_controller: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -82,7 +82,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-13 15:11:58.300836",
"modified": "2025-07-29 11:37:42.678556",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",

View File

@@ -34,15 +34,3 @@ 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"],
)

View File

@@ -155,10 +155,8 @@ def get_payment_entries_for_bank_clearance(
entries = []
condition = ""
pe_condition = ""
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(
f"""
@@ -183,20 +181,19 @@ def get_payment_entries_for_bank_clearance(
payment_entries = frappe.db.sql(
f"""
select
"Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
from `tabPayment Entry` as pe
join `tabCompany` c on c.name = pe.company
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
{pe_condition}
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
pe.posting_date ASC, pe.name DESC
posting_date ASC, name DESC
""",
{
"account": account,

View File

@@ -409,7 +409,7 @@ def start_auto_reconcile(
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
["payment_entry", "journal_entry", "sales_invoice"],
["payment_entry", "journal_entry"],
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, transaction)
query = get_si_matching_query(exact_match, currency, common_filters)
queries.append(query)
if transaction.withdrawal > 0.0:
@@ -854,14 +854,11 @@ def get_je_matching_query(
return query
def get_si_matching_query(exact_match, currency, common_filters, transaction):
def get_si_matching_query(exact_match, currency, common_filters):
# 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
@@ -874,11 +871,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.join(si)
.on(sip.parent == si.name)
.select(
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
(party_rank + amount_rank + 1).as_("rank"),
ConstantColumn("Sales Invoice").as_("doctype"),
si.name,
sip.amount.as_("paid_amount"),
sip.reference_no,
ConstantColumn("").as_("reference_no"),
ConstantColumn("").as_("reference_date"),
si.customer.as_("party"),
ConstantColumn("Customer").as_("party_type"),
@@ -892,9 +889,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.where(si.currency == currency)
)
if frappe.flags.auto_reconcile_vouchers is True:
query = query.where(ref_condition)
return query

View File

@@ -111,54 +111,20 @@ class BankStatementImport(DataImport):
return None
def preprocess_mt940_content(content: str) -> str:
"""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*)$")
def replace_statement_number(match):
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
# If statement number is longer than 5 digits, truncate to last 5 digits
if len(statement_num) > 5:
statement_num = statement_num[-5:]
return prefix + statement_num + sequence_part + trailing_space
# Apply the replacement
processed_content = pattern.sub(replace_statement_number, content)
return processed_content
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
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:
if not is_mt940_format(content):
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
if is_mt940 and not doc.import_mt940_fromat:
if is_mt940_format(content) and not doc.import_mt940_fromat:
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
try:
# Preprocess MT940 content to fix statement number format issues
processed_content = preprocess_mt940_content(content)
transactions = mt940.parse(processed_content)
transactions = mt940.parse(content)
except Exception as e:
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
@@ -283,7 +249,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
def update_mapping_db(bank, template_options):
"""Update bank transaction mapping database with template options."""
bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping:
d.delete()
@@ -295,7 +260,6 @@ def update_mapping_db(bank, template_options):
def add_bank_account(data, bank_account):
"""Add bank account information to data rows."""
bank_account_loc = None
if "Bank Account" not in data[0]:
data[0].append("Bank Account")
@@ -312,7 +276,6 @@ def add_bank_account(data, bank_account):
def write_files(import_file, data):
"""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]
@@ -322,12 +285,11 @@ def write_files(import_file, data):
with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file)
writer.writerows(data)
elif extension in ("xlsx", "xls"):
elif extension == "xlsx" or "xls":
write_xlsx(data, "trans", file_path=full_file_path)
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
"""Write data to Excel file with formatting."""
# from xlsx utils with changes
column_widths = column_widths or []
if wb is None:

View File

@@ -1,209 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
is_mt940_format,
preprocess_mt940_content,
)
from frappe.tests import IntegrationTestCase
class TestBankStatementImport(unittest.TestCase):
"""Unit tests for Bank Statement Import functions"""
def test_preprocess_mt940_content_with_long_statement_number(self):
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
# Test case with 6-digit statement number (167619 -> 67619)
mt940_content = ":28C:167619/1"
expected_content = ":28C:67619/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_with_normal_statement_number(self):
"""Test that statement numbers with 5 or fewer digits are unchanged"""
# Test case with 5-digit statement number (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
# Test case with 4-digit statement number (should remain unchanged)
mt940_content = ":28C:1234/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
def test_preprocess_mt940_content_without_sequence_number(self):
"""Test statement number truncation without sequence number"""
# Test case with long statement number but no sequence (no /1)
mt940_content = ":28C:987654321"
expected_content = ":28C:54321"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_multiple_occurrences(self):
"""Test multiple statement numbers in the same content"""
mt940_content = """:28C:167619/1
:28C:987654/2"""
expected_content = """:28C:67619/1
:28C:87654/2"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_edge_cases(self):
"""Test edge cases like empty content and content without :28C: tags"""
# Test empty content
self.assertEqual(preprocess_mt940_content(""), "")
# Test content without :28C: tags
content_without_28c = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
result = preprocess_mt940_content(content_without_28c)
self.assertEqual(result, content_without_28c) # Should be unchanged
def test_preprocess_mt940_content_with_full_mt940_document(self):
"""Test preprocessing with complete MT940 document"""
mt940_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
expected_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:67619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_is_mt940_format_detection(self):
"""Test MT940 format detection function"""
# Valid MT940 content with all required tags
valid_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
self.assertTrue(is_mt940_format(valid_mt940))
# Invalid MT940 content (CSV format)
invalid_mt940 = """Date,Description,Amount
2023-01-01,Test Transaction,100.00
2023-01-02,Another Transaction,-50.00"""
self.assertFalse(is_mt940_format(invalid_mt940))
# Partially valid MT940 (missing some required tags)
partial_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
self.assertFalse(is_mt940_format(partial_mt940))
# Empty content
self.assertFalse(is_mt940_format(""))
def test_preprocess_mt940_content_boundary_conditions(self):
"""Test boundary conditions for statement number length"""
# Test exactly 6 digits (should be truncated)
mt940_content = ":28C:123456/1"
expected_content = ":28C:23456/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test exactly 5 digits (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content)
# Test very long statement number
mt940_content = ":28C:123456789012345/1"
expected_content = ":28C:12345/1" # Last 5 digits
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_real_world_case(self):
"""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
:25:1234567890
:28C:167619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
# Expected result with statement number 167619 truncated to 67619
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619
:25:1234567890
:28C:67619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Verify that the problematic statement number was actually changed
self.assertIn(":28C:67619/1", result)
self.assertNotIn(":28C:167619/1", result)
# Verify that other content remains unchanged
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces
mt940_content = ":28C:167619/1 \n"
expected_content = ":28C:67619/1 \n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with Windows line endings (CRLF)
mt940_content = ":28C:167619/1\r\n"
expected_content = ":28C:67619/1\r\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with leading spaces (should not match as it's not line start)
mt940_content = " :28C:167619/1\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should remain unchanged
class TestBankStatementImport(IntegrationTestCase):
pass

View File

@@ -116,7 +116,7 @@
{
"allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Small Text",
"fieldtype": "Data",
"label": "Reference Number"
},
{
@@ -239,7 +239,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-09-26 17:06:29.207673",
"modified": "2025-08-29 11:53:45.908169",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -36,7 +36,7 @@ class BankTransaction(Document):
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.SmallText | None
reference_number: DF.Data | None
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
transaction_id: DF.Data | None
transaction_type: DF.Data | None

View File

@@ -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_legacy_budget_controller").then((value) => {
if (value) {
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
if (!value) {
frm.get_field("control_action_for_cumulative_expense_section").hide();
}
});

View File

@@ -24,7 +24,7 @@ class TestBudget(ERPNextTestSuite):
cls.make_projects()
def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")

View File

@@ -137,20 +137,18 @@ 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")
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(
self.voucher_type, self.voucher_no, self.account
)
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
# Zero value transaction is not allowed
if not (
@@ -256,7 +254,7 @@ class GLEntry(Document):
)
def validate_cost_center(self):
if not self.cost_center or self.is_cancelled:
if not self.cost_center:
return
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])

View File

@@ -64,7 +64,6 @@
"addtional_info",
"mode_of_payment",
"payment_order",
"party_not_required",
"column_break3",
"is_opening",
"stock_entry",
@@ -578,14 +577,6 @@
"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",
@@ -600,7 +591,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-09-29 13:05:46.982277",
"modified": "2025-07-06 15:22:58.465131",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -72,7 +72,6 @@ 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
@@ -194,8 +193,8 @@ class JournalEntry(AccountsController):
def on_submit(self):
self.validate_cheque_info()
self.make_gl_entries()
self.check_credit_limit()
self.make_gl_entries()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -645,11 +644,8 @@ class JournalEntry(AccountsController):
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"]:
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
if not (d.party_type and d.party):
frappe.throw(
_(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
@@ -1241,11 +1237,6 @@ 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,
@@ -1273,7 +1264,6 @@ 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))

View File

@@ -8,7 +8,6 @@ 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):
@@ -592,15 +591,6 @@ 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,

View File

@@ -285,7 +285,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-09-29 13:01:48.916517",
"modified": "2025-07-25 04:45:28.117715",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -585,7 +585,6 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm);
}
frm.events.paid_from_account_currency(frm);
}
);
},
@@ -608,7 +607,6 @@ frappe.ui.form.on("Payment Entry", {
frm.events.received_amount(frm);
}
}
frm.events.paid_to_account_currency(frm);
}
);
},

View File

@@ -129,13 +129,7 @@ class PaymentRequest(Document):
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
if (
flt(
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
get_currency_precision(),
)
> ref_amount
):
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype

View File

@@ -10,19 +10,14 @@
"description",
"section_break_4",
"due_date",
"invoice_portion",
"mode_of_payment",
"column_break_5",
"due_date_based_on",
"credit_days",
"credit_months",
"invoice_portion",
"section_break_6",
"discount_date",
"discount",
"discount_type",
"discount_date",
"column_break_9",
"discount_validity_based_on",
"discount_validity",
"discount",
"section_break_9",
"payment_amount",
"outstanding",
@@ -177,50 +172,12 @@
"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-07-31 08:38:25.820701",
"modified": "2025-03-11 11:06:51.792982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -232,4 +189,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,27 +17,12 @@ 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

View File

@@ -162,4 +162,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -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.get("coupon_code_based") == 1:
if pricing_rule.coupon_code_based == 1:
if not args.coupon_code:
continue
coupon_code = frappe.db.get_value(

View File

@@ -13,7 +13,7 @@
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
@@ -34,13 +34,11 @@
<thead>
<tr>
<th style="width: 12%">{{ _("Date") }}</th>
<th style="width: 20%">{{ _("Reference") }}</th>
<th style="width: 15%">{{ _("Reference") }}</th>
<th style="width: 25%">{{ _("Remarks") }}</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>
@@ -49,51 +47,36 @@
{% if(row.posting_date) %}
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
<td>{{ row.voucher_type }}
<br>{{ row.voucher_no }}
{% if not (filters.party or filters.account) %}
<br>{{ row.voucher_no }}</td>
<td>
{% if not (filters.party or filters.account) %}
{{ row.party or row.account }}
<br>
{% endif %}
<br>{{ _("Remarks:") }} {{ row.remarks }}
{% if row.bill_no %}
{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
<br>{{ _("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>
<b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b>
</td>
<td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</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>

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
@@ -70,7 +69,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Frequency",
"options": "Daily\nWeekly\nBiweekly\nMonthly\nQuarterly"
"options": "Weekly\nMonthly\nQuarterly"
},
{
"fieldname": "company",
@@ -417,7 +416,7 @@
}
],
"links": [],
"modified": "2025-10-07 12:19:20.719898",
"modified": "2025-09-03 14:24:43.608565",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -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, add_to_date, format_date, getdate, today
from frappe.utils import add_days, add_months, 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["Daily", "Weekly", "Biweekly", "Monthly", "Quarterly"]
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check
@@ -555,9 +555,8 @@ 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 in ("Daily", "Weekly", "Biweekly"):
frequency = {"Daily": 1, "Weekly": 7, "Biweekly": 14}
new_to_date = add_days(new_to_date, frequency[doc.frequency])
if doc.frequency == "Weekly":
new_to_date = add_days(new_to_date, 7)
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)

View File

@@ -23,7 +23,7 @@
{% endif %}
</div>
<h2 class="text-center" style="margin-top:0">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</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) and filters.show_remarks %}
{% if not(filters.show_future_payments) %}
<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) and filters.show_remarks %}
{% if not (filters.show_future_payments) %}
<td>
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
{{ data[i]["party"] }}
@@ -327,23 +327,12 @@
{% endfor %}
<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>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
{% else %}
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
{% endif %}
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
</tbody>
</table>
<br>

View File

@@ -81,7 +81,6 @@ 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)))

View File

@@ -884,7 +884,6 @@ 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):
@@ -1229,7 +1228,7 @@ class PurchaseInvoice(BuyingController):
)
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_net_purchase_amount_for_linked_assets(item)
self.update_gross_purchase_amount_for_linked_assets(item)
def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict()
@@ -1291,7 +1290,7 @@ class PurchaseInvoice(BuyingController):
),
)
def update_net_purchase_amount_for_linked_assets(self, item):
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all(
"Asset",
filters={
@@ -1307,7 +1306,7 @@ class PurchaseInvoice(BuyingController):
"Asset",
asset.name,
{
"net_purchase_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"purchase_amount": purchase_amount,
},
)

View File

@@ -2147,16 +2147,19 @@ 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",
@@ -2182,6 +2185,7 @@ 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",
@@ -2629,38 +2633,6 @@ 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

View File

@@ -232,7 +232,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
company.save()
test_cc = company.cost_center
default_expense_account = company.service_expense_account
default_expense_account = company.default_expense_account
item = make_item(properties={"is_stock_item": 0})

View File

@@ -279,59 +279,6 @@ 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()
@@ -2855,59 +2802,6 @@ 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)

View File

@@ -483,23 +483,18 @@ class Subscription(Document):
return invoice
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> 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
in [
"Beginning of the current subscription period",
"Days before the current subscription period",
]
),
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
)
items = []
@@ -516,19 +511,33 @@ class Subscription(Document):
deferred = frappe.db.get_value("Item", item_code, deferred_field)
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 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,
}
if deferred:
item.update(

View File

@@ -8,7 +8,6 @@ from frappe.utils.data import (
add_days,
add_months,
add_to_date,
add_years,
cint,
date_diff,
flt,
@@ -556,33 +555,6 @@ 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")

View File

@@ -35,7 +35,7 @@ def make_gl_entries(
):
if gl_map:
if (
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
frappe.get_single_value("Accounts Settings", "use_new_budget_controller")
and gl_map[0].voucher_type != "Period Closing Voucher"
):
bud_val = BudgetValidation(gl_map=gl_map)
@@ -159,7 +159,6 @@ 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

View File

@@ -1,108 +0,0 @@
<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>

View File

@@ -1,125 +0,0 @@
<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

View File

@@ -164,12 +164,6 @@
{% } %}
</tr>
</thead>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>

View File

@@ -976,7 +976,6 @@ class ReceivablePayableReport:
if self.account_type == "Receivable":
self.add_customer_filters()
self.exclude_employee_transaction()
elif self.account_type == "Payable":
self.add_supplier_filters()
@@ -1056,9 +1055,6 @@ class ReceivablePayableReport:
)
)
def exclude_employee_transaction(self):
self.qb_selection_filter.append(self.ple.party_type != "Employee")
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):
@@ -1272,7 +1268,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):

View File

@@ -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")

View File

@@ -103,7 +103,7 @@ def get_data(filters):
"depreciation_amount": d.debit,
"depreciation_date": d.posting_date,
"value_after_depreciation": (
flt(row.net_purchase_amount) - flt(row.accumulated_depreciation_amount)
flt(row.gross_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",
"net_purchase_amount",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"asset_category",
"status",
@@ -151,7 +151,7 @@ def get_columns():
},
{
"label": _("Purchase Amount"),
"fieldname": "net_purchase_amount",
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"width": 120,
},

View File

@@ -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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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, a.asset_name,
SELECT a.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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_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.net_purchase_amount
a.gross_purchase_amount
else
0
end
@@ -583,14 +583,6 @@ def get_columns(filters):
"width": 120,
}
)
columns.append(
{
"label": _("Asset Name"),
"fieldname": "asset_name",
"fieldtype": "Data",
"width": 140,
}
)
columns += [
{

View File

@@ -5,35 +5,30 @@ 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,
},
{
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"]["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"]["export_hidden_cols"] = true;

View File

@@ -1 +0,0 @@
{% include "accounts/report/financial_statements.html" %}

View File

@@ -1,101 +0,0 @@
// 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,
};

View File

@@ -1,34 +0,0 @@
{
"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
}

View File

@@ -1,469 +0,0 @@
# 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,
},
]

View File

@@ -1,123 +0,0 @@
# 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()

View File

@@ -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";
}

View File

@@ -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 = {

View File

@@ -34,12 +34,6 @@
</h5>
{% } %}
<hr>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<table class="table table-bordered">
<thead>
<tr>

View File

@@ -212,7 +212,7 @@ def get_data(
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
out = filter_out_zero_value_rows(out, parent_children_map)
if out and total:
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
@@ -325,24 +325,18 @@ 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
@@ -443,7 +437,6 @@ 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 = []
@@ -474,7 +467,6 @@ 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
@@ -490,10 +482,9 @@ 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") and ignore_reporting_currency:
if filters and filters.get("presentation_currency"):
convert_to_presentation_currency(gl_entries, get_currency(filters))
for entry in gl_entries:
@@ -514,7 +505,6 @@ 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 = (
@@ -534,16 +524,6 @@ 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":

View File

@@ -75,12 +75,6 @@
</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">

View File

@@ -178,12 +178,7 @@ 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
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]
del columns[4:6]
total_base_amount = 0
total_buying_amount = 0
@@ -280,7 +275,7 @@ def get_columns(group_wise_columns, filters):
"label": _("Posting Date"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 120,
"width": 100,
},
"posting_time": {
"label": _("Posting Time"),
@@ -682,9 +677,7 @@ 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,
)

View File

@@ -1,9 +1,9 @@
import frappe
from frappe import qb
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
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,6 +395,7 @@ 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)
@@ -641,42 +642,3 @@ 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)

View File

@@ -5,36 +5,31 @@ 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,
},
{
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"]["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"]["export_hidden_cols"] = true;

View File

@@ -75,8 +75,6 @@ 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)

View File

@@ -135,21 +135,15 @@ def get_data(filters):
return data
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
)
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)
balance_sheet_opening.update(pl_opening)
return balance_sheet_opening
def get_rootwise_opening_balances(
filters, report_type, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True
):
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
gle = []
last_period_closing_voucher = ""
@@ -174,7 +168,6 @@ def get_rootwise_opening_balances(
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
@@ -187,41 +180,24 @@ def get_rootwise_opening_balances(
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,
ignore_reporting_currency=ignore_reporting_currency,
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
)
opening = frappe._dict()
for d in gle:
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)
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)
return opening
@@ -234,10 +210,9 @@ 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)
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
account = frappe.qb.DocType("Account")
opening_balance = (
frappe.qb.from_(closing_balance)
@@ -249,16 +224,17 @@ 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(accounts)))
.where(
(closing_balance.company == filters.company)
& (
closing_balance.account.isin(
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
)
)
)
.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
@@ -310,24 +286,21 @@ def get_opening_balance(
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
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.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:
@@ -346,21 +319,13 @@ def get_opening_balance(
gle = opening_balance.run(as_dict=1)
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
if filters and filters.get("presentation_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,
exchange_rate=None,
ignore_reporting_currency=True,
):
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -379,18 +344,8 @@ def calculate_values(
for entry in gl_entries_by_account.get(d.name, []):
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
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["debit"] += flt(entry.debit)
d["credit"] += flt(entry.credit)
d["closing_debit"] = d["opening_debit"] + d["debit"]
d["closing_credit"] = d["opening_credit"] + d["credit"]

View File

@@ -964,28 +964,19 @@ def update_accounting_ledgers_after_reference_removal(
adv_ple.run()
def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None):
def remove_ref_from_advance_section(ref_doc: object = None):
# TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
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)})
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()
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, payment_name)
remove_ref_from_advance_section(ref_doc)
def remove_ref_doc_link_from_jv(
@@ -1052,6 +1043,7 @@ 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
@@ -2514,10 +2506,6 @@ def build_qb_match_conditions(doctype, user=None) -> list:
for filter in match_filters:
for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, [])
cond = None
if link_option == doctype:
cond = _dt["name"].isin(allowed_values)
for fieldname in fieldnames:
field = _dt[fieldname]
@@ -2526,7 +2514,6 @@ def build_qb_match_conditions(doctype, user=None) -> list:
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
if cond:
criterion.append(cond)
return criterion

View File

@@ -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.net_purchase_amount];
var asset_values = [frm.doc.gross_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.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
precision("net_purchase_amount")
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
precision("gross_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.net_purchase_amount - v.accumulated_depreciation_amount,
precision("net_purchase_amount")
frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount,
precision("gross_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.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
precision("net_purchase_amount")
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
precision("gross_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("net_purchase_amount")));
asset_values.push(flt(last_asset_value - v.value, precision("gross_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.net_purchase_amount) {
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_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,
net_purchase_amount: frm.doc.net_purchase_amount,
gross_purchase_amount: frm.doc.gross_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("net_purchase_amount", 0);
frm.set_df_property("net_purchase_amount", "read_only", 1);
frm.set_value("gross_purchase_amount", 0);
frm.set_df_property("gross_purchase_amount", "read_only", 1);
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
frm.set_df_property("gross_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.net_purchase_amount) {
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
frm.trigger("set_finance_book");
} else {
frm.set_value("finance_books", []);
}
},
net_purchase_amount: function (frm) {
gross_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("net_purchase_amount", data.net_purchase_amount);
frm.set_value("purchase_amount", data.net_purchase_amount);
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
frm.set_value("purchase_amount", data.gross_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.net_purchase_amount,
(row.expected_value_after_useful_life * 100) / frm.doc.gross_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.net_purchase_amount * (row.salvage_value_percentage / 100),
precision("net_purchase_amount")
frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100),
precision("gross_purchase_amount")
);
frappe.model.set_value(
row.doctype,

View File

@@ -31,7 +31,7 @@
"purchase_date",
"available_for_use_date",
"column_break_23",
"net_purchase_amount",
"gross_purchase_amount",
"purchase_amount",
"asset_quantity",
"additional_asset_cost",
@@ -226,6 +226,13 @@
"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",
@@ -237,7 +244,7 @@
"fieldname": "calculate_depreciation",
"fieldtype": "Check",
"label": "Calculate Depreciation",
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component"
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.gross_purchase_amount) || doc.is_composite_component"
},
{
"default": "0",
@@ -551,13 +558,6 @@
"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-23 00:53:54.249309",
"modified": "2025-05-20 13:44:06.229177",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -70,6 +70,7 @@ 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
@@ -85,7 +86,6 @@ 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.net_purchase_amount + self.additional_asset_cost
self.total_asset_cost = self.gross_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.net_purchase_amount)
flt(self.gross_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.net_purchase_amount
self.purchase_amount = self.gross_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.net_purchase_amount and item.qty == self.asset_quantity:
if item.base_net_amount == self.gross_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.net_purchase_amount and item.qty == self.asset_quantity:
if item.base_net_rate == self.gross_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.net_purchase_amount)
finance_books = get_item_details(self.item_code, self.asset_category, self.gross_purchase_amount)
self.set("finance_books", finance_books)
if self.asset_owner == "Company" and not self.asset_owner_company:
@@ -366,8 +366,10 @@ class Asset(AccountsController):
)
def validate_precision(self):
if self.net_purchase_amount:
self.net_purchase_amount = flt(self.net_purchase_amount, self.precision("net_purchase_amount"))
if self.gross_purchase_amount:
self.gross_purchase_amount = flt(
self.gross_purchase_amount, self.precision("gross_purchase_amount")
)
if self.opening_accumulated_depreciation:
self.opening_accumulated_depreciation = flt(
@@ -378,8 +380,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.net_purchase_amount) and not self.is_composite_asset:
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
if (
@@ -438,13 +440,13 @@ class Asset(AccountsController):
if self.is_existing_asset:
return
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
error_message = _(
"Net Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
"Gross 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 Net Purchase Amount"))
frappe.throw(error_message, title=_("Invalid Gross Purchase Amount"))
def make_asset_movement(self):
reference_doctype = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
@@ -484,11 +486,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("net_purchase_amount")
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
)
if flt(row.expected_value_after_useful_life) >= flt(self.net_purchase_amount):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(
_("Row {0}: Expected Value After Useful Life must be less than Net Purchase Amount").format(
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
row.idx
)
)
@@ -505,11 +507,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("net_purchase_amount")
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
)
depreciable_amount = flt(
flt(self.net_purchase_amount) - flt(row.expected_value_after_useful_life),
self.precision("net_purchase_amount"),
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
self.precision("gross_purchase_amount"),
)
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
frappe.throw(
@@ -574,8 +576,8 @@ class Asset(AccountsController):
if accumulated_depreciation_after_full_schedule:
asset_value_after_full_schedule = flt(
flt(self.net_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
self.precision("net_purchase_amount"),
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
self.precision("gross_purchase_amount"),
)
if (
@@ -629,7 +631,7 @@ class Asset(AccountsController):
self.db_set(
"value_after_depreciation",
(flt(self.net_purchase_amount) - flt(self.opening_accumulated_depreciation)),
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
)
def set_status(self, status=None):
@@ -666,7 +668,7 @@ class Asset(AccountsController):
or self.is_fully_depreciated
):
status = "Fully Depreciated"
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
status = "Partially Depreciated"
elif self.docstatus == 2:
status = "Cancelled"
@@ -674,16 +676,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("net_purchase_amount"))
return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
if not finance_book:
return flt(
self.get("finance_books")[0].value_after_depreciation, self.precision("net_purchase_amount")
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
)
for row in self.get("finance_books"):
if finance_book == row.finance_book:
return flt(row.value_after_depreciation, self.precision("net_purchase_amount"))
return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
def get_default_finance_book_idx(self):
if not self.get("default_finance_book") and self.company:
@@ -887,7 +889,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.net_purchase_amount) - flt(self.opening_accumulated_depreciation)
current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
value = flt(args.get("expected_value_after_useful_life")) / current_asset_value
@@ -1056,7 +1058,7 @@ def transfer_asset(args):
@frappe.whitelist()
def get_item_details(item_code, asset_category, net_purchase_amount):
def get_item_details(item_code, asset_category, gross_purchase_amount):
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
books = []
for d in asset_category_doc.finance_books:
@@ -1069,7 +1071,7 @@ def get_item_details(item_code, asset_category, net_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(net_purchase_amount)
"expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100),
"depreciation_start_date": d.depreciation_start_date or nowdate(),
"rate_of_depreciation": d.rate_of_depreciation,
@@ -1209,7 +1211,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"),
"net_purchase_amount": flt(first_item.base_net_amount),
"gross_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"),
@@ -1264,10 +1266,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.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
asset_doc.purchase_amount = existing_asset.net_purchase_amount
asset_doc.gross_purchase_amount = existing_asset.gross_purchase_amount * scaling_factor
asset_doc.purchase_amount = existing_asset.gross_purchase_amount
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
asset_doc.total_asset_cost = asset_doc.gross_purchase_amount + asset_doc.additional_asset_cost
asset_doc.opening_accumulated_depreciation = (
existing_asset.opening_accumulated_depreciation * scaling_factor
)

View File

@@ -589,8 +589,8 @@ def get_gl_entries_on_asset_regain(
asset.get_gl_dict(
{
"account": fixed_asset_account,
"debit_in_account_currency": asset.net_purchase_amount,
"debit": asset.net_purchase_amount,
"debit_in_account_currency": asset.gross_purchase_amount,
"debit": asset.gross_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.net_purchase_amount,
"credit": asset.net_purchase_amount,
"credit_in_account_currency": asset.gross_purchase_amount,
"credit": asset.gross_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.net_purchase_amount) - flt(value_after_depreciation)
accumulated_depr_amount = flt(asset.gross_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.net_purchase_amount - asset_doc.opening_accumulated_depreciation)
return flt(asset_doc.gross_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.net_purchase_amount) - accumulated_depr_amount,
asset_doc.precision("net_purchase_amount"),
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
asset_doc.precision("gross_purchase_amount"),
)

View File

@@ -64,9 +64,9 @@ class TestAsset(AssetSetup):
self.assertEqual(asset.asset_category, "Computers")
def test_net_purchase_amount_is_mandatory(self):
def test_gross_purchase_amount_is_mandatory(self):
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.net_purchase_amount = 0
asset.gross_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.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("net_purchase_amount"),
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_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.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("net_purchase_amount"),
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_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("net_purchase_amount"))
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEqual(
accumulated_depr_amount,
flt(18000.0 + pro_rata_amount, asset.precision("net_purchase_amount")),
flt(18000.0 + pro_rata_amount, asset.precision("gross_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("net_purchase_amount")),
flt(18000.0 + pro_rata_amount, asset.precision("gross_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("net_purchase_amount")),
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
)
@@ -304,8 +304,8 @@ class TestAsset(AssetSetup):
self.assertEqual(asset.status, "Partially Depreciated")
accumulated_depr_amount = flt(
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("net_purchase_amount"),
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_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.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("net_purchase_amount"),
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount = flt(accumulated_depr_amount - 18000)
expected_gle = (
(
"_Test Accumulated Depreciations - _TC",
flt(accumulated_depr_amount, asset.precision("net_purchase_amount")),
flt(accumulated_depr_amount, asset.precision("gross_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("net_purchase_amount")),
flt(57000.0 - pro_rata_amount, asset.precision("gross_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,
net_purchase_amount=60000,
gross_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,
net_purchase_amount=12000,
gross_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.net_purchase_amount, 12000)
self.assertEqual(asset.gross_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.net_purchase_amount, 2400)
self.assertEqual(new_asset.gross_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.net_purchase_amount, 9600)
self.assertEqual(asset.gross_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.net_purchase_amount, 5250.0)
self.assertEqual(asset_doc.gross_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",
net_purchase_amount=12000,
gross_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",
net_purchase_amount=10000,
gross_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) > net_purchase_amount(100,000)."""
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_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 > (net_purchase_amount - expected_value_after_useful_life)."""
"""Tests if an error is raised when opening_accumulated_depreciation > (gross_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.net_purchase_amount) - flt(
asset_value_after_full_schedule = flt(asset.gross_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,
"net_purchase_amount": args.net_purchase_amount or 100000,
"gross_purchase_amount": args.gross_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.net_purchase_amount = 0
asset.gross_purchase_amount = 0
asset.purchase_amount = 0
if not args.do_not_save:

View File

@@ -569,14 +569,14 @@ class AssetCapitalization(StockController):
asset_doc = frappe.get_doc("Asset", self.target_asset)
if self.docstatus == 2:
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
gross_purchase_amount = asset_doc.gross_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:
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
asset_doc.db_set("net_purchase_amount", net_purchase_amount)
asset_doc.db_set("gross_purchase_amount", gross_purchase_amount)
asset_doc.db_set("purchase_amount", purchase_amount)
frappe.msgprint(

View File

@@ -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.net_purchase_amount, total_amount)
self.assertEqual(target_asset.gross_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.net_purchase_amount, total_amount)
self.assertEqual(target_asset.gross_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.net_purchase_amount, total_amount)
self.assertEqual(target_asset.gross_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.net_purchase_amount, total_amount)
self.assertEqual(target_asset.gross_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.net_purchase_amount = args.asset_value or 100000
asset.purchase_amount = asset.net_purchase_amount
asset.gross_purchase_amount = args.asset_value or 100000
asset.purchase_amount = asset.gross_purchase_amount
finance_book = asset.append("finance_books")
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"

View File

@@ -11,7 +11,7 @@
"naming_series",
"company",
"column_break_2",
"net_purchase_amount",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"opening_number_of_booked_depreciations",
"finance_book",
@@ -163,6 +163,15 @@
"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",
@@ -201,21 +210,12 @@
"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": "2025-05-23 01:17:16.708004",
"modified": "2024-12-02 17:54:20.635668",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
@@ -252,8 +252,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -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.net_purchase_amount = self.asset_doc.net_purchase_amount
self.gross_purchase_amount = self.asset_doc.gross_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

View File

@@ -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("net_purchase_amount")
self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
)
if not self.depreciation_amount:
break
self.pending_depreciation_amount = flt(
self.pending_depreciation_amount - self.depreciation_amount,
self.asset_doc.precision("net_purchase_amount"),
self.asset_doc.precision("gross_purchase_amount"),
)
self.adjust_depr_amount_for_salvage_value(row_idx)
if flt(self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")) > 0:
if flt(self.depreciation_amount, self.asset_doc.precision("gross_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("net_purchase_amount")
self.depreciation_amount, self.asset_doc.precision("gross_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 net purchase amount is too low, then depreciation amount
If gross 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("net_purchase_amount")) <= 0:
if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) <= 0:
frappe.throw(
_("Net Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
frappe.bold(self.asset_doc.net_purchase_amount),
_("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
frappe.bold(self.asset_doc.gross_purchase_amount),
frappe.bold(self.fb_row.total_number_of_depreciations),
)
)

View File

@@ -93,7 +93,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2024-07-31",
total_number_of_depreciations=24,
frequency_of_depreciation=1,
net_purchase_amount=731,
gross_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,
net_purchase_amount=731,
gross_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,
net_purchase_amount=731,
gross_purchase_amount=731,
)
expected_schedules = [
@@ -199,7 +199,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1,
depreciation_method="Straight Line",
daily_prorata_based=1,
net_purchase_amount=1096,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=1000,
gross_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",
net_purchase_amount=500,
gross_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",
net_purchase_amount=600,
gross_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",
net_purchase_amount=600,
gross_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",
net_purchase_amount=500,
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Written Down Value",
available_for_use_date="2021-01-01",

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_link_to_form
from frappe.utils import 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 cstr(employee) != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee))
if employee and employee != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", employee)
if location and location != asset.location:
frappe.db.set_value("Asset", asset_id, "location", location)

View File

@@ -26,7 +26,7 @@ class TestAssetShiftAllocation(IntegrationTestCase):
calculate_depreciation=1,
available_for_use_date="2023-01-01",
purchase_date="2023-01-01",
net_purchase_amount=120000,
gross_purchase_amount=120000,
depreciation_start_date="2023-01-31",
total_number_of_depreciations=12,
frequency_of_depreciation=1,

View File

@@ -72,7 +72,7 @@ def get_data(filters):
"purchase_receipt",
"asset_category",
"purchase_date",
"net_purchase_amount",
"gross_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.net_purchase_amount
asset.gross_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),
"net_purchase_amount": asset.net_purchase_amount,
"gross_purchase_amount": asset.gross_purchase_amount,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
"depreciated_amount": depreciation_amount,
"available_for_use_date": asset.available_for_use_date,
@@ -268,7 +268,6 @@ 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)
)
@@ -319,7 +318,6 @@ 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)
)
@@ -356,7 +354,7 @@ def get_group_by_data(
fields = [
group_by,
"name",
"net_purchase_amount",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"calculate_depreciation",
]
@@ -371,7 +369,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["net_purchase_amount"]
a["gross_purchase_amount"]
- a["opening_accumulated_depreciation"]
- a["depreciated_amount"]
+ a["revaluation_amount"]
@@ -385,7 +383,7 @@ def get_group_by_data(
data.append(a)
else:
for field in (
"net_purchase_amount",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"depreciated_amount",
"asset_value",
@@ -436,8 +434,8 @@ def get_columns(filters):
"width": 216,
},
{
"label": _("Net Purchase Amount"),
"fieldname": "net_purchase_amount",
"label": _("Gross Purchase Amount"),
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"width": 250,
@@ -497,8 +495,8 @@ def get_columns(filters):
"width": 90,
},
{
"label": _("Net Purchase Amount"),
"fieldname": "net_purchase_amount",
"label": _("Gross Purchase Amount"),
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"width": 100,

View File

@@ -503,7 +503,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
onload() {
super.onload();
this.frm.set_query("supplier", function () {
return {
filters: {

View File

@@ -16,7 +16,6 @@
"order_confirmation_no",
"order_confirmation_date",
"get_items_from_open_material_requests",
"mps",
"column_break_7",
"transaction_date",
"schedule_date",
@@ -1316,13 +1315,6 @@
"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,
@@ -1330,7 +1322,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-08-28 11:00:56.635116",
"modified": "2025-07-31 17:19:40.816883",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -108,7 +108,6 @@ 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

View File

@@ -540,8 +540,12 @@ 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)
@@ -565,6 +569,7 @@ 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
@@ -712,7 +717,6 @@ 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"
@@ -906,16 +910,18 @@ 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"
@@ -929,6 +935,8 @@ 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 (

View File

@@ -109,6 +109,22 @@ 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 () {

View File

@@ -284,15 +284,15 @@ def get_columns(filters):
def get_message():
return f"""<span class="indicator">
{_("Valid Till")}:&nbsp;&nbsp;
return """<span class="indicator">
Valid till : &nbsp;&nbsp;
</span>
<span class="indicator orange">
{_("Expires in a week or less")}
Expires in a week or less
</span>
&nbsp;&nbsp;
<span class="indicator red">
{_("Expires today or already expired")}
Expires today / Already Expired
</span>"""

View File

@@ -228,11 +228,6 @@ 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
@@ -284,6 +279,11 @@ 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,7 +2570,6 @@ 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 = {
@@ -2583,17 +2582,6 @@ 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"),
@@ -3396,27 +3384,14 @@ def get_payment_term_details(
term = frappe.get_doc("Payment Term", term)
else:
term_details.payment_term = term.payment_term
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.description = term.description
term_details.invoice_portion = term.invoice_portion
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.base_outstanding = term_details.base_payment_amount
term_details.mode_of_payment = term.mode_of_payment
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
@@ -3435,11 +3410,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, cint(term.credit_days))
due_date = add_days(date, 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), cint(term.credit_days))
due_date = add_days(get_last_day(date), 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, cint(term.credit_months)))
due_date = get_last_day(add_months(date, term.credit_months))
return due_date
@@ -3447,11 +3422,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, cint(term.discount_validity))
discount_validity = add_days(date, 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), cint(term.discount_validity))
discount_validity = add_days(get_last_day(date), 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, cint(term.discount_validity)))
discount_validity = get_last_day(add_months(date, term.discount_validity))
return discount_validity

View File

@@ -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, get_item_defaults
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
@@ -307,60 +307,6 @@ 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
@@ -929,7 +875,7 @@ class BuyingController(SubcontractingController):
self.update_fixed_asset(field, delete_asset=True)
def validate_budget(self):
if not frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"):
if frappe.get_single_value("Accounts Settings", "use_new_budget_controller"):
from erpnext.controllers.budget_controller import BudgetValidation
val = BudgetValidation(doc=self)
@@ -1052,7 +998,7 @@ class BuyingController(SubcontractingController):
"purchase_date": self.posting_date,
"calculate_depreciation": 0,
"purchase_amount": purchase_amount,
"net_purchase_amount": purchase_amount,
"gross_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,
@@ -1225,33 +1171,3 @@ 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({})

View File

@@ -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"):

View File

@@ -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_combine_datetime, get_incoming_rate, get_valuation_method
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class StockOverReturnError(frappe.ValidationError):
@@ -202,11 +202,7 @@ 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)
if column != "stock_qty"
else args.get(column)
)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
else:
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
@@ -854,14 +850,13 @@ 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(doctype, field, _bundle_ids)
return get_serial_batches_based_on_bundle(field, _bundle_ids)
def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
def get_serial_batches_based_on_bundle(field, _bundle_ids):
available_dict = frappe._dict({})
batch_serial_nos = frappe.get_all(
"Serial and Batch Bundle",
@@ -873,7 +868,6 @@ def get_serial_batches_based_on_bundle(doctype, 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],
@@ -887,16 +881,6 @@ def get_serial_batches_based_on_bundle(doctype, 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
@@ -925,8 +909,6 @@ def get_serial_batches_based_on_bundle(doctype, 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:
@@ -940,14 +922,10 @@ 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 {}
if "name" in filters:
del filters["name"]
del filters["name"]
filters[field] = ("in", reference_ids)
@@ -990,29 +968,10 @@ 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"
@@ -1102,9 +1061,6 @@ 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")
@@ -1126,7 +1082,8 @@ 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_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
"voucher_type": parent_doc.doctype,
"voucher_no": parent_doc.name,
"voucher_detail_no": child_doc.name,

View File

@@ -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_combine_datetime, get_incoming_rate, get_valuation_method
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class SellingController(StockController):
@@ -519,15 +519,8 @@ 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")
and not item_details.has_serial_no
and not item_details.has_batch_no
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
):
# 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"))
@@ -1006,9 +999,6 @@ 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
@@ -1027,7 +1017,8 @@ 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_datetime": get_combine_datetime(parent.posting_date, parent.posting_time),
"posting_date": parent.posting_date,
"posting_time": parent.posting_time,
"qty": child.qty,
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
"company": parent.company,

View File

@@ -265,8 +265,6 @@ 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"):
@@ -288,63 +286,31 @@ class StatusUpdater(Document):
)
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
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,
}
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",
)
== "Subcontract"
)
args["item_code"] = "production_item" if is_from_pp else "item_code"
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",
# 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,
)
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["idx"] = child_item.idx
item = item[0]
item["idx"] = d.idx
item["target_ref_field"] = args["target_ref_field"].replace("_", " ")
# if not item[args['target_ref_field']]:
@@ -357,28 +323,6 @@ 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

View File

@@ -27,7 +27,6 @@ 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
@@ -267,7 +266,8 @@ class StockController(AccountsController):
):
bundle_details = {
"item_code": row.get("rm_item_code") or row.item_code,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
@@ -360,20 +360,10 @@ 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):
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):
if data := available_dict.get(row.get(field)):
data = filter_serial_batches(self, data, row)
bundle = make_serial_batch_bundle_for_return(data, row, self)
row.db_set(
@@ -389,14 +379,6 @@ 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",
@@ -431,12 +413,6 @@ 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

View File

@@ -12,9 +12,6 @@ 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
@@ -55,42 +52,9 @@ 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:
@@ -202,9 +166,6 @@ 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
@@ -576,7 +537,8 @@ class SubcontractingController(StockController):
"qty": qty,
"serial_nos": serial_nos,
"batches": batches,
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": "Subcontracting Receipt",
"do_not_submit": True,
"type_of_transaction": "Outward" if qty > 0 else "Inward",
@@ -654,67 +616,6 @@ 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":

View File

@@ -208,18 +208,12 @@ 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 = flt(
item.rate_with_margin - item.rate, item.precision("discount_amount")
)
item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0:
item.discount_amount = flt(
item.price_list_rate - item.rate, item.precision("discount_amount")
)
item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
item.discount_amount = flt(
item.price_list_rate - item.rate, item.precision("discount_amount")
)
item.discount_amount = item.price_list_rate - item.rate
item.net_rate = item.rate

Some files were not shown because too many files have changed in this diff Show More