fix: Exchange gain and loss booking on multi-currency invoice reconciliation (#32900)
* fix: Exchange gain and loss booking on multi-curreny invoice reconciliation * test: Update test cases * chore: Ignore SQL linting rule * chore: Joural Entry for exchange gainand loss booking * chore: Journal entry for exchange gain loss booking * test: Update test case * chore: Default exchange gain and loss account
This commit is contained in:
parent
6a4e25c1f9
commit
9a3d947e89
@ -589,15 +589,15 @@ class JournalEntry(AccountsController):
|
|||||||
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||||
else:
|
else:
|
||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
if flt(d.debit > 0):
|
if flt(d.debit) > 0:
|
||||||
accounts_debited.append(d.party or d.account)
|
accounts_debited.append(d.party or d.account)
|
||||||
if flt(d.credit) > 0:
|
if flt(d.credit) > 0:
|
||||||
accounts_credited.append(d.party or d.account)
|
accounts_credited.append(d.party or d.account)
|
||||||
|
|
||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
if flt(d.debit > 0):
|
if flt(d.debit) > 0:
|
||||||
d.against_account = ", ".join(list(set(accounts_credited)))
|
d.against_account = ", ".join(list(set(accounts_credited)))
|
||||||
if flt(d.credit > 0):
|
if flt(d.credit) > 0:
|
||||||
d.against_account = ", ".join(list(set(accounts_debited)))
|
d.against_account = ", ".join(list(set(accounts_debited)))
|
||||||
|
|
||||||
def validate_debit_credit_amount(self):
|
def validate_debit_credit_amount(self):
|
||||||
@ -759,7 +759,7 @@ class JournalEntry(AccountsController):
|
|||||||
pay_to_recd_from = d.party
|
pay_to_recd_from = d.party
|
||||||
|
|
||||||
if pay_to_recd_from and pay_to_recd_from == d.party:
|
if pay_to_recd_from and pay_to_recd_from == d.party:
|
||||||
party_amount += d.debit_in_account_currency or d.credit_in_account_currency
|
party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
|
||||||
party_account_currency = d.account_currency
|
party_account_currency = d.account_currency
|
||||||
|
|
||||||
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
|
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
|
||||||
@ -837,7 +837,7 @@ class JournalEntry(AccountsController):
|
|||||||
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_balance(self):
|
def get_balance(self, difference_account=None):
|
||||||
if not self.get("accounts"):
|
if not self.get("accounts"):
|
||||||
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
|
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
|
||||||
else:
|
else:
|
||||||
@ -852,7 +852,13 @@ class JournalEntry(AccountsController):
|
|||||||
blank_row = d
|
blank_row = d
|
||||||
|
|
||||||
if not blank_row:
|
if not blank_row:
|
||||||
blank_row = self.append("accounts", {})
|
blank_row = self.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": difference_account,
|
||||||
|
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
blank_row.exchange_rate = 1
|
blank_row.exchange_rate = 1
|
||||||
if diff > 0:
|
if diff > 0:
|
||||||
|
@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
reconcile() {
|
reconcile() {
|
||||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount);
|
||||||
|
|
||||||
if (show_dialog && show_dialog.length) {
|
if (show_dialog && show_dialog.length) {
|
||||||
|
|
||||||
@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
title: __("Select Difference Account"),
|
title: __("Select Difference Account"),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
|
fieldname: "allocation",
|
||||||
data: this.data, in_place_edit: true,
|
fieldtype: "Table",
|
||||||
|
label: __("Allocation"),
|
||||||
|
data: this.data,
|
||||||
|
in_place_edit: true,
|
||||||
|
cannot_add_rows: true,
|
||||||
get_data: () => {
|
get_data: () => {
|
||||||
return this.data;
|
return this.data;
|
||||||
},
|
},
|
||||||
@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
read_only: 1
|
read_only: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'HTML',
|
||||||
|
options: "<b> New Journal Entry will be posted for the difference amount </b>"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
primary_action: () => {
|
primary_action: () => {
|
||||||
const args = dialog.get_values()["allocation"];
|
const args = dialog.get_values()["allocation"];
|
||||||
@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.frm.doc.allocation.forEach(d => {
|
this.frm.doc.allocation.forEach(d => {
|
||||||
if (d.difference_amount && !d.difference_account) {
|
if (d.difference_amount) {
|
||||||
dialog.fields_dict.allocation.df.data.push({
|
dialog.fields_dict.allocation.df.data.push({
|
||||||
'docname': d.name,
|
'docname': d.name,
|
||||||
'reference_name': d.reference_name,
|
'reference_name': d.reference_name,
|
||||||
|
@ -14,7 +14,6 @@ from erpnext.accounts.utils import (
|
|||||||
QueryPaymentLedger,
|
QueryPaymentLedger,
|
||||||
get_outstanding_invoices,
|
get_outstanding_invoices,
|
||||||
reconcile_against_document,
|
reconcile_against_document,
|
||||||
update_reference_in_payment_entry,
|
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||||
|
|
||||||
@ -80,12 +79,13 @@ class PaymentReconciliation(Document):
|
|||||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
journal_entries = frappe.db.sql(
|
journal_entries = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||||
{dr_or_cr} as amount, t2.is_advance,
|
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||||
t2.account_currency as currency
|
t2.account_currency as currency
|
||||||
from
|
from
|
||||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||||
@ -215,26 +215,26 @@ class PaymentReconciliation(Document):
|
|||||||
inv.currency = entry.get("currency")
|
inv.currency = entry.get("currency")
|
||||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||||
|
|
||||||
def get_difference_amount(self, allocated_entry):
|
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||||
if allocated_entry.get("reference_type") != "Payment Entry":
|
difference_amount = 0
|
||||||
return
|
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||||
|
"exchange_rate", 1
|
||||||
|
):
|
||||||
|
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||||
|
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||||
|
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||||
|
|
||||||
dr_or_cr = (
|
return difference_amount
|
||||||
"credit_in_account_currency"
|
|
||||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
|
||||||
else "debit_in_account_currency"
|
|
||||||
)
|
|
||||||
|
|
||||||
row = self.get_payment_details(allocated_entry, dr_or_cr)
|
|
||||||
|
|
||||||
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
|
|
||||||
update_reference_in_payment_entry(row, doc, do_not_save=True)
|
|
||||||
|
|
||||||
return doc.difference_amount
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def allocate_entries(self, args):
|
def allocate_entries(self, args):
|
||||||
self.validate_entries()
|
self.validate_entries()
|
||||||
|
|
||||||
|
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"))
|
||||||
|
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||||
|
"Company", self.company, "exchange_gain_loss_account"
|
||||||
|
)
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for pay in args.get("payments"):
|
for pay in args.get("payments"):
|
||||||
pay.update({"unreconciled_amount": pay.get("amount")})
|
pay.update({"unreconciled_amount": pay.get("amount")})
|
||||||
@ -248,7 +248,10 @@ class PaymentReconciliation(Document):
|
|||||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||||
pay["amount"] = 0
|
pay["amount"] = 0
|
||||||
|
|
||||||
res.difference_amount = self.get_difference_amount(res)
|
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
|
||||||
|
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
|
||||||
|
res.difference_account = default_exchange_gain_loss_account
|
||||||
|
res.exchange_rate = inv.get("exchange_rate")
|
||||||
|
|
||||||
if pay.get("amount") == 0:
|
if pay.get("amount") == 0:
|
||||||
entries.append(res)
|
entries.append(res)
|
||||||
@ -278,6 +281,7 @@ class PaymentReconciliation(Document):
|
|||||||
"amount": pay.get("amount"),
|
"amount": pay.get("amount"),
|
||||||
"allocated_amount": allocated_amount,
|
"allocated_amount": allocated_amount,
|
||||||
"difference_amount": pay.get("difference_amount"),
|
"difference_amount": pay.get("difference_amount"),
|
||||||
|
"currency": inv.get("currency"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -300,7 +304,11 @@ class PaymentReconciliation(Document):
|
|||||||
else:
|
else:
|
||||||
reconciled_entry = entry_list
|
reconciled_entry = entry_list
|
||||||
|
|
||||||
reconciled_entry.append(self.get_payment_details(row, dr_or_cr))
|
payment_details = self.get_payment_details(row, dr_or_cr)
|
||||||
|
reconciled_entry.append(payment_details)
|
||||||
|
|
||||||
|
if payment_details.difference_amount:
|
||||||
|
self.make_difference_entry(payment_details)
|
||||||
|
|
||||||
if entry_list:
|
if entry_list:
|
||||||
reconcile_against_document(entry_list)
|
reconcile_against_document(entry_list)
|
||||||
@ -311,6 +319,56 @@ class PaymentReconciliation(Document):
|
|||||||
msgprint(_("Successfully Reconciled"))
|
msgprint(_("Successfully Reconciled"))
|
||||||
self.get_unreconciled_entries()
|
self.get_unreconciled_entries()
|
||||||
|
|
||||||
|
def make_difference_entry(self, row):
|
||||||
|
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
|
||||||
|
|
||||||
|
party_account_currency = frappe.get_cached_value(
|
||||||
|
"Account", self.receivable_payable_account, "account_currency"
|
||||||
|
)
|
||||||
|
difference_account_currency = frappe.get_cached_value(
|
||||||
|
"Account", row.difference_account, "account_currency"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Account Currency has balance
|
||||||
|
dr_or_cr = "debit" if self.party_type == "Customer" else "debit"
|
||||||
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
|
journal_account = frappe._dict(
|
||||||
|
{
|
||||||
|
"account": self.receivable_payable_account,
|
||||||
|
"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": row.against_voucher_type,
|
||||||
|
"reference_name": row.against_voucher,
|
||||||
|
dr_or_cr: flt(row.difference_amount),
|
||||||
|
dr_or_cr + "_in_account_currency": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
journal_entry.append("accounts", journal_account)
|
||||||
|
|
||||||
|
journal_account = frappe._dict(
|
||||||
|
{
|
||||||
|
"account": row.difference_account,
|
||||||
|
"account_currency": difference_account_currency,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||||
|
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
journal_entry.append("accounts", journal_account)
|
||||||
|
|
||||||
|
journal_entry.save()
|
||||||
|
journal_entry.submit()
|
||||||
|
|
||||||
def get_payment_details(self, row, dr_or_cr):
|
def get_payment_details(self, row, dr_or_cr):
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
{
|
{
|
||||||
@ -320,6 +378,7 @@ class PaymentReconciliation(Document):
|
|||||||
"against_voucher_type": row.get("invoice_type"),
|
"against_voucher_type": row.get("invoice_type"),
|
||||||
"against_voucher": row.get("invoice_number"),
|
"against_voucher": row.get("invoice_number"),
|
||||||
"account": self.receivable_payable_account,
|
"account": self.receivable_payable_account,
|
||||||
|
"exchange_rate": row.get("exchange_rate"),
|
||||||
"party_type": self.party_type,
|
"party_type": self.party_type,
|
||||||
"party": self.party,
|
"party": self.party,
|
||||||
"is_advance": row.get("is_advance"),
|
"is_advance": row.get("is_advance"),
|
||||||
@ -344,6 +403,41 @@ class PaymentReconciliation(Document):
|
|||||||
if not self.get("payments"):
|
if not self.get("payments"):
|
||||||
frappe.throw(_("No records found in the Payments table"))
|
frappe.throw(_("No records found in the Payments table"))
|
||||||
|
|
||||||
|
def get_invoice_exchange_map(self, invoices):
|
||||||
|
sales_invoices = [
|
||||||
|
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
|
||||||
|
]
|
||||||
|
purchase_invoices = [
|
||||||
|
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
|
||||||
|
]
|
||||||
|
invoice_exchange_map = frappe._dict()
|
||||||
|
|
||||||
|
if sales_invoices:
|
||||||
|
sales_invoice_map = frappe._dict(
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Sales Invoice",
|
||||||
|
filters={"name": ("in", sales_invoices)},
|
||||||
|
fields=["name", "conversion_rate"],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice_exchange_map.update(sales_invoice_map)
|
||||||
|
|
||||||
|
if purchase_invoices:
|
||||||
|
purchase_invoice_map = frappe._dict(
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Purchase Invoice",
|
||||||
|
filters={"name": ("in", purchase_invoices)},
|
||||||
|
fields=["name", "conversion_rate"],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice_exchange_map.update(purchase_invoice_map)
|
||||||
|
|
||||||
|
return invoice_exchange_map
|
||||||
|
|
||||||
def validate_allocation(self):
|
def validate_allocation(self):
|
||||||
unreconciled_invoices = frappe._dict()
|
unreconciled_invoices = frappe._dict()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, nowdate
|
from frappe.utils import add_days, flt, nowdate
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
from erpnext import get_default_cost_center
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
@ -75,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.item = item if isinstance(item, str) else item.item_code
|
self.item = item if isinstance(item, str) else item.item_code
|
||||||
|
|
||||||
def create_customer(self):
|
def create_customer(self):
|
||||||
if frappe.db.exists("Customer", "_Test PR Customer"):
|
self.customer = make_customer("_Test PR Customer")
|
||||||
self.customer = "_Test PR Customer"
|
self.customer2 = make_customer("_Test PR Customer 2")
|
||||||
else:
|
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
|
||||||
customer = frappe.new_doc("Customer")
|
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
|
||||||
customer.customer_name = "_Test PR Customer"
|
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
||||||
customer.type = "Individual"
|
|
||||||
customer.save()
|
|
||||||
self.customer = customer.name
|
|
||||||
|
|
||||||
if frappe.db.exists("Customer", "_Test PR Customer 2"):
|
|
||||||
self.customer2 = "_Test PR Customer 2"
|
|
||||||
else:
|
|
||||||
customer = frappe.new_doc("Customer")
|
|
||||||
customer.customer_name = "_Test PR Customer 2"
|
|
||||||
customer.type = "Individual"
|
|
||||||
customer.save()
|
|
||||||
self.customer2 = customer.name
|
|
||||||
|
|
||||||
if frappe.db.exists("Customer", "_Test PR Customer 3"):
|
|
||||||
self.customer3 = "_Test PR Customer 3"
|
|
||||||
else:
|
|
||||||
customer = frappe.new_doc("Customer")
|
|
||||||
customer.customer_name = "_Test PR Customer 3"
|
|
||||||
customer.type = "Individual"
|
|
||||||
customer.default_currency = "EUR"
|
|
||||||
customer.save()
|
|
||||||
self.customer3 = customer.name
|
|
||||||
|
|
||||||
def create_account(self):
|
def create_account(self):
|
||||||
account_name = "Debtors EUR"
|
account_name = "Debtors EUR"
|
||||||
@ -598,6 +576,156 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(pr.payments[0].amount, amount)
|
self.assertEqual(pr.payments[0].amount, amount)
|
||||||
self.assertEqual(pr.payments[0].currency, "EUR")
|
self.assertEqual(pr.payments[0].currency, "EUR")
|
||||||
|
|
||||||
|
def test_difference_amount_via_journal_entry(self):
|
||||||
|
# Make Sale Invoice
|
||||||
|
si = self.create_sales_invoice(
|
||||||
|
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
si.customer = self.customer4
|
||||||
|
si.currency = "EUR"
|
||||||
|
si.conversion_rate = 85
|
||||||
|
si.debit_to = self.debtors_eur
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# Make payment using Journal Entry
|
||||||
|
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||||
|
je1.multi_currency = 1
|
||||||
|
je1.accounts[0].exchange_rate = 1
|
||||||
|
je1.accounts[0].credit_in_account_currency = 0
|
||||||
|
je1.accounts[0].credit = 0
|
||||||
|
je1.accounts[0].debit_in_account_currency = 8000
|
||||||
|
je1.accounts[0].debit = 8000
|
||||||
|
je1.accounts[1].party_type = "Customer"
|
||||||
|
je1.accounts[1].party = self.customer4
|
||||||
|
je1.accounts[1].exchange_rate = 80
|
||||||
|
je1.accounts[1].credit_in_account_currency = 100
|
||||||
|
je1.accounts[1].credit = 8000
|
||||||
|
je1.accounts[1].debit_in_account_currency = 0
|
||||||
|
je1.accounts[1].debit = 0
|
||||||
|
je1.save()
|
||||||
|
je1.submit()
|
||||||
|
|
||||||
|
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||||
|
je2.multi_currency = 1
|
||||||
|
je2.accounts[0].exchange_rate = 1
|
||||||
|
je2.accounts[0].credit_in_account_currency = 0
|
||||||
|
je2.accounts[0].credit = 0
|
||||||
|
je2.accounts[0].debit_in_account_currency = 16000
|
||||||
|
je2.accounts[0].debit = 16000
|
||||||
|
je2.accounts[1].party_type = "Customer"
|
||||||
|
je2.accounts[1].party = self.customer4
|
||||||
|
je2.accounts[1].exchange_rate = 80
|
||||||
|
je2.accounts[1].credit_in_account_currency = 200
|
||||||
|
je1.accounts[1].credit = 16000
|
||||||
|
je1.accounts[1].debit_in_account_currency = 0
|
||||||
|
je1.accounts[1].debit = 0
|
||||||
|
je2.save()
|
||||||
|
je2.submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.party = self.customer4
|
||||||
|
pr.receivable_payable_account = self.debtors_eur
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
# Test exact payment allocation
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[0].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
|
# Test partial payment allocation (with excess payment entry)
|
||||||
|
pr.set("allocation", [])
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[1].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
|
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||||
|
pr.reconcile()
|
||||||
|
total_debit_amount = frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||||
|
"sum(debit) as amount",
|
||||||
|
group_by="reference_name",
|
||||||
|
)[0].amount
|
||||||
|
|
||||||
|
self.assertEqual(flt(total_debit_amount, 2), -500)
|
||||||
|
|
||||||
|
def test_difference_amount_via_payment_entry(self):
|
||||||
|
# Make Sale Invoice
|
||||||
|
si = self.create_sales_invoice(
|
||||||
|
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
si.customer = self.customer5
|
||||||
|
si.currency = "EUR"
|
||||||
|
si.conversion_rate = 85
|
||||||
|
si.debit_to = self.debtors_eur
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# Make payment using Payment Entry
|
||||||
|
pe1 = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
payment_type="Receive",
|
||||||
|
party_type="Customer",
|
||||||
|
party=self.customer5,
|
||||||
|
paid_from=self.debtors_eur,
|
||||||
|
paid_to=self.bank,
|
||||||
|
paid_amount=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
pe1.source_exchange_rate = 80
|
||||||
|
pe1.received_amount = 8000
|
||||||
|
pe1.save()
|
||||||
|
pe1.submit()
|
||||||
|
|
||||||
|
pe2 = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
payment_type="Receive",
|
||||||
|
party_type="Customer",
|
||||||
|
party=self.customer5,
|
||||||
|
paid_from=self.debtors_eur,
|
||||||
|
paid_to=self.bank,
|
||||||
|
paid_amount=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
pe2.source_exchange_rate = 80
|
||||||
|
pe2.received_amount = 16000
|
||||||
|
pe2.save()
|
||||||
|
pe2.submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.party = self.customer5
|
||||||
|
pr.receivable_payable_account = self.debtors_eur
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[0].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
|
pr.set("allocation", [])
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[1].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
def test_differing_cost_center_on_invoice_and_payment(self):
|
def test_differing_cost_center_on_invoice_and_payment(self):
|
||||||
"""
|
"""
|
||||||
Cost Center filter should not affect outstanding amount calculation
|
Cost Center filter should not affect outstanding amount calculation
|
||||||
@ -618,3 +746,17 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# check PR tool output
|
# check PR tool output
|
||||||
self.assertEqual(len(pr.get("invoices")), 0)
|
self.assertEqual(len(pr.get("invoices")), 0)
|
||||||
self.assertEqual(len(pr.get("payments")), 0)
|
self.assertEqual(len(pr.get("payments")), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def make_customer(customer_name, currency=None):
|
||||||
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
customer = frappe.new_doc("Customer")
|
||||||
|
customer.customer_name = customer_name
|
||||||
|
customer.type = "Individual"
|
||||||
|
|
||||||
|
if currency:
|
||||||
|
customer.default_currency = currency
|
||||||
|
customer.save()
|
||||||
|
return customer.name
|
||||||
|
else:
|
||||||
|
return customer_name
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
"section_break_5",
|
"section_break_5",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"difference_account"
|
"difference_account",
|
||||||
|
"exchange_rate",
|
||||||
|
"currency"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -37,7 +39,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Allocated Amount",
|
"label": "Allocated Amount",
|
||||||
"options": "Currency",
|
"options": "currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -112,7 +114,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Unreconciled Amount",
|
"label": "Unreconciled Amount",
|
||||||
"options": "Currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -120,7 +122,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"options": "Currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -129,11 +131,24 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Reference Row",
|
"label": "Reference Row",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "exchange_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Exchange Rate",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-06 11:48:59.616562",
|
"modified": "2022-12-24 21:01:14.882747",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
@ -141,5 +156,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -11,7 +11,8 @@
|
|||||||
"col_break1",
|
"col_break1",
|
||||||
"amount",
|
"amount",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
"currency"
|
"currency",
|
||||||
|
"exchange_rate"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -62,11 +63,17 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "exchange_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Exchange Rate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-24 22:42:40.923179",
|
"modified": "2022-11-08 18:18:02.502149",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Invoice",
|
"name": "Payment Reconciliation Invoice",
|
||||||
@ -75,5 +82,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -15,7 +15,8 @@
|
|||||||
"difference_amount",
|
"difference_amount",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"remark",
|
"remark",
|
||||||
"currency"
|
"currency",
|
||||||
|
"exchange_rate"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -91,11 +92,17 @@
|
|||||||
"label": "Difference Amount",
|
"label": "Difference Amount",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "exchange_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Exchange Rate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-30 10:51:48.140062",
|
"modified": "2022-11-08 18:18:36.268760",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Payment",
|
"name": "Payment Reconciliation Payment",
|
||||||
@ -103,5 +110,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user