diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index acc21bfadeb..5bd31a91e44 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -47,6 +47,7 @@ "auto_reconciliation_job_trigger", "reconciliation_queue_size", "column_break_resa", + "exchange_gain_loss_posting_date", "invoicing_settings_tab", "accounts_transactions_settings_section", "over_billing_allowance", @@ -389,7 +390,7 @@ { "fieldname": "section_break_jpd0", "fieldtype": "Section Break", - "label": "Payment Reconciliations" + "label": "Payment Reconciliation Settings" }, { "default": "0", @@ -523,6 +524,14 @@ "fieldname": "ignore_is_opening_check_for_reporting", "fieldtype": "Check", "label": "Ignore Is Opening check for reporting" + }, + { + "default": "Payment", + "description": "Only applies for Normal Payments", + "fieldname": "exchange_gain_loss_posting_date", + "fieldtype": "Select", + "label": "Posting Date Inheritance for Exchange Gain / Loss", + "options": "Invoice\nPayment\nReconciliation Date" } ], "icon": "icon-cog", @@ -530,7 +539,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-18 21:24:19.840745", + "modified": "2025-01-23 13:15:44.077853", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 590422c6224..31249e22455 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -45,6 +45,7 @@ class AccountsSettings(Document): enable_fuzzy_matching: DF.Check enable_immutable_ledger: DF.Check enable_party_matching: DF.Check + exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] frozen_accounts_modifier: DF.Link | None general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index db4a4b0f268..72aa4905900 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -335,6 +335,7 @@ class PaymentReconciliation(Document): for payment in non_reconciled_payments: row = self.append("payments", {}) row.update(payment) + row.is_advance = payment.book_advance_payments_in_separate_party_account def get_invoice_entries(self): # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against @@ -424,6 +425,9 @@ class PaymentReconciliation(Document): def allocate_entries(self, args): self.validate_entries() + exc_gain_loss_posting_date = frappe.db.get_single_value( + "Accounts Settings", "exchange_gain_loss_posting_date", cache=True + ) invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments")) default_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" @@ -450,6 +454,11 @@ class PaymentReconciliation(Document): res.difference_account = default_exchange_gain_loss_account res.exchange_rate = inv.get("exchange_rate") res.update({"gain_loss_posting_date": pay.get("posting_date")}) + if not pay.get("is_advance"): + if exc_gain_loss_posting_date == "Invoice": + res.update({"gain_loss_posting_date": inv.get("invoice_date")}) + elif exc_gain_loss_posting_date == "Reconciliation Date": + res.update({"gain_loss_posting_date": nowdate()}) if pay.get("amount") == 0: entries.append(res) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json index 9c967217175..2fda39dcb29 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json @@ -20,6 +20,7 @@ "is_advance", "section_break_5", "difference_amount", + "gain_loss_posting_date", "column_break_7", "difference_account", "exchange_rate", @@ -153,11 +154,16 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Reconciled" + }, + { + "fieldname": "gain_loss_posting_date", + "fieldtype": "Date", + "label": "Difference Posting Date" } ], "istable": 1, "links": [], - "modified": "2024-03-27 13:10:18.933928", + "modified": "2025-01-23 16:09:01.058574", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation Log Allocations", diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py index da02e1a41e6..ca1785afdae 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py @@ -20,6 +20,7 @@ class ProcessPaymentReconciliationLogAllocations(Document): difference_account: DF.Link | None difference_amount: DF.Currency exchange_rate: DF.Float + gain_loss_posting_date: DF.Date | None invoice_number: DF.DynamicLink invoice_type: DF.Link is_advance: DF.Data | None diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bab56b8b1ce..d1eddef433f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2998,6 +2998,7 @@ def get_advance_payment_entries( (payment_ref.allocated_amount).as_("amount"), (payment_ref.name).as_("reference_row"), (payment_ref.reference_name).as_("against_order"), + (payment_entry.book_advance_payments_in_separate_party_account), ) q = q.where(payment_ref.reference_doctype == order_doctype) @@ -3042,6 +3043,7 @@ def get_common_query( (payment_entry.name).as_("reference_name"), payment_entry.posting_date, (payment_entry.remarks).as_("remarks"), + (payment_entry.book_advance_payments_in_separate_party_account), ) .where(payment_entry.payment_type == payment_type) .where(payment_entry.party_type == party_type) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 6bc11b72677..25ec1d8d1fd 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -9,6 +9,7 @@ from frappe import qb from frappe.query_builder.functions import Sum from frappe.tests import IntegrationTestCase from frappe.utils import add_days, getdate, nowdate +from frappe.utils.data import getdate as convert_to_date from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry @@ -870,6 +871,69 @@ class TestAccountsController(IntegrationTestCase): self.assertEqual(pi.items[0].rate, arms_length_price) self.assertEqual(pi.items[0].valuation_rate, 100) + @IntegrationTestCase.change_settings( + "Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"} + ) + def test_17_gain_loss_posting_date_for_normal_payment(self): + # Sales Invoice in Foreign Currency + rate = 80 + rate_in_account_currency = 1 + + adv_date = convert_to_date(add_days(nowdate(), -2)) + inv_date = convert_to_date(add_days(nowdate(), -1)) + + si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=rate_in_account_currency) + + # Test payments with different exchange rates + pe = self.create_payment_entry(posting_date=adv_date, amount=1, source_exc_rate=75.1).save().submit() + + pr = self.create_payment_reconciliation() + pr.from_invoice_date = add_days(nowdate(), -1) + pr.to_invoice_date = nowdate() + pr.from_payment_date = add_days(nowdate(), -2) + pr.to_payment_date = nowdate() + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Outstanding in both currencies should be '0' + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + self.assertEqual( + getdate(nowdate()), frappe.db.get_value("Journal Entry", exc_je_for_pe[0].parent, "posting_date") + ) + # Cancel Payment + pe.reload() + pe.cancel() + + # outstanding should be same as grand total + si.reload() + self.assertEqual(si.outstanding_amount, rate_in_account_currency) + self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6eaffff2f9f..098000b0b03 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -398,3 +398,4 @@ erpnext.patches.v15_0.update_asset_status_to_work_in_progress erpnext.patches.v15_0.rename_manufacturing_settings_field erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config +execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")