From f68ab3dfff50a2161858f072b33f75c306a731f4 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 7 Jun 2023 15:14:24 -0400 Subject: [PATCH 1/3] test: reconcile credit against invoice --- .../test_payment_reconciliation.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 3be11ae31a..e4efc6894f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate 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.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item +test_dependencies = ["Item"] + class TestPaymentReconciliation(FrappeTestCase): def setUp(self): @@ -163,7 +166,7 @@ class TestPaymentReconciliation(FrappeTestCase): def create_payment_reconciliation(self): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = "Customer" + pr.party_type = self.party_type or "Customer" pr.party = self.customer pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() @@ -890,6 +893,42 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + def test_reconciliation_purchase_invoice_against_return(self): + pi = make_purchase_invoice( + supplier="_Test Supplier USD", currency="USD", conversion_rate=50 + ).submit() + + pi_return = frappe.get_doc(pi.as_dict()) + pi_return.name = None + pi_return.docstatus = 0 + pi_return.is_return = 1 + pi_return.conversion_rate = 80 + pi_return.items[0].qty = -pi_return.items[0].qty + pi_return.submit() + + self.company = "_Test Company" + self.party_type = "Supplier" + self.customer = "_Test Supplier USD" + + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + + invoices = [] + payments = [] + for invoice in pr.invoices: + if invoice.invoice_number == pi.name: + invoices.append(invoice.as_dict()) + break + for payment in pr.payments: + if payment.reference_name == pi_return.name: + payments.append(payment.as_dict()) + break + + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): From 7973951c370de0bc95c82ed132b109cdade9f2b9 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 7 Jun 2023 15:55:16 -0400 Subject: [PATCH 2/3] fix: missing attribute error --- .../payment_reconciliation/test_payment_reconciliation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index e4efc6894f..2ac7df0e39 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -166,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase): def create_payment_reconciliation(self): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = self.party_type or "Customer" + pr.party_type = ( + self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" + ) pr.party = self.customer pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() From 54935438e127d984ca28ddac4cda84e090e7f72a Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 7 Jun 2023 15:55:37 -0400 Subject: [PATCH 3/3] fix: reconcile invoice against credit note --- .../payment_reconciliation.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cc2b9420cc..77adf45911 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -316,6 +316,7 @@ class PaymentReconciliation(Document): entry_list = [] dr_or_cr_notes = [] + difference_entries = [] for row in self.get("allocation"): reconciled_entry = [] if row.invoice_number and row.allocated_amount: @@ -328,13 +329,15 @@ class PaymentReconciliation(Document): reconciled_entry.append(payment_details) if payment_details.difference_amount: - self.make_difference_entry(payment_details) + difference_entries.append( + self.make_difference_entry(payment_details, do_not_save_and_submit=bool(dr_or_cr_notes)) + ) if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, difference_entries, self.company) @frappe.whitelist() def reconcile(self): @@ -362,7 +365,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() - def make_difference_entry(self, row): + def make_difference_entry(self, row, do_not_save_and_submit=False): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company @@ -410,8 +413,11 @@ class PaymentReconciliation(Document): journal_entry.append("accounts", journal_account) - journal_entry.save() - journal_entry.submit() + if not do_not_save_and_submit: + journal_entry.save() + journal_entry.submit() + + return journal_entry def get_payment_details(self, row, dr_or_cr): return frappe._dict( @@ -577,7 +583,14 @@ class PaymentReconciliation(Document): return condition -def reconcile_dr_cr_note(dr_cr_notes, company): +def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company): + def find_difference_entry(voucher_type, voucher_no): + for jv in difference_entries: + accounts = iter(jv.accounts) + for account in accounts: + if account.reference_type == voucher_type and account.reference_name == voucher_no: + return next(accounts) + for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -622,5 +635,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ], } ) + + if difference_entry := find_difference_entry(inv.against_voucher_type, inv.against_voucher): + jv.append("accounts", difference_entry) + jv.flags.ignore_mandatory = True jv.submit()