From c06a6bfc099680ea8f1f69b7432927c4597a241f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Apr 2023 09:46:54 +0530 Subject: [PATCH] refactor: book exchange gain/loss through journal (cherry picked from commit 81cd7873d343d893a6e2a3b41107f17e03eedcd8) --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 19 +- .../payment_reconciliation.py | 10 +- .../doctype/sales_invoice/sales_invoice.py | 3 +- erpnext/accounts/utils.py | 34 +++- erpnext/controllers/accounts_controller.py | 170 ++++++++++++------ 6 files changed, 171 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 74a90fe2e8f..3c2fb1dd0ed 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d3f0043fe6a..f0c36cef47f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -101,6 +101,7 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger", "Repost Payment Ledger Items", ) + super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() @@ -783,10 +784,25 @@ class PaymentEntry(AccountsController): flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) else: + + # Use source/target exchange rate, so no difference amount is calculated. + # then update exchange gain/loss amount in refernece table + # if there is an amount, submit a JE for that + + exchange_rate = 1 + if self.payment_type == "Receive": + exchange_rate = self.source_exchange_rate + elif self.payment_type == "Pay": + exchange_rate = self.target_exchange_rate + base_allocated_amount += flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) + allocated_amount_in_pe_exchange_rate = flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + ) + d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate return base_allocated_amount def set_total_allocated_amount(self): @@ -977,6 +993,7 @@ class PaymentEntry(AccountsController): gl_entries = self.build_gl_map() gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) + self.make_exchange_gain_loss_journal() def add_party_gl_entries(self, gl_entries): if self.party_account: diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 05623c09932..ab03699785a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -347,11 +347,11 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount and row.reference_type not in [ - "Sales Invoice", - "Purchase Invoice", - ]: - self.make_difference_entry(payment_details) + # if payment_details.difference_amount and row.reference_type not in [ + # "Sales Invoice", + # "Purchase Invoice", + # ]: + # self.make_difference_entry(payment_details) if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c2804d19a34..9e9cd61e203 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1046,6 +1046,8 @@ class SalesInvoice(SellingController): merge_entries=False, from_repost=from_repost, ) + + self.make_exchange_gain_loss_journal() elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -1071,7 +1073,6 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) - self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5662e99c5e7..adb789e7493 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -612,9 +612,7 @@ def update_reference_in_payment_entry( "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate - if not d.exchange_gain_loss - else payment_entry.get_exchange_rate(), + "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation } @@ -657,11 +655,41 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal() if not do_not_save: payment_entry.save(ignore_permissions=True) +def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: + """ + Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={ + "reference_type": parent_doc.doctype, + "reference_name": parent_doc.name, + "docstatus": 1, + }, + fields=["parent"], + as_list=1, + ) + if journals: + exchange_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "name": ["in", [x[0] for x in journals]], + "voucher_type": "Exchange Gain Or Loss", + "docstatus": 1, + }, + as_list=1, + ) + for doc in exchange_journals: + frappe.get_doc("Journal Entry", doc[0]).cancel() + + def unlink_ref_doc_from_payment_entries(ref_doc): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9912dd47f8b..dcaaff08ade 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -5,7 +5,7 @@ import json import frappe -from frappe import _, bold, throw +from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -962,67 +962,119 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference - def make_exchange_gain_loss_gl_entries(self, gl_entries): - if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]: - for d in self.get("advances"): - if d.exchange_gain_loss: - is_purchase_invoice = self.get("doctype") == "Purchase Invoice" - party = self.supplier if is_purchase_invoice else self.customer - party_account = self.credit_to if is_purchase_invoice else self.debit_to - party_type = "Supplier" if is_purchase_invoice else "Customer" - - gain_loss_account = frappe.get_cached_value( - "Company", self.company, "exchange_gain_loss_account" - ) - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) - ) - account_currency = get_account_currency(gain_loss_account) - if account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - - # for purchase - dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if not is_purchase_invoice: - # just reverse for sales? - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - gl_entries.append( - self.get_gl_dict( - { - "account": gain_loss_account, - "account_currency": account_currency, - "against": party, - dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), - dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), - "project": self.project, - }, - item=d, + def make_exchange_gain_loss_journal(self) -> None: + """ + Make Exchange Gain/Loss journal for Invoices and Payments + """ + # Cancelling is existing exchange gain/loss journals is handled in on_cancel event + if self.docstatus == 1: + if self.get("doctype") == "Payment Entry": + gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] + booked = [] + if gain_loss_to_book: + vtypes = [x.reference_doctype for x in gain_loss_to_book] + vnames = [x.reference_name for x in gain_loss_to_book] + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + parents = ( + qb.from_(jea) + .select(jea.parent) + .where( + (jea.reference_type == "Payment Entry") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) ) + .run() ) - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + booked = [] + if parents: + booked = ( + qb.from_(je) + .inner_join(jea) + .on(je.name == jea.parent) + .select(jea.reference_type, jea.reference_name, jea.reference_detail_no) + .where( + (je.docstatus == 1) + & (je.name.isin(parents)) + & (je.voucher_type == "Exchange Gain or Loss") + ) + .run() + ) - gl_entries.append( - self.get_gl_dict( + for d in gain_loss_to_book: + if d.exchange_gain_loss and ( + (d.reference_doctype, d.reference_name, str(d.idx)) not in booked + ): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + if self.payment_type == "Receive": + party_account = self.paid_from + elif self.payment_type == "Pay": + party_account = self.paid_to + + party_account_currency = frappe.get_cached_value( + "Account", party_account, "account_currency" + ) + dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format( + self.get("company") + ) + ) + gain_loss_account_currency = get_account_currency(gain_loss_account) + if gain_loss_account_currency != self.company_currency: + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) + + journal_account = frappe._dict( { "account": party_account, - "party_type": party_type, - "party": party, - "against": gain_loss_account, - dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + "party_type": self.party_type, + "party": self.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": d.reference_doctype, + "reference_name": d.reference_name, + "reference_detail_no": d.idx, dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center, - "project": self.project, - }, - self.party_account_currency, - item=self, + dr_or_cr + "_in_account_currency": 0, + } ) - ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": self.doctype, + "reference_name": self.name, + "reference_detail_no": d.idx, + reverse_dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + reverse_dr_or_cr: abs(d.exchange_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + # frappe.throw("stopping...") def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( @@ -1111,9 +1163,15 @@ class AccountsController(TransactionBase): reconcile_against_document(lst) def on_cancel(self): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries + from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + ) + + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + # Cancel Exchange Gain/Loss Journal before unlinking + cancel_exchange_gain_loss_journal(self) - if self.doctype in ["Sales Invoice", "Purchase Invoice"]: if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self)