From 277c651a9f20dadc9559a3e6e58504833857049e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 2 Jul 2026 15:08:04 +0530 Subject: [PATCH 1/5] refactor: add payment ledger to ignore link (cherry picked from commit 6a4c5b60626ef78ab89d81a10ab2a8f460529c05) --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index f17d34eac47..468b0a42c06 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -90,7 +90,7 @@ class ExchangeRateRevaluation(Document): ) def on_cancel(self): - self.ignore_linked_doctypes = "GL Entry" + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] @frappe.whitelist() def check_journal_entry_condition(self): From 051757760fec66a1bf64b2e2ab321f9bdcb60346 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 2 Jul 2026 17:52:03 +0530 Subject: [PATCH 2/5] refactor: reversal capability on exchange rate revaluation (cherry picked from commit a0b14c0607e466be920edbfa8987ec1d9d051161) # Conflicts: # erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py --- .../exchange_rate_revaluation.js | 34 ++++++-- .../exchange_rate_revaluation.py | 81 ++++++++++++++++--- .../test_exchange_rate_revaluation.py | 4 +- .../journal_entry/journal_entry_list.js | 5 +- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index 5efd3239341..8c6ce039c1d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -22,17 +22,27 @@ frappe.ui.form.on("Exchange Rate Revaluation", { refresh: function (frm) { if (frm.doc.docstatus == 1) { frappe.call({ - method: "check_journal_entry_condition", + method: "check_journal_and_reversal", doc: frm.doc, callback: function (r) { if (r.message) { - frm.add_custom_button( - __("Journal Entries"), - function () { - return frm.events.make_jv(frm); - }, - __("Create") - ); + if (!r.message.journals_posted) { + frm.add_custom_button( + __("Journal Entries"), + function () { + return frm.events.make_jv(frm); + }, + __("Create") + ); + } else if (!r.message.reversals_posted) { + frm.add_custom_button( + __("Reversal Journal Entries"), + function () { + return frm.events.make_reverse_journal(frm); + }, + __("Create") + ); + } } }, }); @@ -100,6 +110,14 @@ frappe.ui.form.on("Exchange Rate Revaluation", { }, }); }, + make_reverse_journal: function (frm) { + frappe.call({ + method: "make_reverse_journal", + doc: frm.doc, + freeze: true, + freeze_message: __("Reversing Journals..."), + }); + }, }); frappe.ui.form.on("Exchange Rate Revaluation Account", { diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 468b0a42c06..3fa9d9a1983 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -7,8 +7,13 @@ from frappe import _, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder import Criterion, Order +<<<<<<< HEAD from frappe.query_builder.functions import NullIf, Sum from frappe.utils import flt, get_link_to_form +======= +from frappe.query_builder.functions import Max, NullIf, Sum +from frappe.utils import flt, get_link_to_form, nowdate +>>>>>>> a0b14c0607 (refactor: reversal capability on exchange rate revaluation) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on @@ -93,22 +98,28 @@ class ExchangeRateRevaluation(Document): self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] @frappe.whitelist() - def check_journal_entry_condition(self): + def check_journal_and_reversal(self): exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() + journals_posted = False + reversals_posted = False + + je = qb.DocType("Journal Entry") jea = qb.DocType("Journal Entry Account") journals = ( - qb.from_(jea) - .select(jea.parent) + qb.from_(je) + .join(jea) + .on(je.name == jea.parent) + .select(je.name) .distinct() .where( (jea.reference_type == "Exchange Rate Revaluation") & (jea.reference_name == self.name) & (jea.docstatus == 1) + & (je.reversal_of.isnull()) # omit journals that have reversals ) - .run() + .run(pluck="name") ) - if journals: gle = qb.DocType("GL Entry") total_amt = ( @@ -123,12 +134,31 @@ class ExchangeRateRevaluation(Document): .run() ) - if total_amt and total_amt[0][0] != self.total_gain_loss: - return True + if total_amt and total_amt[0][0] == self.total_gain_loss: + journals_posted = True else: - return False + journals_posted = False - return True + # reverse journals + reverse_journals = ( + qb.from_(je) + .join(jea) + .on(je.name == jea.parent) + .select(je.name) + .where( + (jea.reference_type == "Exchange Rate Revaluation") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) + & (je.reversal_of.notnull()) + ) + .run(pluck="name") + ) + if reverse_journals: + reversals_posted = True + else: + reversals_posted = False + + return {"journals_posted": journals_posted, "reversals_posted": reversals_posted} def fetch_and_calculate_accounts_data(self): accounts = self.get_accounts_data() @@ -342,6 +372,7 @@ class ExchangeRateRevaluation(Document): @frappe.whitelist() def make_jv_entries(self): + frappe.has_permission("Journal Entry", "write", throw=True) zero_balance_jv = self.make_jv_for_zero_balance() if zero_balance_jv: frappe.msgprint( @@ -568,6 +599,38 @@ class ExchangeRateRevaluation(Document): journal_entry.save() return journal_entry + @frappe.whitelist() + def make_reverse_journal(self): + frappe.has_permission("Journal Entry", "write", throw=True) + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + journals = ( + qb.from_(je) + .join(jea) + .on(je.name == jea.parent) + .select(je.name) + .distinct() + .where( + (jea.reference_type == "Exchange Rate Revaluation") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) + & (je.reversal_of.isnull()) # omit journals that have reversals + ) + .run(pluck="name") + ) + if journals: + from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry + + for x in journals: + reversal = make_reverse_journal_entry(x) + reversal.posting_date = nowdate() + reversal.submit() + frappe.msgprint( + _("Revaluation journal for {0} has been created: {1}").format( + frappe.bold(x), get_link_to_form("Journal Entry", reversal.name) + ) + ) + def calculate_exchange_rate_using_last_gle(company, account, party_type, party): """ diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 77c8d8ec845..5808aca6e37 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -132,7 +132,7 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): err = err.save().submit() # Create JV for ERR - self.assertTrue(err.check_journal_entry_condition()) + self.assertTrue(err.check_journal_and_reversal()) err_journals = err.make_jv_entries() je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) je = je.submit() @@ -221,7 +221,7 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): err = err.save().submit() # Create JV for ERR - self.assertTrue(err.check_journal_entry_condition()) + self.assertTrue(err.check_journal_and_reversal()) err_journals = err.make_jv_entries() je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) je = je.submit() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js index 6ea0df946f2..1738beb3630 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry_list.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry_list.js @@ -1,7 +1,10 @@ frappe.listview_settings["Journal Entry"] = { - add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"], + add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark", "reversal_of"], get_indicator: function (doc) { if (doc.docstatus === 1) { + if (doc.reversal_of && doc.voucher_type == "Exchange Rate Revaluation") { + return [__("Reversal Of Exchange Rate Revaluation"), "blue"]; + } return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`]; } }, From 63e51171822bb6ed73a658dfe9df9086d4c27580 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 9 Jul 2026 13:09:29 +0530 Subject: [PATCH 3/5] refactor: handle reverse ERR journals in AR / AP report (cherry picked from commit 68382420637e4492be2bb52c156cd1222b34fa80) --- .../report/accounts_receivable/accounts_receivable.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 408f0262694..d1132f8594f 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -265,10 +265,12 @@ class ReceivablePayableReport: # Build and use a separate row for Employee Advances. # This allows Payments or Journals made against Emp Advance to be processed. - if ( - not row - and ple.against_voucher_type == "Employee Advance" - and self.filters.handle_employee_advances + if not row and ( + (ple.against_voucher_type == "Employee Advance" and self.filters.handle_employee_advances) + or ( + ple.against_voucher_type == "Exchange Rate Revaluation" + and self.filters.for_revaluation_journals + ) ): _d = self.build_voucher_dict(ple) _d.voucher_type = ple.against_voucher_type From 4711a28dd0b00e244d6f57e536f6ee2c1b47d278 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 10 Jul 2026 10:55:39 +0530 Subject: [PATCH 4/5] refactor(test): for reverse journals as well (cherry picked from commit 65775e59a1bc5fcb114db57a278bd8cc86c071c5) # Conflicts: # erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py --- .../test_exchange_rate_revaluation.py | 151 +++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 5808aca6e37..1b8f2ba3bfa 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -132,7 +132,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): err = err.save().submit() # Create JV for ERR - self.assertTrue(err.check_journal_and_reversal()) + ret = err.check_journal_and_reversal() + self.assertFalse(ret.get("journals_posted")) err_journals = err.make_jv_entries() je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) je = je.submit() @@ -221,7 +222,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): err = err.save().submit() # Create JV for ERR - self.assertTrue(err.check_journal_and_reversal()) + ret = err.check_journal_and_reversal() + self.assertFalse(ret.get("journals_posted")) err_journals = err.make_jv_entries() je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) je = je.submit() @@ -298,3 +300,148 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): for key, _val in expected_data.items(): self.assertEqual(expected_data.get(key), account_details.get(key)) +<<<<<<< HEAD +======= + + @ERPNextTestSuite.change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_05_revaluation_journal_reversal(self): + """ + Test reversing of revaluation journals + """ + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = self.company + err.posting_date = today() + err.fetch_and_calculate_accounts_data() + self.assertEqual(len(err.accounts), 1) + err.save().submit() + + gain_loss_account = err.get_for_unrealized_gain_loss_account() + usd_account = err.accounts[0].account + old_balance = err.accounts[0].balance_in_base_currency + new_balance = err.accounts[0].new_balance_in_base_currency + total_gain_loss = err.total_gain_loss + + # Create JV for ERR + ret = err.check_journal_and_reversal() + self.assertFalse(ret.get("journals_posted")) + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Rate Revaluation") + self.assertEqual(len(je.accounts), 3) + expected = [ + (usd_account, new_balance, 0.0, 100.0, 0.0), + (usd_account, 0.0, old_balance, 0.0, 100.0), + (gain_loss_account, 0.0, total_gain_loss, 0.0, total_gain_loss), + ] + actual = [] + for acc in je.accounts: + actual.append( + ( + acc.account, + acc.debit, + acc.credit, + acc.debit_in_account_currency, + acc.credit_in_account_currency, + ) + ) + self.assertEqual(expected, actual) + + # Assert reversals are not posted + ret = err.check_journal_and_reversal() + self.assertTrue(ret.get("journals_posted")) + self.assertFalse(ret.get("reversals_posted")) + + err.make_reverse_journal() + ret = err.check_journal_and_reversal() + self.assertTrue(ret.get("journals_posted")) + self.assertTrue(ret.get("reversals_posted")) + + reverse_jv = frappe.db.get_all( + "Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name" + ) + self.assertIsNotNone(reverse_jv) + + +class TestExchangeRateRevaluationValidation(ERPNextTestSuite): + """Validation and gain/loss calculation paths, exercised on the document directly + so they don't need the multi-currency GL setup the integration tests above build.""" + + def setUp(self): + frappe.set_user("Administrator") + self.company = "_Test Company" + + def _revaluation_with_rows(self, rows, rounding_loss_allowance=0.05): + doc = frappe.new_doc("Exchange Rate Revaluation") + doc.company = self.company + doc.posting_date = today() + doc.rounding_loss_allowance = rounding_loss_allowance + for row in rows: + doc.append("accounts", row) + return doc + + def test_rounding_loss_allowance_must_be_between_0_and_1(self): + for bad in (-0.1, 1, 1.5): + doc = self._revaluation_with_rows([], rounding_loss_allowance=bad) + self.assertRaises(frappe.ValidationError, doc.validate) + # values inside [0, 1) are accepted, at the lower bound and mid-range + for good in (0.0, 0.5): + self._revaluation_with_rows([], rounding_loss_allowance=good).validate() + + def test_gain_loss_computed_and_split_by_zero_balance(self): + doc = self._revaluation_with_rows( + [ + # open (unbooked) row: base balance moved 1000 -> 1100, a 100 gain + {"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100}, + # already-settled (zero_balance) row carries a booked loss of 40 + {"zero_balance": 1, "gain_loss": -40}, + ] + ) + doc.validate() + + # gain_loss is derived only for open rows; the zero-balance row keeps its value + self.assertEqual(doc.accounts[0].gain_loss, 100) + self.assertEqual(doc.gain_loss_unbooked, 100) + self.assertEqual(doc.gain_loss_booked, -40) + self.assertEqual(doc.total_gain_loss, 60) + + def test_before_submit_drops_rows_without_gain_loss(self): + doc = self._revaluation_with_rows( + [ + {"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100}, + {"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}, + ] + ) + doc.validate() # second row nets to a 0 gain_loss + doc.remove_accounts_without_gain_loss() + self.assertEqual(len(doc.accounts), 1) + self.assertEqual(doc.accounts[0].gain_loss, 100) + + def test_before_submit_requires_at_least_one_gain_loss_row(self): + doc = self._revaluation_with_rows( + [{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}] + ) + doc.validate() + self.assertRaises(frappe.ValidationError, doc.remove_accounts_without_gain_loss) +>>>>>>> 65775e59a1 (refactor(test): for reverse journals as well) From 431ae6bc93275aca9950a26d699d4e1ab41cd7ab Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 10 Jul 2026 11:58:33 +0530 Subject: [PATCH 5/5] chore: resolve conflict --- .../exchange_rate_revaluation.py | 7 +- .../test_exchange_rate_revaluation.py | 145 ------------------ 2 files changed, 1 insertion(+), 151 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 3fa9d9a1983..b75d7c6fc50 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -7,13 +7,8 @@ from frappe import _, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder import Criterion, Order -<<<<<<< HEAD from frappe.query_builder.functions import NullIf, Sum -from frappe.utils import flt, get_link_to_form -======= -from frappe.query_builder.functions import Max, NullIf, Sum from frappe.utils import flt, get_link_to_form, nowdate ->>>>>>> a0b14c0607 (refactor: reversal capability on exchange rate revaluation) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on @@ -619,7 +614,7 @@ class ExchangeRateRevaluation(Document): .run(pluck="name") ) if journals: - from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry for x in journals: reversal = make_reverse_journal_entry(x) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 1b8f2ba3bfa..78453b68c4d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -300,148 +300,3 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin): for key, _val in expected_data.items(): self.assertEqual(expected_data.get(key), account_details.get(key)) -<<<<<<< HEAD -======= - - @ERPNextTestSuite.change_settings( - "Accounts Settings", - {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, - ) - def test_05_revaluation_journal_reversal(self): - """ - Test reversing of revaluation journals - """ - si = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debtors_usd, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - do_not_submit=1, - ) - si.currency = "USD" - si.conversion_rate = 80 - si.save().submit() - - err = frappe.new_doc("Exchange Rate Revaluation") - err.company = self.company - err.posting_date = today() - err.fetch_and_calculate_accounts_data() - self.assertEqual(len(err.accounts), 1) - err.save().submit() - - gain_loss_account = err.get_for_unrealized_gain_loss_account() - usd_account = err.accounts[0].account - old_balance = err.accounts[0].balance_in_base_currency - new_balance = err.accounts[0].new_balance_in_base_currency - total_gain_loss = err.total_gain_loss - - # Create JV for ERR - ret = err.check_journal_and_reversal() - self.assertFalse(ret.get("journals_posted")) - err_journals = err.make_jv_entries() - je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) - je = je.submit() - - je.reload() - self.assertEqual(je.voucher_type, "Exchange Rate Revaluation") - self.assertEqual(len(je.accounts), 3) - expected = [ - (usd_account, new_balance, 0.0, 100.0, 0.0), - (usd_account, 0.0, old_balance, 0.0, 100.0), - (gain_loss_account, 0.0, total_gain_loss, 0.0, total_gain_loss), - ] - actual = [] - for acc in je.accounts: - actual.append( - ( - acc.account, - acc.debit, - acc.credit, - acc.debit_in_account_currency, - acc.credit_in_account_currency, - ) - ) - self.assertEqual(expected, actual) - - # Assert reversals are not posted - ret = err.check_journal_and_reversal() - self.assertTrue(ret.get("journals_posted")) - self.assertFalse(ret.get("reversals_posted")) - - err.make_reverse_journal() - ret = err.check_journal_and_reversal() - self.assertTrue(ret.get("journals_posted")) - self.assertTrue(ret.get("reversals_posted")) - - reverse_jv = frappe.db.get_all( - "Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name" - ) - self.assertIsNotNone(reverse_jv) - - -class TestExchangeRateRevaluationValidation(ERPNextTestSuite): - """Validation and gain/loss calculation paths, exercised on the document directly - so they don't need the multi-currency GL setup the integration tests above build.""" - - def setUp(self): - frappe.set_user("Administrator") - self.company = "_Test Company" - - def _revaluation_with_rows(self, rows, rounding_loss_allowance=0.05): - doc = frappe.new_doc("Exchange Rate Revaluation") - doc.company = self.company - doc.posting_date = today() - doc.rounding_loss_allowance = rounding_loss_allowance - for row in rows: - doc.append("accounts", row) - return doc - - def test_rounding_loss_allowance_must_be_between_0_and_1(self): - for bad in (-0.1, 1, 1.5): - doc = self._revaluation_with_rows([], rounding_loss_allowance=bad) - self.assertRaises(frappe.ValidationError, doc.validate) - # values inside [0, 1) are accepted, at the lower bound and mid-range - for good in (0.0, 0.5): - self._revaluation_with_rows([], rounding_loss_allowance=good).validate() - - def test_gain_loss_computed_and_split_by_zero_balance(self): - doc = self._revaluation_with_rows( - [ - # open (unbooked) row: base balance moved 1000 -> 1100, a 100 gain - {"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100}, - # already-settled (zero_balance) row carries a booked loss of 40 - {"zero_balance": 1, "gain_loss": -40}, - ] - ) - doc.validate() - - # gain_loss is derived only for open rows; the zero-balance row keeps its value - self.assertEqual(doc.accounts[0].gain_loss, 100) - self.assertEqual(doc.gain_loss_unbooked, 100) - self.assertEqual(doc.gain_loss_booked, -40) - self.assertEqual(doc.total_gain_loss, 60) - - def test_before_submit_drops_rows_without_gain_loss(self): - doc = self._revaluation_with_rows( - [ - {"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100}, - {"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}, - ] - ) - doc.validate() # second row nets to a 0 gain_loss - doc.remove_accounts_without_gain_loss() - self.assertEqual(len(doc.accounts), 1) - self.assertEqual(doc.accounts[0].gain_loss, 100) - - def test_before_submit_requires_at_least_one_gain_loss_row(self): - doc = self._revaluation_with_rows( - [{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}] - ) - doc.validate() - self.assertRaises(frappe.ValidationError, doc.remove_accounts_without_gain_loss) ->>>>>>> 65775e59a1 (refactor(test): for reverse journals as well)