refactor: book exchange gain/loss through journal
This commit is contained in:
parent
5e7b05e566
commit
81cd7873d3
@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("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);
|
||||
|
@ -143,6 +143,7 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.make_advance_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
@ -808,10 +809,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):
|
||||
@ -1002,6 +1018,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:
|
||||
|
@ -363,11 +363,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)
|
||||
|
@ -1029,6 +1029,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)
|
||||
|
||||
@ -1054,7 +1056,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)
|
||||
|
@ -614,9 +614,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
|
||||
"account": d.account,
|
||||
}
|
||||
@ -655,11 +653,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)
|
||||
|
@ -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.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
@ -968,67 +968,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 update_against_document_in_jv(self):
|
||||
"""
|
||||
@ -1090,9 +1142,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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user