mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
Merge pull request #40903 from frappe/mergify/bp/version-15-hotfix/pr-40876
fix: ledger entries for Payment against reverse payment reconciliation (backport #40876)
This commit is contained in:
@@ -1205,6 +1205,18 @@ class PaymentEntry(AccountsController):
|
|||||||
):
|
):
|
||||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||||
|
|
||||||
|
def get_dr_and_account_for_advances(self, reference):
|
||||||
|
if reference.reference_doctype == "Sales Invoice":
|
||||||
|
return "credit", reference.account
|
||||||
|
|
||||||
|
if reference.reference_doctype == "Payment Entry":
|
||||||
|
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||||
|
return "credit", self.party_account
|
||||||
|
else:
|
||||||
|
return "debit", self.party_account
|
||||||
|
|
||||||
|
return "debit", reference.account
|
||||||
|
|
||||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||||
args_dict = {
|
args_dict = {
|
||||||
"party_type": self.party_type,
|
"party_type": self.party_type,
|
||||||
@@ -1224,10 +1236,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if getdate(posting_date) < getdate(self.posting_date):
|
if getdate(posting_date) < getdate(self.posting_date):
|
||||||
posting_date = self.posting_date
|
posting_date = self.posting_date
|
||||||
|
|
||||||
dr_or_cr = (
|
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||||
"credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
|
args_dict["account"] = account
|
||||||
)
|
|
||||||
args_dict["account"] = invoice.account
|
|
||||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||||
args_dict.update(
|
args_dict.update(
|
||||||
@@ -2089,6 +2099,10 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
ref_doc.company
|
ref_doc.company
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Only applies for Reverse Payment Entries
|
||||||
|
account_type = None
|
||||||
|
payment_type = None
|
||||||
|
|
||||||
if reference_doctype == "Dunning":
|
if reference_doctype == "Dunning":
|
||||||
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
@@ -2103,6 +2117,18 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
||||||
|
|
||||||
|
elif reference_doctype == "Payment Entry":
|
||||||
|
if reverse_payment_details := frappe.db.get_all(
|
||||||
|
"Payment Entry",
|
||||||
|
filters={"name": reference_name},
|
||||||
|
fields=["payment_type", "party_type"],
|
||||||
|
)[0]:
|
||||||
|
payment_type = reverse_payment_details.payment_type
|
||||||
|
account_type = frappe.db.get_value(
|
||||||
|
"Party Type", reverse_payment_details.party_type, "account_type"
|
||||||
|
)
|
||||||
|
exchange_rate = 1
|
||||||
|
|
||||||
elif reference_doctype != "Journal Entry":
|
elif reference_doctype != "Journal Entry":
|
||||||
if not total_amount:
|
if not total_amount:
|
||||||
if party_account_currency == company_currency:
|
if party_account_currency == company_currency:
|
||||||
@@ -2147,6 +2173,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
"outstanding_amount": flt(outstanding_amount),
|
"outstanding_amount": flt(outstanding_amount),
|
||||||
"exchange_rate": flt(exchange_rate),
|
"exchange_rate": flt(exchange_rate),
|
||||||
"bill_no": ref_doc.get("bill_no"),
|
"bill_no": ref_doc.get("bill_no"),
|
||||||
|
"account_type": account_type,
|
||||||
|
"payment_type": payment_type,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -1087,6 +1087,8 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||||
expected_response = {
|
expected_response = {
|
||||||
"account": get_party_account("Customer", so.customer, so.company),
|
"account": get_party_account("Customer", so.customer, so.company),
|
||||||
|
"account_type": None, # only applies for Reverse Payment Entry
|
||||||
|
"payment_type": None, # only applies for Reverse Payment Entry
|
||||||
"total_amount": 5000.0,
|
"total_amount": 5000.0,
|
||||||
"outstanding_amount": 5000.0,
|
"outstanding_amount": 5000.0,
|
||||||
"exchange_rate": 1.0,
|
"exchange_rate": 1.0,
|
||||||
@@ -1559,7 +1561,7 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
company = "_Test Company"
|
company = "_Test Company"
|
||||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||||
advance_account = create_account(
|
advance_account = create_account(
|
||||||
parent_account="Current Assets - _TC",
|
parent_account="Current Liabilities - _TC",
|
||||||
account_name="Advances Received",
|
account_name="Advances Received",
|
||||||
company=company,
|
company=company,
|
||||||
account_type="Receivable",
|
account_type="Receivable",
|
||||||
@@ -1615,9 +1617,9 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
|
|
||||||
# assert General and Payment Ledger entries post partial reconciliation
|
# assert General and Payment Ledger entries post partial reconciliation
|
||||||
self.expected_gle = [
|
self.expected_gle = [
|
||||||
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
|
|
||||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||||
|
{"account": advance_account, "debit": 0.0, "credit": 400.0},
|
||||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||||
]
|
]
|
||||||
self.expected_ple = [
|
self.expected_ple = [
|
||||||
@@ -1628,7 +1630,7 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
"amount": -1000.0,
|
"amount": -1000.0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"account": "Debtors - _TC",
|
"account": advance_account,
|
||||||
"voucher_no": pe.name,
|
"voucher_no": pe.name,
|
||||||
"against_voucher_no": reverse_pe.name,
|
"against_voucher_no": reverse_pe.name,
|
||||||
"amount": -400.0,
|
"amount": -400.0,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"due_date",
|
"due_date",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"payment_term",
|
"payment_term",
|
||||||
|
"account_type",
|
||||||
|
"payment_type",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"total_amount",
|
"total_amount",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
@@ -108,12 +110,22 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Account Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Payment Type"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-08 07:40:38.487874",
|
"modified": "2024-04-05 09:44:08.310593",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class PaymentEntryReference(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
account: DF.Link | None
|
account: DF.Link | None
|
||||||
|
account_type: DF.Data | None
|
||||||
allocated_amount: DF.Float
|
allocated_amount: DF.Float
|
||||||
bill_no: DF.Data | None
|
bill_no: DF.Data | None
|
||||||
due_date: DF.Date | None
|
due_date: DF.Date | None
|
||||||
@@ -25,6 +26,7 @@ class PaymentEntryReference(Document):
|
|||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
payment_term: DF.Link | None
|
payment_term: DF.Link | None
|
||||||
|
payment_type: DF.Data | None
|
||||||
reference_doctype: DF.Link
|
reference_doctype: DF.Link
|
||||||
reference_name: DF.DynamicLink
|
reference_name: DF.DynamicLink
|
||||||
total_amount: DF.Float
|
total_amount: DF.Float
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
"account_currency": "USD",
|
"account_currency": "USD",
|
||||||
"account_type": "Payable",
|
"account_type": "Payable",
|
||||||
},
|
},
|
||||||
|
# 'Payable' account for capturing advance paid, under 'Assets' group
|
||||||
|
{
|
||||||
|
"attribute": "advance_payable_account",
|
||||||
|
"account_name": "Advance Paid",
|
||||||
|
"parent_account": "Current Assets - _PR",
|
||||||
|
"account_currency": "INR",
|
||||||
|
"account_type": "Payable",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for x in accounts:
|
for x in accounts:
|
||||||
@@ -1332,6 +1340,188 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
|
def test_reverse_payment_against_payment_for_supplier(self):
|
||||||
|
"""
|
||||||
|
Reconcile a payment against a reverse payment, for a supplier.
|
||||||
|
"""
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
amount = 4000
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.paid_from = self.cash
|
||||||
|
pe.paid_to = self.creditors
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
reverse_pe = self.create_payment_entry(amount=amount)
|
||||||
|
reverse_pe.party_type = "Supplier"
|
||||||
|
reverse_pe.party = self.supplier
|
||||||
|
reverse_pe.payment_type = "Receive"
|
||||||
|
reverse_pe.paid_from = self.creditors
|
||||||
|
reverse_pe.paid_to = self.cash
|
||||||
|
reverse_pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||||
|
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||||
|
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
pe.reload()
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||||
|
# There should not be any Exc Gain/Loss
|
||||||
|
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||||
|
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||||
|
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": "Exchange Gain Or Loss",
|
||||||
|
"reference_type": "Payment Entry",
|
||||||
|
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# There should be no Exchange Gain/Loss created
|
||||||
|
self.assertEqual(journals, [])
|
||||||
|
|
||||||
|
def test_advance_reverse_payment_against_payment_for_supplier(self):
|
||||||
|
"""
|
||||||
|
Reconcile an Advance payment against reverse payment, for a supplier.
|
||||||
|
"""
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
self.company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": self.advance_payable_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
amount = 4000
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=amount)
|
||||||
|
pe.party_type = "Supplier"
|
||||||
|
pe.party = self.supplier
|
||||||
|
pe.payment_type = "Pay"
|
||||||
|
pe.paid_from = self.cash
|
||||||
|
pe.paid_to = self.advance_payable_account
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
reverse_pe = self.create_payment_entry(amount=amount)
|
||||||
|
reverse_pe.party_type = "Supplier"
|
||||||
|
reverse_pe.party = self.supplier
|
||||||
|
reverse_pe.payment_type = "Receive"
|
||||||
|
reverse_pe.paid_from = self.advance_payable_account
|
||||||
|
reverse_pe.paid_to = self.cash
|
||||||
|
reverse_pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.default_advance_account = self.advance_payable_account
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||||
|
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||||
|
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
pe.reload()
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||||
|
# There should not be any Exc Gain/Loss
|
||||||
|
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||||
|
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||||
|
|
||||||
|
journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": "Exchange Gain Or Loss",
|
||||||
|
"reference_type": "Payment Entry",
|
||||||
|
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# There should be no Exchange Gain/Loss created
|
||||||
|
self.assertEqual(journals, [])
|
||||||
|
|
||||||
|
# Assert Ledger Entries
|
||||||
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||||
|
order_by="account, against_voucher, debit",
|
||||||
|
)
|
||||||
|
expected_gle = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": reverse_pe.name,
|
||||||
|
"debit": amount,
|
||||||
|
"credit": 0.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": "Cash - _PR",
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher": None,
|
||||||
|
"debit": 0.0,
|
||||||
|
"credit": amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(gl_entries, expected_gle)
|
||||||
|
pl_entries = frappe.db.get_all(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"voucher_no": pe.name},
|
||||||
|
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||||
|
order_by="account, against_voucher_no, amount",
|
||||||
|
)
|
||||||
|
expected_ple = [
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": pe.name,
|
||||||
|
"amount": amount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.advance_payable_account,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
"against_voucher_no": reverse_pe.name,
|
||||||
|
"amount": -amount,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
self.assertEqual(pl_entries, expected_ple)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
Reference in New Issue
Block a user