diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index b90f922d82b..6f5fe349dd2 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -275,12 +275,25 @@ class PartyLedgerSummaryReport: if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes": self.party_data[gle.party].opening_balance += amount else: - if amount > 0: - self.party_data[gle.party].invoiced_amount += amount - elif gle.voucher_no in self.return_invoices: - self.party_data[gle.party].return_amount -= amount + # Cache the party data reference to avoid repeated dictionary lookups + party_data = self.party_data[gle.party] + + # Check if this is a direct return invoice (most specific condition first) + if gle.voucher_no in self.return_invoices: + party_data.return_amount -= amount + # Check if this entry is against a return invoice + elif gle.against_voucher in self.return_invoices: + # For entries against return invoices, positive amounts are payments + if amount > 0: + party_data.paid_amount -= amount + else: + party_data.invoiced_amount += amount + # Normal transaction logic else: - self.party_data[gle.party].paid_amount -= amount + if amount > 0: + party_data.invoiced_amount += amount + else: + party_data.paid_amount -= amount out = [] for party, row in self.party_data.items(): @@ -289,7 +302,7 @@ class PartyLedgerSummaryReport: or row.invoiced_amount or row.paid_amount or row.return_amount - or row.closing_amount + or row.closing_balance # Fixed typo from closing_amount to closing_balance ): total_party_adjustment = sum( amount for amount in self.party_adjustment_details.get(party, {}).values() @@ -313,6 +326,7 @@ class PartyLedgerSummaryReport: gle.party, gle.voucher_type, gle.voucher_no, + gle.against_voucher, # For handling returned invoices (Credit/Debit Notes) gle.debit, gle.credit, gle.is_opening, diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py index ce47793bbd5..ca9c62dac6c 100644 --- a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -150,3 +150,157 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin): for field in expected_after_cr_and_payment: with self.subTest(field=field): self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field)) + + def test_journal_voucher_against_return_invoice(self): + filters = {"company": self.company, "from_date": today(), "to_date": today()} + + # Create Sales Invoice of 10 qty at rate 100 (Amount: 1000.0) + si1 = self.create_sales_invoice(do_not_submit=True) + si1.save().submit() + + expected = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Payment Entry (Receive) for the first invoice + pe1 = self.create_payment_entry(si1.name, True) + pe1.paid_amount = 1000 # Full payment 1000.0 + pe1.save().submit() + + expected_after_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 1000.0, + "return_amount": 0, + "closing_balance": 0.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Credit Note (return invoice) for first invoice (1000.0) + cr_note = self.create_credit_note(si1.name, do_not_submit=True) + cr_note.items[0].qty = -10 # 1 item of qty 10 at rate 100 (Amount: 1000.0) + cr_note.save().submit() + + expected_after_cr_note = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 1000.0, + "return_amount": 1000.0, + "closing_balance": -1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_note: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_note.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create Payment Entry for the returned amount (1000.0) - Pay the customer back + pe2 = get_payment_entry("Sales Invoice", cr_note.name, bank_account=self.cash) + pe2.insert().submit() + + expected_after_cr_and_return_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 1000.0, + "closing_balance": 0, + "currency": "INR", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_and_return_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_and_return_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + ) + + # Create second Sales Invoice of 10 qty at rate 100 (Amount: 1000.0) + si2 = self.create_sales_invoice(do_not_submit=True) + si2.save().submit() + + # Create Payment Entry (Receive) for the second invoice - payment (500.0) + pe3 = self.create_payment_entry(si2.name, True) + pe3.paid_amount = 500 # Partial payment 500.0 + pe3.save().submit() + + expected_after_cr_and_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0.0, + "invoiced_amount": 2000.0, + "paid_amount": 500.0, + "return_amount": 1000.0, + "closing_balance": 500.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_and_payment: + with self.subTest(field=field): + actual_value = report[0].get(field) + expected_value = expected_after_cr_and_payment.get(field) + self.assertEqual( + actual_value, + expected_value, + f"Field {field} does not match expected value. " + f"Expected: {expected_value}, Got: {actual_value}", + )