mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 20:48:27 +00:00
Merge branch 'frappe:develop' into add-employee-name-to-session-user
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -49,6 +50,13 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -62,10 +70,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -105,8 +114,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
@@ -116,6 +117,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"use_new_budget_controller"
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -598,12 +598,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_new_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use New Budget Controller"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -651,6 +645,12 @@
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -659,7 +659,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_new_budget_controller: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -409,7 +409,7 @@ def start_auto_reconcile(
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry"],
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -666,7 +666,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -854,11 +854,14 @@ def get_je_matching_query(
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -871,11 +874,11 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -889,6 +892,9 @@ def get_si_matching_query(exact_match, currency, common_filters):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -111,20 +111,54 @@ 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)
|
||||
|
||||
if not is_mt940_format(content):
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
transactions = mt940.parse(content)
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
@@ -249,6 +283,7 @@ 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()
|
||||
@@ -260,6 +295,7 @@ 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")
|
||||
@@ -276,6 +312,7 @@ 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]
|
||||
@@ -285,11 +322,12 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension == "xlsx" or "xls":
|
||||
elif extension in ("xlsx", "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:
|
||||
|
||||
@@ -1,10 +1,209 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
|
||||
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
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
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
@@ -239,7 +239,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:53:45.908169",
|
||||
"modified": "2025-09-26 17:06:29.207673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.Data | None
|
||||
reference_number: DF.SmallText | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
|
||||
@@ -23,8 +23,8 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
|
||||
if (!value) {
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
@@ -137,8 +137,8 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
# skipping validation for payroll entry creation in case party is not required
|
||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||
|
||||
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
@@ -256,7 +256,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -577,6 +578,14 @@
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -591,7 +600,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -193,8 +194,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -645,10 +646,10 @@ class JournalEntry(AccountsController):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
# skipping validation for payroll entry creation
|
||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party) and not skip_validation:
|
||||
if (
|
||||
not (d.party_type and d.party) and not self.party_not_required
|
||||
): # skipping validation if party_not_required is passed via payroll entry
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -1240,6 +1241,11 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
@@ -1267,6 +1273,7 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
frappe.flags.party_not_required = False
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(IntegrationTestCase):
|
||||
@@ -591,6 +592,15 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified": "2025-09-29 13:01:48.916517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -585,6 +585,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
}
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -607,6 +608,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.received_amount(frm);
|
||||
}
|
||||
}
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -129,7 +129,13 @@ class PaymentRequest(Document):
|
||||
|
||||
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
if (
|
||||
flt(
|
||||
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
|
||||
get_currency_precision(),
|
||||
)
|
||||
> ref_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"invoice_portion",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"section_break_6",
|
||||
"discount_type",
|
||||
"discount_date",
|
||||
"column_break_9",
|
||||
"discount",
|
||||
"discount_type",
|
||||
"column_break_9",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
@@ -172,12 +177,50 @@
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
|
||||
"fieldname": "credit_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Days",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
|
||||
"fieldname": "credit_months",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Months",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount",
|
||||
"fieldname": "discount_validity_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Discount Validity Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount_validity_based_on",
|
||||
"fieldname": "discount_validity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discount Validity",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-11 11:06:51.792982",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -189,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,27 @@ class PaymentSchedule(Document):
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
credit_days: DF.Int
|
||||
credit_months: DF.Int
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
discount_date: DF.Date | None
|
||||
discount_type: DF.Literal["Percentage", "Amount"]
|
||||
discount_validity: DF.Int
|
||||
discount_validity_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
discounted_amount: DF.Currency
|
||||
due_date: DF.Date
|
||||
due_date_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
invoice_portion: DF.Percent
|
||||
mode_of_payment: DF.Link | None
|
||||
outstanding: DF.Currency
|
||||
|
||||
@@ -162,4 +162,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,6 +884,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.make_write_off_gl_entry(gl_entries)
|
||||
self.make_gle_for_rounding_adjustment(gl_entries)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
self.set_gl_entry_for_purchase_expense(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def check_asset_cwip_enabled(self):
|
||||
@@ -1228,7 +1229,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if item.is_fixed_asset and item.landed_cost_voucher_amount:
|
||||
self.update_gross_purchase_amount_for_linked_assets(item)
|
||||
self.update_net_purchase_amount_for_linked_assets(item)
|
||||
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
@@ -1290,7 +1291,7 @@ class PurchaseInvoice(BuyingController):
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
def update_net_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={
|
||||
@@ -1306,7 +1307,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"Asset",
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2147,19 +2147,16 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -2185,7 +2182,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
|
||||
@@ -483,18 +483,23 @@ class Subscription(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]:
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
|
||||
"""
|
||||
Returns the `Item`s linked to `Subscription Plan`
|
||||
"""
|
||||
if prorate is None:
|
||||
prorate = False
|
||||
|
||||
prorate_factor = 1
|
||||
if prorate:
|
||||
prorate_factor = get_prorata_factor(
|
||||
self.current_invoice_end,
|
||||
self.current_invoice_start,
|
||||
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
|
||||
cint(
|
||||
self.generate_invoice_at
|
||||
in [
|
||||
"Beginning of the current subscription period",
|
||||
"Days before the current subscription period",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
items = []
|
||||
@@ -511,33 +516,19 @@ class Subscription(Document):
|
||||
|
||||
deferred = frappe.db.get_value("Item", item_code, deferred_field)
|
||||
|
||||
if not prorate:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
else:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
|
||||
if deferred:
|
||||
item.update(
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
add_years,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
@@ -555,6 +556,33 @@ class TestSubscription(IntegrationTestCase):
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
def test_invoice_generation_days_before_subscription_period_with_prorate(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
settings.prorate = 1
|
||||
settings.save()
|
||||
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 5",
|
||||
cost=1000,
|
||||
billing_interval="Year",
|
||||
billing_interval_count=1,
|
||||
currency="INR",
|
||||
)
|
||||
|
||||
start_date = add_days(nowdate(), 2)
|
||||
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
generate_invoice_at="Days before the current subscription period",
|
||||
generate_new_invoices_past_due_date=1,
|
||||
number_of_days=2,
|
||||
plans=[{"plan": "_Test Plan Name 5", "qty": 1}],
|
||||
)
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -35,7 +35,7 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if (
|
||||
frappe.get_single_value("Accounts Settings", "use_new_budget_controller")
|
||||
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
||||
and gl_map[0].voucher_type != "Period Closing Voucher"
|
||||
):
|
||||
bud_val = BudgetValidation(gl_map=gl_map)
|
||||
@@ -159,6 +159,7 @@ def validate_accounting_period(gl_map):
|
||||
WHERE
|
||||
ap.name = cd.parent
|
||||
AND ap.company = %(company)s
|
||||
AND ap.disabled = 0
|
||||
AND cd.closed = 1
|
||||
AND cd.document_type = %(voucher_type)s
|
||||
AND %(date)s between ap.start_date and ap.end_date
|
||||
|
||||
@@ -1272,7 +1272,7 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
ranges = [*self.ranges, "Above"]
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
|
||||
@@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_("Difference"), fieldname="diff")
|
||||
|
||||
self.setup_ageing_columns()
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
self.add_column(label=_("Total Amount Due"), fieldname="total_due")
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
|
||||
@@ -103,7 +103,7 @@ def get_data(filters):
|
||||
"depreciation_amount": d.debit,
|
||||
"depreciation_date": d.posting_date,
|
||||
"value_after_depreciation": (
|
||||
flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
flt(row.net_purchase_amount) - flt(row.accumulated_depreciation_amount)
|
||||
),
|
||||
"depreciation_entry": d.voucher_no,
|
||||
}
|
||||
@@ -119,7 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
"status",
|
||||
@@ -151,7 +151,7 @@ def get_columns():
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -95,7 +95,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -103,7 +103,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -114,7 +114,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -125,7 +125,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -357,7 +357,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
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.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -365,7 +365,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
0
|
||||
end), 0) as value_as_on_from_date,
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as value_of_new_purchase,
|
||||
@@ -373,7 +373,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -384,7 +384,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
@@ -395,7 +395,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Capitalized" then
|
||||
a.gross_purchase_amount
|
||||
a.net_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
@@ -5,30 +5,35 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{% include "accounts/report/financial_statements.html" %}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Consolidated Trial Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Company",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Company", txt);
|
||||
},
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
on_change: function (query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
{
|
||||
fieldname: "presentation_currency",
|
||||
label: __("Currency"),
|
||||
fieldtype: "Select",
|
||||
options: erpnext.get_presentation_currency_list(),
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_opening",
|
||||
label: __("With Period Closing Entry For Opening Balances"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "with_period_closing_entry_for_current_period",
|
||||
label: __("Period Closing Entry For Current Period"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_unclosed_fy_pl_balances",
|
||||
label: __("Show unclosed fiscal year's P&L balances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_group_accounts",
|
||||
label: __("Show Group Accounts"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
formatter: erpnext.financial_statements.formatter,
|
||||
tree: true,
|
||||
name_field: "account",
|
||||
parent_field: "parent_account",
|
||||
initial_depth: 3,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2025-09-03 00:53:22.230646",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-09-03 00:53:22.230646",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Consolidated Trial Balance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Consolidated Trial Balance",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, getdate, now_datetime, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.account import get_root_company
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
accumulate_values_into_parents,
|
||||
calculate_values,
|
||||
get_opening_balances,
|
||||
hide_group_accounts,
|
||||
prepare_opening_closing,
|
||||
value_fields,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
validate_filters as tb_validate_filters,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_rate_as_at
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
"""Return columns and data for the report.
|
||||
|
||||
This is the main entry point for the report. It accepts the filters as a
|
||||
dictionary and should return columns and data. It is called by the framework
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
validate_filters(filters=filters)
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_companies(filters)
|
||||
filters.show_net_values = True
|
||||
tb_validate_filters(filters)
|
||||
|
||||
|
||||
def validate_companies(filters):
|
||||
if not filters.company:
|
||||
return
|
||||
|
||||
root_company = get_root_company(filters.company[0])
|
||||
root_company = root_company[0] if root_company else filters.company[0]
|
||||
|
||||
lft, rgt = frappe.db.get_value("Company", root_company, fieldname=["lft", "rgt"])
|
||||
|
||||
company_subtree = frappe.db.get_all(
|
||||
"Company",
|
||||
{"lft": [">=", lft], "rgt": ["<=", rgt]},
|
||||
"name",
|
||||
order_by="lft",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for company in filters.company:
|
||||
if company not in company_subtree:
|
||||
frappe.throw(
|
||||
_("Consolidated Trial Balance can be generated for Companies having same root Company.")
|
||||
)
|
||||
|
||||
sort_companies(filters)
|
||||
|
||||
|
||||
def sort_companies(filters):
|
||||
companies = frappe.db.get_all(
|
||||
"Company", {"name": ["in", filters.company]}, "name", order_by="lft", pluck="name"
|
||||
)
|
||||
filters.company = companies
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
data = []
|
||||
if filters.company:
|
||||
reporting_currency, ignore_reporting_currency = get_reporting_currency(filters)
|
||||
else:
|
||||
return data
|
||||
|
||||
for company in filters.company:
|
||||
company_filter = frappe._dict(filters)
|
||||
company_filter.company = company
|
||||
|
||||
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
|
||||
consolidate_trial_balance_data(data, tb_data)
|
||||
|
||||
for d in data:
|
||||
prepare_opening_closing(d)
|
||||
|
||||
total_row = calculate_total_row(data, reporting_currency)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
update_to_presentation_currency(
|
||||
data,
|
||||
reporting_currency,
|
||||
filters.get("presentation_currency"),
|
||||
filters.get("to_date"),
|
||||
ignore_reporting_currency,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
default_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
opening_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
filters.get("from_date"),
|
||||
)
|
||||
current_date = (
|
||||
filters.get("to_date") if getdate(filters.get("to_date")) <= now_datetime().date() else nowdate()
|
||||
)
|
||||
closing_exchange_rate = get_exchange_rate(
|
||||
default_currency,
|
||||
reporting_currency,
|
||||
current_date,
|
||||
)
|
||||
|
||||
if not (opening_exchange_rate and closing_exchange_rate):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}.",
|
||||
).format(default_currency, reporting_currency, current_date)
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(
|
||||
filters,
|
||||
ignore_is_opening,
|
||||
exchange_rate=opening_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
root_lft=None,
|
||||
root_rgt=None,
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
|
||||
ignore_opening_entries=True,
|
||||
group_by_account=True,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
exchange_rate=closing_exchange_rate,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
# Prepare opening closing for group account
|
||||
if parent_children_map.get(d.account) and filters.get("show_net_values"):
|
||||
prepare_opening_closing(d)
|
||||
|
||||
has_value = False
|
||||
row = {
|
||||
"account": d.name,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": reporting_currency,
|
||||
"is_group_account": d.is_group,
|
||||
"acc_name": d.account_name,
|
||||
"acc_number": d.account_number,
|
||||
"account_name": (
|
||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||
),
|
||||
"root_type": d.root_type,
|
||||
"account_type": d.account_type,
|
||||
}
|
||||
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= get_zero_cutoff(reporting_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, reporting_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": reporting_currency,
|
||||
}
|
||||
|
||||
for d in data:
|
||||
if not d.get("parent_account"):
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
calculate_foreign_currency_translation_reserve(total_row, data)
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def calculate_foreign_currency_translation_reserve(total_row, data):
|
||||
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
|
||||
dr_cr_diff = total_row["debit"] - total_row["credit"]
|
||||
|
||||
idx = get_fctr_root_row_index(data)
|
||||
|
||||
fctr_row = {
|
||||
"account": _("Foreign Currency Translation Reserve"),
|
||||
"account_name": _("Foreign Currency Translation Reserve"),
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": abs(opening_dr_cr_diff) if opening_dr_cr_diff < 0 else 0.0,
|
||||
"opening_credit": abs(opening_dr_cr_diff) if opening_dr_cr_diff > 0 else 0.0,
|
||||
"debit": abs(dr_cr_diff) if dr_cr_diff < 0 else 0.0,
|
||||
"credit": abs(dr_cr_diff) if dr_cr_diff > 0 else 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"root_type": data[idx].get("root_type"),
|
||||
"account_type": "Equity",
|
||||
"parent_account": data[idx].get("account"),
|
||||
"indent": data[idx].get("indent") + 1,
|
||||
"has_value": True,
|
||||
"currency": total_row.get("currency"),
|
||||
}
|
||||
|
||||
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
||||
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
||||
|
||||
prepare_opening_closing(fctr_row)
|
||||
|
||||
data.insert(idx + 1, fctr_row)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += fctr_row[field]
|
||||
|
||||
|
||||
def get_fctr_root_row_index(data):
|
||||
"""
|
||||
Returns: index, root_type, parent_account
|
||||
"""
|
||||
liabilities_idx, equity_idx, tmp_idx = -1, -1, 0
|
||||
for d in data:
|
||||
if liabilities_idx == -1 and d.get("root_type") == "Liability":
|
||||
liabilities_idx = tmp_idx
|
||||
|
||||
if equity_idx == -1 and d.get("root_type") == "Equity":
|
||||
equity_idx = tmp_idx
|
||||
|
||||
tmp_idx += 1
|
||||
|
||||
if equity_idx == -1:
|
||||
return liabilities_idx
|
||||
|
||||
return equity_idx
|
||||
|
||||
|
||||
def consolidate_trial_balance_data(data, tb_data):
|
||||
if not data:
|
||||
data.extend(list(tb_data))
|
||||
return
|
||||
|
||||
for entry in tb_data:
|
||||
if entry:
|
||||
consolidate_gle_data(data, entry, tb_data)
|
||||
|
||||
|
||||
def get_reporting_currency(filters):
|
||||
reporting_currency = frappe.get_cached_value("Company", filters.company[0], "reporting_currency")
|
||||
default_currency = None
|
||||
for company in filters.company:
|
||||
company_default_currency = erpnext.get_company_currency(company)
|
||||
if not default_currency:
|
||||
default_currency = company_default_currency
|
||||
|
||||
if company_default_currency != default_currency:
|
||||
return (reporting_currency, False)
|
||||
|
||||
return (default_currency, True)
|
||||
|
||||
|
||||
def consolidate_gle_data(data, entry, tb_data):
|
||||
entry_gle_exists = False
|
||||
for gle in data:
|
||||
if gle and gle["account_name"] == entry["account_name"]:
|
||||
entry_gle_exists = True
|
||||
gle["closing_credit"] += entry["closing_credit"]
|
||||
gle["closing_debit"] += entry["closing_debit"]
|
||||
gle["credit"] += entry["credit"]
|
||||
gle["debit"] += entry["debit"]
|
||||
gle["opening_credit"] += entry["opening_credit"]
|
||||
gle["opening_debit"] += entry["opening_debit"]
|
||||
gle["has_value"] = 1
|
||||
|
||||
if not entry_gle_exists:
|
||||
entry_parent_account = next(
|
||||
(d for d in tb_data if d.get("account") == entry.get("parent_account")), None
|
||||
)
|
||||
parent_account_in_data = None
|
||||
if entry_parent_account:
|
||||
parent_account_in_data = next(
|
||||
(d for d in data if d and d.get("account_name") == entry_parent_account.get("account_name")),
|
||||
None,
|
||||
)
|
||||
if parent_account_in_data:
|
||||
entry["parent_account"] = parent_account_in_data.get("account")
|
||||
entry["indent"] = (parent_account_in_data.get("indent") or 0) + 1
|
||||
data.insert(data.index(parent_account_in_data) + 1, entry)
|
||||
else:
|
||||
entry["parent_account"] = None
|
||||
entry["indent"] = 0
|
||||
data.append(entry)
|
||||
|
||||
|
||||
def update_to_presentation_currency(data, from_currency, to_currency, date, ignore_reporting_currency):
|
||||
if from_currency == to_currency:
|
||||
return
|
||||
|
||||
exchange_rate = get_rate_as_at(date, from_currency, to_currency)
|
||||
|
||||
for d in data:
|
||||
if not ignore_reporting_currency:
|
||||
for field in value_fields:
|
||||
if d.get(field):
|
||||
d[field] = d[field] * flt(exchange_rate)
|
||||
d.update(currency=to_currency)
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Data",
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_name",
|
||||
"label": _("Account Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_number",
|
||||
"label": _("Account Number"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_debit",
|
||||
"label": _("Opening (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_credit",
|
||||
"label": _("Opening (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_debit",
|
||||
"label": _("Closing (Dr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_credit",
|
||||
"label": _("Closing (Cr)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.report.consolidated_trial_balance.consolidated_trial_balance import execute
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class ForeignCurrencyTranslationReserveNotFoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class TestConsolidatedTrialBalance(IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from erpnext.accounts.report.trial_balance.test_trial_balance import create_company
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
# Group Company
|
||||
create_company(company_name="Parent Group Company India", is_group=1)
|
||||
|
||||
create_company(company_name="Child Company India", parent_company="Parent Group Company India")
|
||||
|
||||
# Child Company with different currency
|
||||
create_company(
|
||||
company_name="Child Company US",
|
||||
country="United States",
|
||||
currency="USD",
|
||||
parent_company="Parent Group Company India",
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Parent Group Company India",
|
||||
acc1="Marketing Expenses - PGCI",
|
||||
acc2="Cash - PGCI",
|
||||
amount=100000,
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company India", acc1="Cash - CCI", acc2="Secured Loans - CCI", amount=50000
|
||||
)
|
||||
|
||||
create_journal_entry(
|
||||
company="Child Company US", acc1="Marketing Expenses - CCU", acc2="Cash - CCU", amount=1000
|
||||
)
|
||||
|
||||
cls.fiscal_year = get_fiscal_year(today(), company="Parent Group Company India")[0]
|
||||
|
||||
def test_single_company_report(self):
|
||||
filters = frappe._dict({"company": ["Parent Group Company India"], "fiscal_year": self.fiscal_year})
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertEqual(total_row["closing_credit"], 100000)
|
||||
|
||||
def test_child_company_report_with_same_default_currency_as_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company India"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
|
||||
def test_child_company_with_different_default_currency_from_parent_company(self):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": ["Parent Group Company India", "Child Company US"],
|
||||
"fiscal_year": self.fiscal_year,
|
||||
}
|
||||
)
|
||||
|
||||
report = execute(filters)
|
||||
total_row = report[1][-1]
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR")
|
||||
|
||||
fctr = [d for d in report[1] if d.get("account") == _("Foreign Currency Translation Reserve")]
|
||||
|
||||
if not fctr:
|
||||
raise ForeignCurrencyTranslationReserveNotFoundError
|
||||
|
||||
ccu_total_credit = 1000 * flt(exchange_rate)
|
||||
|
||||
self.assertEqual(total_row["closing_debit"], total_row["closing_credit"])
|
||||
self.assertNotEqual(total_row["closing_credit"], ccu_total_credit)
|
||||
|
||||
self.assertEqual(total_row["closing_credit"], flt(100000 + ccu_total_credit))
|
||||
|
||||
|
||||
def create_journal_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = args.posting_date or today()
|
||||
je.company = args.company
|
||||
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": args.acc1,
|
||||
"debit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": args.acc2,
|
||||
"credit_in_account_currency": args.amount if args.amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(args.amount) if args.amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
je.save()
|
||||
je.submit()
|
||||
@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
|
||||
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
|
||||
|
||||
if (heading_ratios.includes(value)) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
|
||||
column.fieldtype = "Data";
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
|
||||
|
||||
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Liquidity Ratios"})
|
||||
data.append({"ratio": _("Liquidity Ratios")})
|
||||
|
||||
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||||
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
|
||||
|
||||
for d in ratio_data:
|
||||
row = {
|
||||
@@ -165,13 +165,13 @@ def add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Solvency Ratios"})
|
||||
data.append({"ratio": _("Solvency Ratios")})
|
||||
|
||||
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||||
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||||
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||||
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
|
||||
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
|
||||
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
|
||||
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
|
||||
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
@@ -195,7 +195,7 @@ def add_solvency_ratios(
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Turnover Ratios"})
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||||
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||||
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||||
["Inventory Turnover Ratio", cogs, avg_stock],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
]
|
||||
for ratio in ratio_data:
|
||||
row = {
|
||||
|
||||
@@ -212,7 +212,7 @@ def get_data(
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out and total:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
@@ -325,18 +325,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
|
||||
def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
|
||||
def get_all_parents(account, parent_children_map):
|
||||
for parent, children in parent_children_map.items():
|
||||
for child in children:
|
||||
if child["name"] == account and parent:
|
||||
accounts_to_show.add(parent)
|
||||
get_all_parents(parent, parent_children_map)
|
||||
|
||||
data_with_value = []
|
||||
accounts_to_show = set()
|
||||
|
||||
for d in data:
|
||||
if show_zero_values or d.get("has_value"):
|
||||
accounts_to_show.add(d.get("account"))
|
||||
get_all_parents(d.get("account"), parent_children_map)
|
||||
|
||||
for d in data:
|
||||
if d.get("account") in accounts_to_show:
|
||||
data_with_value.append(d)
|
||||
else:
|
||||
# show group with zero balance, if there are balances against child
|
||||
children = [child.name for child in parent_children_map.get(d.get("account")) or []]
|
||||
if children:
|
||||
for row in data:
|
||||
if row.get("account") in children and row.get("has_value"):
|
||||
data_with_value.append(d)
|
||||
break
|
||||
|
||||
return data_with_value
|
||||
|
||||
@@ -437,6 +443,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
@@ -467,6 +474,7 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
last_period_closing_voucher[0].name,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
from_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
ignore_opening_entries = True
|
||||
@@ -482,9 +490,10 @@ def set_gl_entries_by_account(
|
||||
ignore_closing_entries,
|
||||
ignore_opening_entries=ignore_opening_entries,
|
||||
group_by_account=group_by_account,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gl_entries, get_currency(filters))
|
||||
|
||||
for entry in gl_entries:
|
||||
@@ -505,6 +514,7 @@ def get_accounting_entries(
|
||||
period_closing_voucher=None,
|
||||
ignore_opening_entries=False,
|
||||
group_by_account=False,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
gl_entry = frappe.qb.DocType(doctype)
|
||||
query = (
|
||||
@@ -524,6 +534,16 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
if not ignore_reporting_currency:
|
||||
query = query.select(
|
||||
gl_entry.debit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.debit_in_reporting_currency).as_("debit_in_reporting_currency"),
|
||||
gl_entry.credit_in_reporting_currency
|
||||
if not group_by_account
|
||||
else Sum(gl_entry.credit_in_reporting_currency).as_("credit_in_reporting_currency"),
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
|
||||
|
||||
if doctype == "GL Entry":
|
||||
|
||||
@@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name":
|
||||
del columns[4:6]
|
||||
else:
|
||||
del columns[5:7]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
@@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
"width": 120,
|
||||
},
|
||||
"posting_time": {
|
||||
"label": _("Posting Time"),
|
||||
@@ -677,7 +682,9 @@ class GrossProfitGenerator:
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
@@ -395,7 +395,6 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
@@ -642,3 +641,42 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 0.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
@@ -5,31 +5,36 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -75,6 +75,8 @@ def create_company(**args):
|
||||
"company_name": args.company_name or "Trial Balance Company",
|
||||
"country": args.country or "India",
|
||||
"default_currency": args.currency or "INR",
|
||||
"parent_company": args.get("parent_company"),
|
||||
"is_group": args.get("is_group"),
|
||||
}
|
||||
)
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -135,15 +135,21 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters, ignore_is_opening):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
|
||||
def get_opening_balances(filters, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(
|
||||
filters, "Balance Sheet", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
pl_opening = get_rootwise_opening_balances(
|
||||
filters, "Profit and Loss", ignore_is_opening, exchange_rate, ignore_reporting_currency
|
||||
)
|
||||
|
||||
balance_sheet_opening.update(pl_opening)
|
||||
return balance_sheet_opening
|
||||
|
||||
|
||||
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
def get_rootwise_opening_balances(
|
||||
filters, report_type, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True
|
||||
):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = ""
|
||||
@@ -168,6 +174,7 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
@@ -180,24 +187,41 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
accounting_dimensions,
|
||||
start_date=start_date,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
else:
|
||||
gle = get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
|
||||
"GL Entry",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
ignore_reporting_currency=ignore_reporting_currency,
|
||||
)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening.setdefault(
|
||||
d.account,
|
||||
{
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
},
|
||||
)
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
opening_dr_cr = {
|
||||
"account": d.account,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
}
|
||||
|
||||
opening.setdefault(d.account, opening_dr_cr)
|
||||
|
||||
if ignore_reporting_currency:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit)
|
||||
|
||||
else:
|
||||
if d.get("report_type") == "Balance Sheet" and not (
|
||||
d.get("root_type") == "Equity" or d.get("account_type") == "Equity"
|
||||
):
|
||||
opening[d.account]["opening_debit"] += flt(d.debit) * flt(exchange_rate)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit) * flt(exchange_rate)
|
||||
else:
|
||||
opening[d.account]["opening_debit"] += flt(d.debit_in_reporting_currency)
|
||||
opening[d.account]["opening_credit"] += flt(d.credit_in_reporting_currency)
|
||||
|
||||
return opening
|
||||
|
||||
@@ -210,6 +234,7 @@ 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")
|
||||
@@ -228,6 +253,12 @@ def get_opening_balance(
|
||||
.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
|
||||
@@ -315,13 +346,21 @@ def get_opening_balance(
|
||||
|
||||
gle = opening_balance.run(as_dict=1)
|
||||
|
||||
if filters and filters.get("presentation_currency"):
|
||||
if filters and filters.get("presentation_currency") and ignore_reporting_currency:
|
||||
convert_to_presentation_currency(gle, get_currency(filters))
|
||||
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
|
||||
def calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
show_net_values,
|
||||
ignore_is_opening=0,
|
||||
exchange_rate=None,
|
||||
ignore_reporting_currency=True,
|
||||
):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -340,8 +379,18 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
|
||||
for entry in gl_entries_by_account.get(d.name, []):
|
||||
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
if ignore_reporting_currency:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
else:
|
||||
if d.report_type == "Balance Sheet" and not (
|
||||
d.root_type == "Equity" or d.account_type == "Equity"
|
||||
):
|
||||
d["debit"] += flt(entry.debit) * flt(exchange_rate)
|
||||
d["credit"] += flt(entry.credit) * flt(exchange_rate)
|
||||
else:
|
||||
d["debit"] += flt(entry.debit_in_reporting_currency)
|
||||
d["credit"] += flt(entry.credit_in_reporting_currency)
|
||||
|
||||
d["closing_debit"] = d["opening_debit"] + d["debit"]
|
||||
d["closing_credit"] = d["opening_credit"] + d["credit"]
|
||||
|
||||
@@ -964,19 +964,28 @@ def update_accounting_ledgers_after_reference_removal(
|
||||
adv_ple.run()
|
||||
|
||||
|
||||
def remove_ref_from_advance_section(ref_doc: object = None):
|
||||
def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None):
|
||||
# TODO: this might need some testing
|
||||
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
ref_doc.set("advances", [])
|
||||
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
|
||||
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
|
||||
row_names = []
|
||||
for adv in ref_doc.get("advances") or []:
|
||||
if adv.get("reference_name", None) == payment_name:
|
||||
row_names.append(adv.name)
|
||||
|
||||
if not row_names:
|
||||
return
|
||||
|
||||
child_table = (
|
||||
"Sales Invoice Advance" if ref_doc.doctype == "Sales Invoice" else "Purchase Invoice Advance"
|
||||
)
|
||||
frappe.db.delete(child_table, {"name": ("in", row_names)})
|
||||
|
||||
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_from_advance_section(ref_doc)
|
||||
remove_ref_from_advance_section(ref_doc, payment_name)
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_jv(
|
||||
@@ -1043,7 +1052,6 @@ def remove_ref_doc_link_from_pe(
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
|
||||
var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: "Date" })];
|
||||
var asset_values = [frm.doc.gross_purchase_amount];
|
||||
var asset_values = [frm.doc.net_purchase_amount];
|
||||
|
||||
if (frm.doc.calculate_depreciation) {
|
||||
if (frm.doc.opening_accumulated_depreciation) {
|
||||
@@ -351,8 +351,8 @@ frappe.ui.form.on("Asset", {
|
||||
x_intervals.push(frappe.format(depreciation_date, { fieldtype: "Date" }));
|
||||
asset_values.push(
|
||||
flt(
|
||||
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("net_purchase_amount")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -371,8 +371,8 @@ frappe.ui.form.on("Asset", {
|
||||
$.each(asset_depr_schedule_doc.depreciation_schedule || [], function (i, v) {
|
||||
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: "Date" }));
|
||||
var asset_value = flt(
|
||||
frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - v.accumulated_depreciation_amount,
|
||||
precision("net_purchase_amount")
|
||||
);
|
||||
if (v.journal_entry) {
|
||||
asset_values.push(asset_value);
|
||||
@@ -392,8 +392,8 @@ frappe.ui.form.on("Asset", {
|
||||
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: "Date" }));
|
||||
asset_values.push(
|
||||
flt(
|
||||
frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount - frm.doc.opening_accumulated_depreciation,
|
||||
precision("net_purchase_amount")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -408,7 +408,7 @@ frappe.ui.form.on("Asset", {
|
||||
$.each(depr_entries || [], function (i, v) {
|
||||
x_intervals.push(frappe.format(v.posting_date, { fieldtype: "Date" }));
|
||||
let last_asset_value = asset_values[asset_values.length - 1];
|
||||
asset_values.push(flt(last_asset_value - v.value, precision("gross_purchase_amount")));
|
||||
asset_values.push(flt(last_asset_value - v.value, precision("net_purchase_amount")));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
item_code: function (frm) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.net_purchase_amount) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
@@ -447,7 +447,7 @@ frappe.ui.form.on("Asset", {
|
||||
args: {
|
||||
item_code: frm.doc.item_code,
|
||||
asset_category: frm.doc.asset_category,
|
||||
gross_purchase_amount: frm.doc.gross_purchase_amount,
|
||||
net_purchase_amount: frm.doc.net_purchase_amount,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
@@ -463,10 +463,10 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
is_composite_asset: function (frm) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
frm.set_value("gross_purchase_amount", 0);
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", 1);
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 1);
|
||||
} else {
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", 0);
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
}
|
||||
|
||||
frm.trigger("toggle_reference_doc");
|
||||
@@ -592,14 +592,14 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
calculate_depreciation: function (frm) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.net_purchase_amount) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
}
|
||||
},
|
||||
|
||||
gross_purchase_amount: function (frm) {
|
||||
net_purchase_amount: function (frm) {
|
||||
if (frm.doc.finance_books) {
|
||||
frm.doc.finance_books.forEach((d) => {
|
||||
frm.events.set_depreciation_rate(frm, d);
|
||||
@@ -650,8 +650,8 @@ frappe.ui.form.on("Asset", {
|
||||
let data = r.message;
|
||||
frm.set_value("company", data.company);
|
||||
frm.set_value("purchase_date", data.purchase_date);
|
||||
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("net_purchase_amount", data.net_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.net_purchase_amount);
|
||||
frm.set_value("asset_quantity", data.asset_quantity);
|
||||
frm.set_value("cost_center", data.cost_center);
|
||||
if (data.asset_location) {
|
||||
@@ -702,7 +702,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (expected_value_after_useful_life_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_salvage_value_percentage = flt(
|
||||
(row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount,
|
||||
(row.expected_value_after_useful_life * 100) / frm.doc.net_purchase_amount,
|
||||
precision("salvage_value_percentage", row)
|
||||
);
|
||||
frappe.model.set_value(
|
||||
@@ -715,8 +715,8 @@ frappe.ui.form.on("Asset", {
|
||||
} else if (salvage_value_percentage_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_expected_value_after_useful_life = flt(
|
||||
frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100),
|
||||
precision("gross_purchase_amount")
|
||||
frm.doc.net_purchase_amount * (row.salvage_value_percentage / 100),
|
||||
precision("net_purchase_amount")
|
||||
);
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"purchase_amount",
|
||||
"asset_quantity",
|
||||
"additional_asset_cost",
|
||||
@@ -226,13 +226,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
"fieldtype": "Date",
|
||||
@@ -244,7 +237,7 @@
|
||||
"fieldname": "calculate_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate Depreciation",
|
||||
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.gross_purchase_amount) || doc.is_composite_component"
|
||||
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -558,6 +551,13 @@
|
||||
"fieldname": "is_composite_component",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Composite Component"
|
||||
},
|
||||
{
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -601,7 +601,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-20 13:44:06.229177",
|
||||
"modified": "2025-05-23 00:53:54.249309",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -70,7 +70,6 @@ class Asset(AccountsController):
|
||||
disposal_date: DF.Date | None
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
frequency_of_depreciation: DF.Int
|
||||
gross_purchase_amount: DF.Currency
|
||||
image: DF.AttachImage | None
|
||||
insurance_end_date: DF.Date | None
|
||||
insurance_start_date: DF.Date | None
|
||||
@@ -86,6 +85,7 @@ class Asset(AccountsController):
|
||||
location: DF.Link
|
||||
maintenance_required: DF.Check
|
||||
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
|
||||
net_purchase_amount: DF.Currency
|
||||
next_depreciation_date: DF.Date | None
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
@@ -129,7 +129,7 @@ class Asset(AccountsController):
|
||||
self.set_missing_values()
|
||||
self.validate_gross_and_purchase_amount()
|
||||
self.validate_finance_books()
|
||||
self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
|
||||
self.total_asset_cost = self.net_purchase_amount + self.additional_asset_cost
|
||||
self.status = self.get_status()
|
||||
|
||||
def create_asset_depreciation_schedule(self):
|
||||
@@ -159,7 +159,7 @@ class Asset(AccountsController):
|
||||
return
|
||||
|
||||
self.value_after_depreciation = (
|
||||
flt(self.gross_purchase_amount)
|
||||
flt(self.net_purchase_amount)
|
||||
- flt(self.opening_accumulated_depreciation)
|
||||
+ flt(self.additional_asset_cost)
|
||||
)
|
||||
@@ -224,7 +224,7 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset or self.is_composite_asset:
|
||||
return
|
||||
|
||||
self.purchase_amount = self.gross_purchase_amount
|
||||
self.purchase_amount = self.net_purchase_amount
|
||||
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
purchase_doc = self.purchase_receipt or self.purchase_invoice
|
||||
|
||||
@@ -244,12 +244,12 @@ class Asset(AccountsController):
|
||||
|
||||
for item in purchase_doc.items:
|
||||
if self.asset_quantity > 1:
|
||||
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
if item.base_net_amount == self.net_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
elif item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
if item.base_net_rate == self.net_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
@@ -327,7 +327,7 @@ class Asset(AccountsController):
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if self.item_code and not self.get("finance_books"):
|
||||
finance_books = get_item_details(self.item_code, self.asset_category, self.gross_purchase_amount)
|
||||
finance_books = get_item_details(self.item_code, self.asset_category, self.net_purchase_amount)
|
||||
self.set("finance_books", finance_books)
|
||||
|
||||
if self.asset_owner == "Company" and not self.asset_owner_company:
|
||||
@@ -366,10 +366,8 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(
|
||||
self.gross_purchase_amount, self.precision("gross_purchase_amount")
|
||||
)
|
||||
if self.net_purchase_amount:
|
||||
self.net_purchase_amount = flt(self.net_purchase_amount, self.precision("net_purchase_amount"))
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
@@ -380,8 +378,8 @@ class Asset(AccountsController):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
|
||||
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
if not flt(self.net_purchase_amount) and not self.is_composite_asset:
|
||||
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
|
||||
if is_cwip_accounting_enabled(self.asset_category):
|
||||
if (
|
||||
@@ -440,13 +438,13 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
|
||||
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
|
||||
error_message = _(
|
||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
"Net Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
)
|
||||
error_message += "<br>"
|
||||
error_message += _("Please do not book expense of multiple assets against one single Asset.")
|
||||
frappe.throw(error_message, title=_("Invalid Gross Purchase Amount"))
|
||||
frappe.throw(error_message, title=_("Invalid Net Purchase Amount"))
|
||||
|
||||
def make_asset_movement(self):
|
||||
reference_doctype = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
@@ -486,11 +484,11 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_asset_finance_books(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
row.expected_value_after_useful_life, self.precision("net_purchase_amount")
|
||||
)
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.net_purchase_amount):
|
||||
frappe.throw(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
|
||||
_("Row {0}: Expected Value After Useful Life must be less than Net Purchase Amount").format(
|
||||
row.idx
|
||||
)
|
||||
)
|
||||
@@ -507,11 +505,11 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_opening_depreciation_values(self, row):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
row.expected_value_after_useful_life, self.precision("net_purchase_amount")
|
||||
)
|
||||
depreciable_amount = flt(
|
||||
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("gross_purchase_amount"),
|
||||
flt(self.net_purchase_amount) - flt(row.expected_value_after_useful_life),
|
||||
self.precision("net_purchase_amount"),
|
||||
)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
@@ -576,8 +574,8 @@ class Asset(AccountsController):
|
||||
|
||||
if accumulated_depreciation_after_full_schedule:
|
||||
asset_value_after_full_schedule = flt(
|
||||
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision("gross_purchase_amount"),
|
||||
flt(self.net_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
|
||||
self.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -631,7 +629,7 @@ class Asset(AccountsController):
|
||||
|
||||
self.db_set(
|
||||
"value_after_depreciation",
|
||||
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
(flt(self.net_purchase_amount) - flt(self.opening_accumulated_depreciation)),
|
||||
)
|
||||
|
||||
def set_status(self, status=None):
|
||||
@@ -668,7 +666,7 @@ class Asset(AccountsController):
|
||||
or self.is_fully_depreciated
|
||||
):
|
||||
status = "Fully Depreciated"
|
||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
@@ -676,16 +674,16 @@ class Asset(AccountsController):
|
||||
|
||||
def get_value_after_depreciation(self, finance_book=None):
|
||||
if not self.calculate_depreciation:
|
||||
return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
return flt(self.value_after_depreciation, self.precision("net_purchase_amount"))
|
||||
|
||||
if not finance_book:
|
||||
return flt(
|
||||
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
|
||||
self.get("finance_books")[0].value_after_depreciation, self.precision("net_purchase_amount")
|
||||
)
|
||||
|
||||
for row in self.get("finance_books"):
|
||||
if finance_book == row.finance_book:
|
||||
return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
|
||||
return flt(row.value_after_depreciation, self.precision("net_purchase_amount"))
|
||||
|
||||
def get_default_finance_book_idx(self):
|
||||
if not self.get("default_finance_book") and self.company:
|
||||
@@ -889,7 +887,7 @@ class Asset(AccountsController):
|
||||
if flt(args.get("value_after_depreciation")):
|
||||
current_asset_value = flt(args.get("value_after_depreciation"))
|
||||
else:
|
||||
current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
|
||||
current_asset_value = flt(self.net_purchase_amount) - flt(self.opening_accumulated_depreciation)
|
||||
|
||||
value = flt(args.get("expected_value_after_useful_life")) / current_asset_value
|
||||
|
||||
@@ -1058,7 +1056,7 @@ def transfer_asset(args):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
def get_item_details(item_code, asset_category, net_purchase_amount):
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
@@ -1071,7 +1069,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
"daily_prorata_based": d.daily_prorata_based,
|
||||
"shift_based": d.shift_based,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
"expected_value_after_useful_life": flt(net_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
"depreciation_start_date": d.depreciation_start_date or nowdate(),
|
||||
"rate_of_depreciation": d.rate_of_depreciation,
|
||||
@@ -1211,7 +1209,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"net_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
@@ -1266,10 +1264,10 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
|
||||
|
||||
|
||||
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
||||
asset_doc.gross_purchase_amount = existing_asset.gross_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.gross_purchase_amount
|
||||
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||
asset_doc.purchase_amount = existing_asset.net_purchase_amount
|
||||
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
||||
asset_doc.total_asset_cost = asset_doc.gross_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.opening_accumulated_depreciation = (
|
||||
existing_asset.opening_accumulated_depreciation * scaling_factor
|
||||
)
|
||||
|
||||
@@ -589,8 +589,8 @@ def get_gl_entries_on_asset_regain(
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"debit_in_account_currency": asset.net_purchase_amount,
|
||||
"debit": asset.net_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
@@ -642,8 +642,8 @@ def get_gl_entries_on_asset_disposal(
|
||||
asset.get_gl_dict(
|
||||
{
|
||||
"account": fixed_asset_account,
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"credit_in_account_currency": asset.net_purchase_amount,
|
||||
"credit": asset.net_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": date,
|
||||
},
|
||||
@@ -681,7 +681,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
accumulated_depr_amount = flt(asset.net_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
@@ -792,7 +792,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
||||
validate_disposal_date(asset_doc.available_for_use_date, getdate(disposal_date), "available for use")
|
||||
|
||||
if asset_doc.available_for_use_date == getdate(disposal_date):
|
||||
return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
|
||||
return flt(asset_doc.net_purchase_amount - asset_doc.opening_accumulated_depreciation)
|
||||
|
||||
if not asset_doc.calculate_depreciation:
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
@@ -813,8 +813,8 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
|
||||
].accumulated_depreciation_amount
|
||||
|
||||
return flt(
|
||||
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
flt(asset_doc.net_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,9 +64,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
self.assertEqual(asset.asset_category, "Computers")
|
||||
|
||||
def test_gross_purchase_amount_is_mandatory(self):
|
||||
def test_net_purchase_amount_is_mandatory(self):
|
||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.net_purchase_amount = 0
|
||||
|
||||
self.assertRaises(frappe.MandatoryError, asset.save)
|
||||
|
||||
@@ -213,8 +213,8 @@ class TestAsset(AssetSetup):
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
self.assertEqual(accumulated_depr_amount, 18000.0)
|
||||
|
||||
@@ -252,8 +252,8 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
second_asset_depr_schedule.depreciation_amount = 9006.17
|
||||
@@ -266,10 +266,10 @@ class TestAsset(AssetSetup):
|
||||
date,
|
||||
original_schedule_date=get_last_day(date),
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("net_purchase_amount"))
|
||||
self.assertEqual(
|
||||
accumulated_depr_amount,
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Scrapped")
|
||||
@@ -278,13 +278,13 @@ class TestAsset(AssetSetup):
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
)
|
||||
@@ -304,8 +304,8 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0
|
||||
|
||||
@@ -347,21 +347,21 @@ class TestAsset(AssetSetup):
|
||||
|
||||
asset.load_from_db()
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
asset.net_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("net_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount = flt(accumulated_depr_amount - 18000)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(accumulated_depr_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(accumulated_depr_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("net_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
@@ -385,7 +385,7 @@ class TestAsset(AssetSetup):
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
net_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
@@ -483,7 +483,7 @@ class TestAsset(AssetSetup):
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2024-03-31",
|
||||
opening_accumulated_depreciation=493.15,
|
||||
gross_purchase_amount=12000,
|
||||
net_purchase_amount=12000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
@@ -493,7 +493,7 @@ class TestAsset(AssetSetup):
|
||||
post_depreciation_entries(date="2024-03-31")
|
||||
|
||||
self.assertEqual(asset.asset_quantity, 10)
|
||||
self.assertEqual(asset.gross_purchase_amount, 12000)
|
||||
self.assertEqual(asset.net_purchase_amount, 12000)
|
||||
self.assertEqual(asset.opening_accumulated_depreciation, 493.15)
|
||||
|
||||
new_asset = split_asset(asset.name, 2)
|
||||
@@ -510,14 +510,14 @@ class TestAsset(AssetSetup):
|
||||
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
|
||||
|
||||
self.assertEqual(new_asset.asset_quantity, 2)
|
||||
self.assertEqual(new_asset.gross_purchase_amount, 2400)
|
||||
self.assertEqual(new_asset.net_purchase_amount, 2400)
|
||||
self.assertEqual(new_asset.opening_accumulated_depreciation, 98.63)
|
||||
self.assertEqual(new_asset.split_from, asset.name)
|
||||
self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 400)
|
||||
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 400)
|
||||
|
||||
self.assertEqual(asset.asset_quantity, 8)
|
||||
self.assertEqual(asset.gross_purchase_amount, 9600)
|
||||
self.assertEqual(asset.net_purchase_amount, 9600)
|
||||
self.assertEqual(asset.opening_accumulated_depreciation, 394.52)
|
||||
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 1600)
|
||||
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 1600)
|
||||
@@ -603,7 +603,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = (
|
||||
nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
|
||||
)
|
||||
self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
|
||||
self.assertEqual(asset_doc.net_purchase_amount, 5250.0)
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
@@ -732,7 +732,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=12000,
|
||||
net_purchase_amount=12000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
@@ -935,7 +935,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2022-02-15",
|
||||
purchase_date="2022-02-15",
|
||||
depreciation_method="Written Down Value",
|
||||
gross_purchase_amount=10000,
|
||||
net_purchase_amount=10000,
|
||||
expected_value_after_useful_life=5000,
|
||||
depreciation_start_date="2022-02-28",
|
||||
total_number_of_depreciations=5,
|
||||
@@ -1123,7 +1123,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertTrue(depr_schedule_doc.has_pro_rata)
|
||||
|
||||
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):
|
||||
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000)."""
|
||||
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > net_purchase_amount(100,000)."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1151,7 +1151,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_opening_accumulated_depreciation(self):
|
||||
"""Tests if an error is raised when opening_accumulated_depreciation > (gross_purchase_amount - expected_value_after_useful_life)."""
|
||||
"""Tests if an error is raised when opening_accumulated_depreciation > (net_purchase_amount - expected_value_after_useful_life)."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
@@ -1489,7 +1489,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft")
|
||||
)
|
||||
|
||||
asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt(
|
||||
asset_value_after_full_schedule = flt(asset.net_purchase_amount) - flt(
|
||||
accumulated_depreciation_after_full_schedule
|
||||
)
|
||||
|
||||
@@ -1739,7 +1739,7 @@ def create_asset(**args):
|
||||
"calculate_depreciation": args.calculate_depreciation or 0,
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"net_purchase_amount": args.net_purchase_amount or 100000,
|
||||
"purchase_amount": args.purchase_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
@@ -1771,7 +1771,7 @@ def create_asset(**args):
|
||||
)
|
||||
|
||||
if asset.is_composite_asset:
|
||||
asset.gross_purchase_amount = 0
|
||||
asset.net_purchase_amount = 0
|
||||
asset.purchase_amount = 0
|
||||
|
||||
if not args.do_not_save:
|
||||
|
||||
@@ -569,14 +569,14 @@ class AssetCapitalization(StockController):
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
if self.docstatus == 2:
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
|
||||
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
||||
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value)
|
||||
else:
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
|
||||
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
||||
|
||||
asset_doc.db_set("gross_purchase_amount", gross_purchase_amount)
|
||||
asset_doc.db_set("net_purchase_amount", net_purchase_amount)
|
||||
asset_doc.db_set("purchase_amount", purchase_amount)
|
||||
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -98,7 +98,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
@@ -273,7 +273,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.status, "Work In Progress")
|
||||
|
||||
@@ -333,7 +333,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
@@ -528,8 +528,8 @@ def create_depreciation_asset(**args):
|
||||
asset.purchase_date = args.purchase_date or "2020-01-01"
|
||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||
|
||||
asset.gross_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_amount = asset.gross_purchase_amount
|
||||
asset.net_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_amount = asset.net_purchase_amount
|
||||
|
||||
finance_book = asset.append("finance_books")
|
||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"naming_series",
|
||||
"company",
|
||||
"column_break_2",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"finance_book",
|
||||
@@ -163,15 +163,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Gross Purchase Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
@@ -210,12 +201,21 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Value After Depreciation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Net Purchase Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-02 17:54:20.635668",
|
||||
"modified": "2025-05-23 01:17:16.708004",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
@@ -252,7 +252,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
|
||||
finance_book: DF.Link | None
|
||||
finance_book_id: DF.Int
|
||||
frequency_of_depreciation: DF.Int
|
||||
gross_purchase_amount: DF.Currency
|
||||
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
|
||||
net_purchase_amount: DF.Currency
|
||||
notes: DF.SmallText | None
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
opening_number_of_booked_depreciations: DF.Int
|
||||
@@ -149,7 +149,7 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
|
||||
self.opening_number_of_booked_depreciations = (
|
||||
self.asset_doc.opening_number_of_booked_depreciations or 0
|
||||
)
|
||||
self.gross_purchase_amount = self.asset_doc.gross_purchase_amount
|
||||
self.net_purchase_amount = self.asset_doc.net_purchase_amount
|
||||
self.depreciation_method = self.fb_row.depreciation_method
|
||||
self.total_number_of_depreciations = self.fb_row.total_number_of_depreciations
|
||||
self.frequency_of_depreciation = self.fb_row.frequency_of_depreciation
|
||||
|
||||
@@ -82,19 +82,19 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
self.set_depreciation_amount_for_last_row(row_idx)
|
||||
|
||||
self.depreciation_amount = flt(
|
||||
self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
|
||||
self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")
|
||||
)
|
||||
if not self.depreciation_amount:
|
||||
break
|
||||
|
||||
self.pending_depreciation_amount = flt(
|
||||
self.pending_depreciation_amount - self.depreciation_amount,
|
||||
self.asset_doc.precision("gross_purchase_amount"),
|
||||
self.asset_doc.precision("net_purchase_amount"),
|
||||
)
|
||||
|
||||
self.adjust_depr_amount_for_salvage_value(row_idx)
|
||||
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")) > 0:
|
||||
self.add_depr_schedule_row(row_idx)
|
||||
|
||||
def initialize_variables(self):
|
||||
@@ -310,7 +310,7 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
)
|
||||
|
||||
self.depreciation_amount = flt(
|
||||
self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
|
||||
self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")
|
||||
)
|
||||
if self.depreciation_amount > 0:
|
||||
self.schedule_date = self.disposal_date
|
||||
@@ -380,13 +380,13 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
|
||||
|
||||
def validate_depreciation_amount_for_low_value_assets(self):
|
||||
"""
|
||||
If gross purchase amount is too low, then depreciation amount
|
||||
If net purchase amount is too low, then depreciation amount
|
||||
can come zero sometimes based on the frequency and number of depreciations.
|
||||
"""
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||
if flt(self.depreciation_amount, self.asset_doc.precision("net_purchase_amount")) <= 0:
|
||||
frappe.throw(
|
||||
_("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
|
||||
frappe.bold(self.asset_doc.gross_purchase_amount),
|
||||
_("Net Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
|
||||
frappe.bold(self.asset_doc.net_purchase_amount),
|
||||
frappe.bold(self.fb_row.total_number_of_depreciations),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
daily_prorata_based=1,
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
@@ -171,7 +171,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2024-12-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=3,
|
||||
gross_purchase_amount=731,
|
||||
net_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
@@ -199,7 +199,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
daily_prorata_based=1,
|
||||
gross_purchase_amount=1096,
|
||||
net_purchase_amount=1096,
|
||||
available_for_use_date="2020-01-15",
|
||||
depreciation_start_date="2020-01-31",
|
||||
frequency_of_depreciation=1,
|
||||
@@ -377,7 +377,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -457,7 +457,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair_for_6_months_frequency(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -522,7 +522,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_repair_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-15",
|
||||
@@ -601,7 +601,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_wdv_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
available_for_use_date="2023-04-01",
|
||||
@@ -662,7 +662,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -742,7 +742,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_value_adjustent(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=1000,
|
||||
net_purchase_amount=1000,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-01",
|
||||
@@ -844,7 +844,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_cancelling_asset_value_adjustent_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-01-15",
|
||||
@@ -918,7 +918,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_for_parallel_adjustment_and_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=600,
|
||||
net_purchase_amount=600,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2021-01-01",
|
||||
@@ -1007,7 +1007,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_sale_of_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=600,
|
||||
net_purchase_amount=600,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2021-01-01",
|
||||
@@ -1085,7 +1085,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
def test_depreciation_schedule_after_sale_of_asset_wdv_method(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
net_purchase_amount=500,
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
available_for_use_date="2021-01-01",
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestAssetShiftAllocation(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=120000,
|
||||
net_purchase_amount=120000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_data(filters):
|
||||
"purchase_receipt",
|
||||
"asset_category",
|
||||
"purchase_date",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"location",
|
||||
"available_for_use_date",
|
||||
"purchase_invoice",
|
||||
@@ -87,7 +87,7 @@ def get_data(filters):
|
||||
depreciation_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
revaluation_amount = revaluation_amount_map.get(asset.asset_id, 0.0)
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount
|
||||
asset.net_purchase_amount
|
||||
- asset.opening_accumulated_depreciation
|
||||
- depreciation_amount
|
||||
+ revaluation_amount
|
||||
@@ -101,7 +101,7 @@ def get_data(filters):
|
||||
"cost_center": asset.cost_center,
|
||||
"vendor_name": pr_supplier_map.get(asset.purchase_receipt)
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"net_purchase_amount": asset.net_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": depreciation_amount,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
@@ -268,6 +268,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
.where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account))
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.is_opening == "No")
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
)
|
||||
@@ -354,7 +355,7 @@ def get_group_by_data(
|
||||
fields = [
|
||||
group_by,
|
||||
"name",
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"calculate_depreciation",
|
||||
]
|
||||
@@ -369,7 +370,7 @@ def get_group_by_data(
|
||||
a["depreciated_amount"] = depreciation_amount_map.get(a["name"], 0.0)
|
||||
a["revaluation_amount"] = revaluation_amount_map.get(a["name"], 0.0)
|
||||
a["asset_value"] = (
|
||||
a["gross_purchase_amount"]
|
||||
a["net_purchase_amount"]
|
||||
- a["opening_accumulated_depreciation"]
|
||||
- a["depreciated_amount"]
|
||||
+ a["revaluation_amount"]
|
||||
@@ -383,7 +384,7 @@ def get_group_by_data(
|
||||
data.append(a)
|
||||
else:
|
||||
for field in (
|
||||
"gross_purchase_amount",
|
||||
"net_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"depreciated_amount",
|
||||
"asset_value",
|
||||
@@ -434,8 +435,8 @@ def get_columns(filters):
|
||||
"width": 216,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"label": _("Net Purchase Amount"),
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 250,
|
||||
@@ -495,8 +496,8 @@ def get_columns(filters):
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Gross Purchase Amount"),
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"label": _("Net Purchase Amount"),
|
||||
"fieldname": "net_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"width": 100,
|
||||
|
||||
@@ -540,12 +540,8 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_make_purchase_invoice_with_terms(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
|
||||
@@ -569,7 +565,6 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
|
||||
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
|
||||
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||
@@ -717,6 +712,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
)
|
||||
self.assertEqual(due_date, "2023-03-31")
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
|
||||
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
@@ -910,18 +906,16 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -935,8 +929,6 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
|
||||
@@ -109,22 +109,6 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Bank Account"),
|
||||
function () {
|
||||
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Pricing Rule"),
|
||||
function () {
|
||||
frm.trigger("make_pricing_rule");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
function () {
|
||||
|
||||
@@ -228,6 +228,11 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_date_with_fiscal_year()
|
||||
self.validate_party_accounts()
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
# validate inter company transaction rate
|
||||
@@ -279,11 +284,6 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
@@ -2570,6 +2570,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.payment_schedule = []
|
||||
self.payment_terms_template = po_or_so.payment_terms_template
|
||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
||||
|
||||
for schedule in po_or_so.payment_schedule:
|
||||
payment_schedule = {
|
||||
@@ -2582,6 +2583,17 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
if schedule.due_date_based_on:
|
||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||
|
||||
if schedule.discount_validity_based_on:
|
||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
@@ -3384,14 +3396,27 @@ def get_payment_term_details(
|
||||
term = frappe.get_doc("Payment Term", term)
|
||||
else:
|
||||
term_details.payment_term = term.payment_term
|
||||
term_details.description = term.description
|
||||
term_details.invoice_portion = term.invoice_portion
|
||||
|
||||
fields_to_copy = [
|
||||
"description",
|
||||
"invoice_portion",
|
||||
"discount_type",
|
||||
"discount",
|
||||
"mode_of_payment",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
]
|
||||
|
||||
for field in fields_to_copy:
|
||||
term_details[field] = term.get(field)
|
||||
|
||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||
term_details.discount_type = term.discount_type
|
||||
term_details.discount = term.discount
|
||||
term_details.outstanding = term_details.payment_amount
|
||||
term_details.mode_of_payment = term.mode_of_payment
|
||||
term_details.base_outstanding = term_details.base_payment_amount
|
||||
|
||||
if bill_date:
|
||||
term_details.due_date = get_due_date(term, bill_date)
|
||||
@@ -3410,11 +3435,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
|
||||
due_date = None
|
||||
date = bill_date or posting_date
|
||||
if term.due_date_based_on == "Day(s) after invoice date":
|
||||
due_date = add_days(date, term.credit_days)
|
||||
due_date = add_days(date, cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = add_days(get_last_day(date), term.credit_days)
|
||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||
due_date = get_last_day(add_months(date, term.credit_months))
|
||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
@@ -3422,11 +3447,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
|
||||
discount_validity = None
|
||||
date = bill_date or posting_date
|
||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||
discount_validity = add_days(date, term.discount_validity)
|
||||
discount_validity = add_days(date, cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||
discount_validity = add_days(get_last_day(date), term.discount_validity)
|
||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||
discount_validity = get_last_day(add_months(date, term.discount_validity))
|
||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||
return discount_validity
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.get_item_details import get_conversion_factor, get_item_defaults
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -307,6 +307,60 @@ class BuyingController(SubcontractingController):
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def set_gl_entry_for_purchase_expense(self, gl_entries):
|
||||
if self.doctype == "Purchase Invoice" and not self.update_stock:
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
details = get_purchase_expense_account(row.item_code, self.company)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details.purchase_expense_account = frappe.get_cached_value(
|
||||
"Company", self.company, "purchase_expense_account"
|
||||
)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
return
|
||||
|
||||
if not details.purchase_expense_contra_account:
|
||||
details.purchase_expense_contra_account = frappe.get_cached_value(
|
||||
"Company", self.company, "purchase_expense_contra_account"
|
||||
)
|
||||
|
||||
if not details.purchase_expense_contra_account:
|
||||
frappe.throw(
|
||||
_("Please set Purchase Expense Contra Account in Company {0}").format(self.company)
|
||||
)
|
||||
|
||||
amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount"))
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=details.purchase_expense_account,
|
||||
cost_center=row.cost_center,
|
||||
debit=amount,
|
||||
credit=0.0,
|
||||
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
|
||||
against_account=details.purchase_expense_contra_account,
|
||||
account_currency=frappe.get_cached_value(
|
||||
"Account", details.purchase_expense_account, "account_currency"
|
||||
),
|
||||
item=row,
|
||||
)
|
||||
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=details.purchase_expense_contra_account,
|
||||
cost_center=row.cost_center,
|
||||
debit=0.0,
|
||||
credit=amount,
|
||||
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
|
||||
against_account=details.purchase_expense_account,
|
||||
account_currency=frappe.get_cached_value(
|
||||
"Account", details.purchase_expense_contra_account, "account_currency"
|
||||
),
|
||||
item=row,
|
||||
)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
||||
@@ -875,7 +929,7 @@ class BuyingController(SubcontractingController):
|
||||
self.update_fixed_asset(field, delete_asset=True)
|
||||
|
||||
def validate_budget(self):
|
||||
if frappe.get_single_value("Accounts Settings", "use_new_budget_controller"):
|
||||
if not frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"):
|
||||
from erpnext.controllers.budget_controller import BudgetValidation
|
||||
|
||||
val = BudgetValidation(doc=self)
|
||||
@@ -998,7 +1052,7 @@ class BuyingController(SubcontractingController):
|
||||
"purchase_date": self.posting_date,
|
||||
"calculate_depreciation": 0,
|
||||
"purchase_amount": purchase_amount,
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"net_purchase_amount": purchase_amount,
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
@@ -1171,3 +1225,33 @@ def validate_item_type(doc, fieldname, message):
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_purchase_expense_account(item_code, company):
|
||||
defaults = get_item_defaults(item_code, company)
|
||||
|
||||
details = frappe._dict(
|
||||
{
|
||||
"purchase_expense_account": defaults.get("purchase_expense_account"),
|
||||
"purchase_expense_contra_account": defaults.get("purchase_expense_contra_account"),
|
||||
}
|
||||
)
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": defaults.item_group, "company": company},
|
||||
["purchase_expense_account", "purchase_expense_contra_account"],
|
||||
as_dict=1,
|
||||
) or frappe._dict({})
|
||||
|
||||
if not details.purchase_expense_account:
|
||||
details = frappe.db.get_value(
|
||||
"Item Default",
|
||||
{"parent": defaults.brand, "company": company},
|
||||
["purchase_expense_account", "purchase_expense_contra_account"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return details or frappe._dict({})
|
||||
|
||||
@@ -318,7 +318,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
|
||||
@@ -202,7 +202,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
current_stock_qty = args.get(column)
|
||||
elif args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
current_stock_qty = (
|
||||
args.get(column) * args.get("conversion_factor", 1.0)
|
||||
if column != "stock_qty"
|
||||
else args.get(column)
|
||||
)
|
||||
else:
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
|
||||
@@ -208,12 +208,18 @@ class calculate_taxes_and_totals:
|
||||
if item.discount_amount and not item.discount_percentage:
|
||||
item.rate = item.rate_with_margin - item.discount_amount
|
||||
else:
|
||||
item.discount_amount = item.rate_with_margin - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.rate_with_margin - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
|
||||
elif flt(item.price_list_rate) > 0:
|
||||
item.discount_amount = item.price_list_rate - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.price_list_rate - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
|
||||
item.discount_amount = item.price_list_rate - item.rate
|
||||
item.discount_amount = flt(
|
||||
item.price_list_rate - item.rate, item.precision("discount_amount")
|
||||
)
|
||||
|
||||
item.net_rate = item.rate
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"canonical_uri",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
@@ -71,10 +72,17 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "code_list.canonical_uri",
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Canonical URI"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified": "2025-10-04 17:22:28.176155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
@@ -94,10 +102,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class CommonCode(Document):
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
canonical_uri: DF.Data | None
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
|
||||
2800
erpnext/locale/ar.po
2800
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
2814
erpnext/locale/bs.po
2814
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/cs.po
2800
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/da.po
2800
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/de.po
2800
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2802
erpnext/locale/eo.po
2802
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/es.po
2800
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
2866
erpnext/locale/fa.po
2866
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2802
erpnext/locale/fr.po
2802
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
2860
erpnext/locale/hr.po
2860
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
2804
erpnext/locale/hu.po
2804
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/id.po
2800
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/it.po
2800
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3094
erpnext/locale/nb.po
3094
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/nl.po
2800
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/pl.po
2800
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/pt.po
2800
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/ru.po
2800
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
2874
erpnext/locale/sr.po
2874
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2832
erpnext/locale/sv.po
2832
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
63249
erpnext/locale/ta.po
Normal file
63249
erpnext/locale/ta.po
Normal file
File diff suppressed because it is too large
Load Diff
2802
erpnext/locale/th.po
2802
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
2802
erpnext/locale/tr.po
2802
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
2800
erpnext/locale/vi.po
2800
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
2802
erpnext/locale/zh.po
2802
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -255,6 +255,13 @@ class BOMCreator(Document):
|
||||
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
|
||||
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
|
||||
|
||||
key = (row.fg_item, row.fg_reference_id)
|
||||
if key not in production_item_wise_rm:
|
||||
production_item_wise_rm.setdefault(
|
||||
key,
|
||||
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
||||
)
|
||||
|
||||
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
||||
|
||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||
|
||||
@@ -224,6 +224,9 @@ class MasterProductionSchedule(Document):
|
||||
if ignore_orders:
|
||||
names = [name for name in names if name not in ignore_orders]
|
||||
|
||||
if not names:
|
||||
return []
|
||||
|
||||
query = query.where(doctype.parent.isin(names))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
@@ -775,6 +775,7 @@ class ProductionPlan(Document):
|
||||
work_order_data = {
|
||||
"wip_warehouse": default_warehouses.get("wip_warehouse"),
|
||||
"fg_warehouse": default_warehouses.get("fg_warehouse"),
|
||||
"scrap_warehouse": default_warehouses.get("scrap_warehouse"),
|
||||
"company": self.get("company"),
|
||||
}
|
||||
|
||||
@@ -1714,7 +1715,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
}
|
||||
)
|
||||
|
||||
sales_order = doc.get("sales_order")
|
||||
sales_order = data.get("sales_order")
|
||||
|
||||
for item_code, details in item_details.items():
|
||||
so_item_details.setdefault(sales_order, frappe._dict())
|
||||
@@ -1909,7 +1910,7 @@ def get_sub_assembly_items(
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
for field in ["wip_warehouse", "fg_warehouse"]:
|
||||
for field in ["wip_warehouse", "fg_warehouse", "scrap_warehouse"]:
|
||||
if not row.get(field):
|
||||
row[field] = default_warehouses.get(field)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase, timeout
|
||||
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
|
||||
@@ -3035,6 +3037,289 @@ class TestWorkOrder(IntegrationTestCase):
|
||||
self.assertEqual(wo_order.material_transferred_for_manufacturing, 3)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0)
|
||||
|
||||
def test_req_qty_clamping_in_manufacture_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
fg_item = "Test Unconsumed RM FG Item"
|
||||
rm_item_1 = "Test Unconsumed RM Item 1"
|
||||
rm_item_2 = "Test Unconsumed RM Item 2"
|
||||
|
||||
source_warehouse = "_Test Warehouse - _TC"
|
||||
wip_warehouse = "Stores - _TC"
|
||||
fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
make_item(rm_item_1, {"is_stock_item": 1})
|
||||
make_item(rm_item_2, {"is_stock_item": 1})
|
||||
|
||||
# create a BOM: 1 FG = 1 RM1 + 1 RM2
|
||||
bom = make_bom(
|
||||
item=fg_item,
|
||||
source_warehouse=source_warehouse,
|
||||
raw_materials=[rm_item_1, rm_item_2],
|
||||
operating_cost_per_bom_quantity=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
for row in bom.exploded_items:
|
||||
make_stock_entry_test_record(
|
||||
item_code=row.item_code,
|
||||
target=source_warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=50,
|
||||
source_warehouse=source_warehouse,
|
||||
wip_warehouse=wip_warehouse,
|
||||
)
|
||||
wo.submit()
|
||||
|
||||
# first partial transfer & manufacture (6 units)
|
||||
se_transfer_1 = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", 6, wip_warehouse)
|
||||
)
|
||||
se_transfer_1.insert()
|
||||
se_transfer_1.submit()
|
||||
|
||||
stock_entry_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 6, fg_warehouse))
|
||||
|
||||
# remove rm_2 from the items to simulate unconsumed RM scenario
|
||||
stock_entry_1.items = [row for row in stock_entry_1.items if row.item_code != rm_item_2]
|
||||
stock_entry_1.save()
|
||||
stock_entry_1.submit()
|
||||
|
||||
wo.reload()
|
||||
|
||||
se_transfer_2 = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", 20, wip_warehouse)
|
||||
)
|
||||
se_transfer_2.insert()
|
||||
se_transfer_2.submit()
|
||||
|
||||
stock_entry_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 20, fg_warehouse))
|
||||
|
||||
# validate rm_item_2 quantity is clamped correctly (per-unit BOM = 1 → max 20)
|
||||
for row in stock_entry_2.items:
|
||||
if row.item_code == rm_item_2:
|
||||
self.assertLessEqual(row.qty, 20)
|
||||
self.assertGreaterEqual(row.qty, 0)
|
||||
|
||||
def test_overproduction_allowed_qty(self):
|
||||
"""Test overproduction allowed qty in work order"""
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 50)
|
||||
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=10)
|
||||
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=100,
|
||||
basic_rate=1000.0,
|
||||
)
|
||||
|
||||
mt_stock_entry = frappe.get_doc(
|
||||
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 10)
|
||||
)
|
||||
mt_stock_entry.submit()
|
||||
|
||||
fg_stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||
fg_stock_entry.items[2].qty = 15
|
||||
fg_stock_entry.fg_completed_qty = 15
|
||||
fg_stock_entry.submit()
|
||||
|
||||
wo_order.reload()
|
||||
|
||||
self.assertEqual(wo_order.produced_qty, 15)
|
||||
self.assertEqual(wo_order.status, "Completed")
|
||||
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 0)
|
||||
|
||||
def test_reserved_serial_batch(self):
|
||||
raw_materials = []
|
||||
for item_code, properties in {
|
||||
"Test Reserved FG Item": {"is_stock_item": 1},
|
||||
"Test Reserved Serial Item": {"has_serial_no": 1, "serial_no_series": "TSNN-RSI-.####"},
|
||||
"Test Reserved Batch Item": {
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BCH-RBI-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
"Test Reserved Serial Batch Item": {
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TSNB-RSBI-.####",
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BCH-RSBI-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
}.items():
|
||||
make_item(item_code, properties=properties)
|
||||
if item_code != "Test Reserved FG Item":
|
||||
raw_materials.append(item_code)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="Stores - _TC",
|
||||
qty=5,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
original_auto_reserve = frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")
|
||||
original_backflush = frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "backflush_raw_materials_based_on"
|
||||
)
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1)
|
||||
|
||||
make_bom(
|
||||
item="Test Reserved FG Item",
|
||||
source_warehouse="Stores - _TC",
|
||||
raw_materials=raw_materials,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item="Test Reserved FG Item",
|
||||
qty=5,
|
||||
source_warehouse="Stores - _TC",
|
||||
reserve_stock=1,
|
||||
)
|
||||
|
||||
_reserved_item = get_reserved_entries(wo.name)
|
||||
for key, value in _reserved_item.items():
|
||||
self.assertEqual(key[1], "Stores - _TC")
|
||||
self.assertEqual(value.reserved_qty, 5)
|
||||
if value.serial_nos:
|
||||
self.assertEqual(len(value.serial_nos), 5)
|
||||
|
||||
if value.batch_nos:
|
||||
self.assertEqual(sum(value.batch_nos.values()), 5)
|
||||
|
||||
# Transfer 5 qty
|
||||
mt_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5))
|
||||
mt_stock_entry.submit()
|
||||
|
||||
for row in mt_stock_entry.items:
|
||||
value = _reserved_item[(row.item_code, row.s_warehouse)]
|
||||
self.assertEqual(row.qty, value.reserved_qty)
|
||||
if value.serial_nos:
|
||||
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
self.assertEqual(sorted(serial_nos), sorted(value.serial_nos))
|
||||
|
||||
if value.batch_nos:
|
||||
self.assertTrue(row.batch_no in value.batch_nos)
|
||||
|
||||
_before_reserved_item = get_reserved_entries(wo.name, mt_stock_entry.items[0].t_warehouse)
|
||||
|
||||
# Manufacture 2 qty
|
||||
fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 2))
|
||||
fg_stock_entry.submit()
|
||||
|
||||
for row in fg_stock_entry.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
value = _before_reserved_item[(row.item_code, row.s_warehouse)]
|
||||
if row.serial_no:
|
||||
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
for sn in serial_nos:
|
||||
self.assertTrue(sn in value.serial_nos)
|
||||
value.serial_nos.remove(sn)
|
||||
|
||||
if row.batch_no:
|
||||
self.assertTrue(row.batch_no in value.batch_nos)
|
||||
value.batch_nos[row.batch_no] -= row.qty
|
||||
if row.serial_no:
|
||||
sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
for sn in sns:
|
||||
self.assertTrue(sn in value.serial_batches[row.batch_no])
|
||||
value.serial_batches[row.batch_no].remove(sn)
|
||||
|
||||
# Manufacture 3 qty
|
||||
fg_stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
|
||||
fg_stock_entry.submit()
|
||||
|
||||
for row in fg_stock_entry.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
value = _before_reserved_item[(row.item_code, row.s_warehouse)]
|
||||
|
||||
if row.serial_no:
|
||||
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
self.assertEqual(sorted(serial_nos), sorted(value.serial_nos))
|
||||
|
||||
if row.batch_no:
|
||||
self.assertTrue(row.batch_no in value.batch_nos)
|
||||
self.assertEqual(value.batch_nos[row.batch_no], row.qty)
|
||||
if row.serial_no:
|
||||
sns = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
self.assertEqual(sorted(sns), sorted(value.serial_batches[row.batch_no]))
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", "backflush_raw_materials_based_on", original_backflush
|
||||
)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", original_auto_reserve)
|
||||
|
||||
|
||||
def get_reserved_entries(voucher_no, warehouse=None):
|
||||
doctype = frappe.qb.DocType("Stock Reservation Entry")
|
||||
sabb = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.left_join(sabb)
|
||||
.on(doctype.name == sabb.parent)
|
||||
.select(
|
||||
doctype.name,
|
||||
doctype.item_code,
|
||||
doctype.warehouse,
|
||||
doctype.reserved_qty,
|
||||
sabb.serial_no,
|
||||
sabb.batch_no,
|
||||
sabb.qty,
|
||||
sabb.delivered_qty,
|
||||
)
|
||||
.where((doctype.voucher_no == voucher_no) & (doctype.docstatus == 1))
|
||||
)
|
||||
|
||||
if warehouse:
|
||||
query = query.where(doctype.warehouse == warehouse)
|
||||
|
||||
reservation_entries = query.run(as_dict=True)
|
||||
|
||||
_reserved_item = frappe._dict({})
|
||||
for entry in reservation_entries:
|
||||
key = (entry.item_code, entry.warehouse)
|
||||
if key not in _reserved_item:
|
||||
_reserved_item[key] = frappe._dict(
|
||||
{
|
||||
"reserved_qty": 0,
|
||||
"serial_nos": [],
|
||||
"batch_nos": defaultdict(int),
|
||||
"serial_batches": defaultdict(list),
|
||||
}
|
||||
)
|
||||
|
||||
_reserved_item[key].reserved_qty += entry.qty
|
||||
if entry.batch_no:
|
||||
_reserved_item[key].batch_nos[entry.batch_no] += entry.qty
|
||||
if entry.serial_no:
|
||||
_reserved_item[key].serial_batches[entry.batch_no].append(entry.serial_no)
|
||||
if entry.serial_no:
|
||||
_reserved_item[key].serial_nos.append(entry.serial_no)
|
||||
|
||||
return _reserved_item
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user