diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index e55f217682d..bbffaa7d015 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -151,7 +151,13 @@ def _execute(filters, additional_table_columns=None): ) if inv.doctype == "Sales Invoice": - row.update({"debit": inv.base_grand_total, "credit": 0.0}) + # credit only settlements the invoice itself posts to the receivable (mirrors its GL) + row.update( + { + "debit": inv.base_grand_total, + "credit": get_in_invoice_receivable_credit(inv), + } + ) else: row.update({"debit": 0.0, "credit": inv.base_grand_total}) data.append(row) @@ -167,6 +173,14 @@ def _execute(filters, additional_table_columns=None): return columns, res, None, None, None, include_payments +def get_in_invoice_receivable_credit(inv): + # amount the invoice settles against its own receivable, matching the invoice's GL entries + credit = flt(inv.loyalty_amount) # loyalty redemption, POS or not + if inv.is_pos: # POS payments and write-off credit the receivable only on POS invoices + credit += flt(inv.base_paid_amount) - flt(inv.base_change_amount) + flt(inv.base_write_off_amount) + return credit + + def get_columns(invoice_list, additional_table_columns, include_payments=False): """return columns based on filters""" columns = [ @@ -433,6 +447,11 @@ def get_invoices(filters, additional_query_columns): si.base_net_total, si.base_grand_total, si.base_rounded_total, + si.is_pos, + si.base_paid_amount, + si.base_change_amount, + si.base_write_off_amount, + si.loyalty_amount, si.outstanding_amount, si.is_internal_customer, si.represents_company, diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 9e72f81f6e5..3b8b14a763d 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -1,7 +1,8 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import getdate, today +from frappe.utils import flt, getdate, today +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.sales_register.sales_register import execute from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -54,6 +55,46 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): si = si.submit() return si + def test_ledger_view_nets_pos_paid_invoice(self): + # A POS payment settles the receivable inside the invoice, so the ledger view must credit it + # and net to zero instead of showing a phantom outstanding. + make_pos_profile() + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + si.is_pos = 1 + si.append("payments", {"mode_of_payment": "Cash", "amount": 100}) + si = si.save().submit() + self.assertEqual(flt(si.outstanding_amount), 0.0) + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + } + ) + rows = execute(filters)[1] + inv_row = next(x for x in rows if x.get("voucher_no") == si.name) + + self.assertEqual(flt(inv_row.get("debit")), 100.0) + self.assertEqual(flt(inv_row.get("credit")), 100.0) + + # running balance is unchanged by a fully-paid POS invoice + idx = rows.index(inv_row) + self.assertEqual(flt(inv_row.get("balance")), flt(rows[idx - 1].get("balance"))) + def test_basic_report_output(self): si = self.create_sales_invoice(rate=98)