diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index 1d02a996b76..db475a86265 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -15,8 +15,6 @@ def execute(filters=None): columns = get_columns(filters) - filters.get("date") - data = [] if not filters.get("shareholder"): @@ -24,7 +22,7 @@ def execute(filters=None): else: share_type, no_of_shares, rate, amount = 1, 2, 3, 4 - all_shares = get_all_shares(filters.get("shareholder")) + all_shares = get_all_shares(filters.get("shareholder"), filters.get("date")) for share_entry in all_shares: row = False for datum in data: @@ -63,5 +61,28 @@ def get_columns(filters): return columns -def get_all_shares(shareholder): - return frappe.get_doc("Shareholder", shareholder).share_balance +def get_all_shares(shareholder, date): + """Share movements for the shareholder up to (and including) `date`, signed by direction: + shares received are positive, shares transferred/sold out are negative.""" + transfers = frappe.get_all( + "Share Transfer", + filters={"docstatus": 1, "date": ("<=", date)}, + fields=["share_type", "no_of_shares", "rate", "amount", "from_shareholder", "to_shareholder"], + order_by="date", + ) + + shares = [] + for transfer in transfers: + if transfer.to_shareholder == shareholder: + shares.append(transfer) + elif transfer.from_shareholder == shareholder: + shares.append( + frappe._dict( + share_type=transfer.share_type, + no_of_shares=-transfer.no_of_shares, + rate=transfer.rate, + amount=-transfer.amount, + ) + ) + + return shares diff --git a/erpnext/accounts/report/share_balance/test_share_balance.py b/erpnext/accounts/report/share_balance/test_share_balance.py index c118442d239..bb6da5a133d 100644 --- a/erpnext/accounts/report/share_balance/test_share_balance.py +++ b/erpnext/accounts/report/share_balance/test_share_balance.py @@ -103,12 +103,8 @@ class TestShareBalanceReport(ERPNextTestSuite): self.assertEqual(other_row[2], 40) self.assertEqual(other_row[4], 400) - def test_report_reflects_current_balance_regardless_of_date(self): - # NOTE: the report reads the Shareholder's current `share_balance` table - # and does NOT actually filter by the `date` value (it is required but - # unused in the computation). So an as-on date before the issue still - # shows the current holding rather than 0. This asserts the real - # behavior and flags the missing date-based filtering. + def test_as_on_date_before_issue_shows_no_holding(self): + # the report is as-on `date`: before any share transfer, the shareholder holds nothing create_share_transfer( transfer_type="Issue", to_shareholder=self.shareholder, @@ -120,8 +116,36 @@ class TestShareBalanceReport(ERPNextTestSuite): date="2026-06-01", ) - row = self.get_row(date="2020-01-01") # long before the issue - self.assertEqual(row[2], 100) + data = execute( + frappe._dict({"date": "2026-05-01", "company": COMPANY, "shareholder": self.shareholder}) + )[1] + self.assertEqual(data, []) + + def test_as_on_date_reflects_holding_up_to_that_date(self): + # two issues on different dates; an as-on date between them sees only the first + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=101, + to_no=200, + no_of_shares=100, + rate=20, + date="2026-06-10", + ) + + self.assertEqual(self.get_row(date="2026-06-05")[2], 100) # only the first issue + self.assertEqual(self.get_row(date="2026-06-15")[2], 200) # both issues def get_row(self, date, shareholder=None): filters = frappe._dict(