Merge branch 'frappe:develop' into add-employee-name-to-session-user

This commit is contained in:
El-Shafei H.
2025-10-08 09:49:56 +03:00
committed by GitHub
161 changed files with 116693 additions and 26357 deletions

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ class AccountsSettings(Document):
submit_journal_entries: DF.Check
unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check
use_new_budget_controller: DF.Check
use_legacy_budget_controller: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -409,7 +409,7 @@ def start_auto_reconcile(
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
["payment_entry", "journal_entry"],
["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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ class BankTransaction(Document):
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.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

View File

@@ -23,8 +23,8 @@ frappe.ui.form.on("Budget", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_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();
}
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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);
}
);
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},
)

View File

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

View File

@@ -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(

View File

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

View File

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

View File

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

View File

@@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_("Difference"), fieldname="diff")
self.setup_ageing_columns()
self.add_column(label="Total Amount Due", fieldname="total_due")
self.add_column(label=_("Total Amount Due"), fieldname="total_due")
if self.filters.show_future_payments:
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")

View File

@@ -103,7 +103,7 @@ def get_data(filters):
"depreciation_amount": d.debit,
"depreciation_date": d.posting_date,
"value_after_depreciation": (
flt(row.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,
},

View File

@@ -87,7 +87,7 @@ def get_asset_categories_for_grouped_by_category(filters):
SELECT a.asset_category,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
},
]

View File

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

View File

@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
},
],
formatter: function (value, row, column, data, default_formatter) {
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
if (heading_ratios.includes(value)) {
value = $(`<span>${value}</span>`);
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
value = $value.wrap("<p></p>").parent().html();
}
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
column.fieldtype = "Data";
}

View File

@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Liquidity Ratios"})
data.append({"ratio": _("Liquidity Ratios")})
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
for d in ratio_data:
row = {
@@ -165,13 +165,13 @@ def add_solvency_ratios(
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Solvency Ratios"})
data.append({"ratio": _("Solvency Ratios")})
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
net_profit_ratio = {"ratio": "Net Profit Ratio"}
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
@@ -195,7 +195,7 @@ def add_solvency_ratios(
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": "Turnover Ratios"})
data.append({"ratio": _("Turnover Ratios")})
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
)
ratio_data = [
["Fixed Asset Turnover Ratio", net_sales, total_asset],
["Debtor Turnover Ratio", net_sales, avg_debtors],
["Creditor Turnover Ratio", direct_expense, avg_creditors],
["Inventory Turnover Ratio", cogs, avg_stock],
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
[_("Inventory Turnover Ratio"), cogs, avg_stock],
]
for ratio in ratio_data:
row = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -340,7 +340,7 @@ frappe.ui.form.on("Asset", {
}
var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: "Date" })];
var asset_values = [frm.doc.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,

View File

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

View File

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

View File

@@ -589,8 +589,8 @@ def get_gl_entries_on_asset_regain(
asset.get_gl_dict(
{
"account": fixed_asset_account,
"debit_in_account_currency": asset.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"),
)

View File

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

View File

@@ -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(

View File

@@ -98,7 +98,7 @@ class TestAssetCapitalization(IntegrationTestCase):
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.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"

View File

@@ -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": []
}
}

View File

@@ -39,8 +39,8 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
finance_book: DF.Link | None
finance_book_id: DF.Int
frequency_of_depreciation: DF.Int
gross_purchase_amount: DF.Currency
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
net_purchase_amount: DF.Currency
notes: DF.SmallText | None
opening_accumulated_depreciation: DF.Currency
opening_number_of_booked_depreciations: DF.Int
@@ -149,7 +149,7 @@ class AssetDepreciationSchedule(DepreciationScheduleController):
self.opening_number_of_booked_depreciations = (
self.asset_doc.opening_number_of_booked_depreciations or 0
)
self.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

View File

@@ -82,19 +82,19 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
self.set_depreciation_amount_for_last_row(row_idx)
self.depreciation_amount = flt(
self.depreciation_amount, self.asset_doc.precision("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),
)
)

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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 () {

View File

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

View File

@@ -17,7 +17,7 @@ from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
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({})

View File

@@ -318,7 +318,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
if filters:
if filters.get("customer"):
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
)
if filters.get("company"):

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

63249
erpnext/locale/ta.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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